web-dev-qa-db-fra.com

Comment écrire la méthode du référentiel pour .ThenInclude dans EF Core 2

J'essaie d'écrire une méthode de référentiel pour Entity Framework Core 2.0 qui peut gérer le retour des collections enfant de propriétés à l'aide de .ThenInclude, mais j'ai des problèmes avec la deuxième expression. Voici une méthode de travail pour .Include, qui renverra les propriétés enfants (vous fournissez une liste de lambdas) de votre entité.

public T GetSingle(Expression<Func<T, bool>> predicate, params Expression<Func<T, object>>[] includeProperties)
{
    IQueryable<T> query = _context.Set<T>();
    foreach (var includeProperty in includeProperties)
    {
        query = query.Include(includeProperty);
    } 

    return query.Where(predicate).FirstOrDefault();
}

Voici maintenant ma tentative d'écrire une méthode qui prendra un tuple de deux expressions et les injectera dans une chaîne .Include (a => a.someChild) .ThenInclude (b => b.aChildOfSomeChild). Ce n'est pas une solution parfaite car il ne gère qu'un enfant d'un enfant, mais c'est un début.

public T GetSingle(Expression<Func<T, bool>> predicate, params Tuple<Expression<Func<T, object>>, Expression<Func<T, object>>>[] includeProperties)
{
    IQueryable<T> query = _context.Set<T>();
    foreach (var includeProperty in includeProperties)
    {
         query = query.Include(includeProperty.Item1).ThenInclude(includeProperty.Item2);              
    }

    return query.Where(predicate).FirstOrDefault();
}

Intellisense renvoie une erreur indiquant "Le type ne peut pas être déduit de l'utilisation, essayez de spécifier le type explicitement". J'ai le sentiment que c'est parce que l'expression dans Item2 doit être classée comme étant en quelque sorte liée à Item1, car elle doit connaître la relation enfant qu'elle a.

Des idées ou de meilleures techniques pour écrire une méthode comme celle-ci?

14
redwards510

J'ai trouvé cette méthode de dépôt en ligne et elle fait exactement ce que je voulais. La réponse de Yared était bonne, mais pas tout à fait là-bas.

/// <summary>
/// Gets the first or default entity based on a predicate, orderby delegate and include delegate. This method default no-tracking query.
/// </summary>
/// <param name="selector">The selector for projection.</param>
/// <param name="predicate">A function to test each element for a condition.</param>
/// <param name="orderBy">A function to order elements.</param>
/// <param name="include">A function to include navigation properties</param>
/// <param name="disableTracking"><c>True</c> to disable changing tracking; otherwise, <c>false</c>. Default to <c>true</c>.</param>
/// <returns>An <see cref="IPagedList{TEntity}"/> that contains elements that satisfy the condition specified by <paramref name="predicate"/>.</returns>
/// <remarks>This method default no-tracking query.</remarks>
public TResult GetFirstOrDefault<TResult>(Expression<Func<TEntity, TResult>> selector,
                                          Expression<Func<TEntity, bool>> predicate = null,
                                          Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>> orderBy = null,
                                          Func<IQueryable<TEntity>, IIncludableQueryable<TEntity, object>> include = null,
                                          bool disableTracking = true)
{
    IQueryable<TEntity> query = _dbSet;
    if (disableTracking)
    {
        query = query.AsNoTracking();
    }

    if (include != null)
    {
        query = include(query);
    }

    if (predicate != null)
    {
        query = query.Where(predicate);
    }

    if (orderBy != null)
    {
        return orderBy(query).Select(selector).FirstOrDefault();
    }
    else
    {
        return query.Select(selector).FirstOrDefault();
    }
}

Usage:

var affiliate = await affiliateRepository.GetFirstOrDefaultAsync(
    predicate: b => b.Id == id,
    include: source => source
        .Include(a => a.Branches)
        .ThenInclude(a => a.Emails)
        .Include(a => a.Branches)
        .ThenInclude(a => a.Phones));
26
redwards510

J'ai eu le même problème car EF Core ne prend pas en charge le chargement paresseux, mais j'ai essayé de contourner le problème de la manière suivante:

Créez d'abord une classe d'attributs pour marquer nos propriétés de navigation souhaitées à partir d'autres propriétés d'une classe donnée.

[AttributeUsage(AttributeTargets.Property, Inherited = false)]
public class NavigationPropertyAttribute : Attribute
{
    public NavigationPropertyAttribute()
    {
    }
}

Méthodes d'extension pour filtrer les propriétés de navigation et appliquer Include/ThenInclude à l'aide du chargement Eager basé sur une chaîne.

public static class DbContextHelper
{

    public static Func<IQueryable<T>, IQueryable<T>> GetNavigations<T>() where T : BaseEntity
    {
        var type = typeof(T);
        var navigationProperties = new List<string>();

        //get navigation properties
        GetNavigationProperties(type, type, string.Empty, navigationProperties);

        Func<IQueryable<T>, IQueryable<T>> includes = ( query => {
                    return  navigationProperties.Aggregate(query, (current, inc) => current.Include(inc));   
            });

        return includes;
    }

    private static void GetNavigationProperties(Type baseType, Type type, string parentPropertyName, IList<string> accumulator)
    {
        //get navigation properties
        var properties = type.GetProperties();
        var navigationPropertyInfoList = properties.Where(prop => prop.IsDefined(typeof(NavigationPropertyAttribute)));

        foreach (PropertyInfo prop in navigationPropertyInfoList)
        {
            var propertyType = prop.PropertyType;
            var elementType = propertyType.GetTypeInfo().IsGenericType ? propertyType.GetGenericArguments()[0] : propertyType;

            //Prepare navigation property in {parentPropertyName}.{propertyName} format and Push into accumulator
            var properyName = string.Format("{0}{1}{2}", parentPropertyName, string.IsNullOrEmpty(parentPropertyName) ? string.Empty : ".", prop.Name);
            accumulator.Add(properyName);

            //Skip recursion of propert has JsonIgnore attribute or current property type is the same as baseType
            var isJsonIgnored = prop.IsDefined(typeof(JsonIgnoreAttribute));
            if(!isJsonIgnored && elementType != baseType){
                GetNavigationProperties(baseType, elementType, properyName, accumulator);
            }
        }
    }
}

Exemples de classes POCO implémentant NavigationPropertyAttribute

public class A : BaseEntity{
  public string Prop{ get; set; }
}

public class B : BaseEntity{
   [NavigationProperty]
   public virtual A A{ get; set; }
}

public class C : BaseEntity{
   [NavigationProperty]
   public virtual B B{ get; set; }
}

Utilisation dans le référentiel

public async Task<T> GetAsync(Expression<Func<T, bool>> predicate)
{
    Func<IQueryable<T>, IQueryable<T>> includes = DbContextHelper.GetNavigations<T>();
    IQueryable<T> query = _context.Set<T>();
    if (includes != null)
    {
        query = includes(query);
    }

    var entity = await query.FirstOrDefaultAsync(predicate);
    return entity;
}

Le résultat Json pour l'échantillon de classe C serait:

{
  "B" : {
        "A" : {
              "Prop" : "SOME_VALUE"
            }
      }
} 
5
Yared