如何在 EF Core 2 中为 .ThenInclude 编写 Repository 方法

How to write Repository method for .ThenInclude in EF Core 2

我正在尝试为 Entity Framework Core 2.0 编写一个存储库方法,它可以使用 .ThenInclude 处理 returning child 属性集合,但我遇到了问题第二个表达。这是 .Include 的工作方法,它将 return child 实体的属性(您提供 lambda 列表)。

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();
}

现在这是我尝试编写一个方法,该方法将采用两个表达式的元组并将它们提供给 .Include(a => a.someChild).ThenInclude(b => b.aChildOfSomeChild ) 链。这不是一个完美的解决方案,因为它只处理 child 中的一个 child,但这是一个开始。

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 return 一个错误说 "The type cannot be inferred from the usage, try specifying the type explicitly"。我有一种感觉,因为 Item2 中的表达式需要归类为与 Item1 有某种关联,因为它需要知道它具有的 child 关系。

编写这样的方法有什么想法或更好的技巧吗?

我遇到了同样的问题,因为 EF Core 不支持延迟加载,但我尝试通过以下方式获得解决方法:

首先创建一个属性 class 以从给定 class 的其他属性中标记我们想要的导航属性。

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

用于过滤导航属性并应用 Include/ThenInclude 使用基于字符串的预加载的扩展方法。

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);
            }
        }
    }
}

示例 POCO classes 实现 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; }
}

存储库中的使用

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;
}

Json 样本 class C 的结果将是:

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

我在网上找到了这个存储库方法,它完全符合我的要求。 Yared 的回答很好,但并非一直如此。

/// <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();
    }
}

用法:

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));

回到 EF6,我们可以这样写:

query.Include(t => t.Navigation1, t => t.Navigation2.Select(x => x.Child1));

它既完美又简单。我们可以在存储库中公开它,而无需将引用从 EF 程序集拖到其他项目。

这已从 EF Core 中删除,但由于 EF6 是开源的,因此可以轻松提取转换路径中的 lambda 表达式的方法以在 EF Core 中使用,因此您可以获得完全相同的行为。

这里是完整的扩展方法。

/// <summary>
///     Provides extension methods to the <see cref="Expression" /> class.
/// </summary>
public static class ExpressionExtensions
{
    /// <summary>
    ///     Converts the property accessor lambda expression to a textual representation of it's path. <br />
    ///     The textual representation consists of the properties that the expression access flattened and separated by a dot character (".").
    /// </summary>
    /// <param name="expression">The property selector expression.</param>
    /// <returns>The extracted textual representation of the expression's path.</returns>
    public static string AsPath(this LambdaExpression expression)
    {
        if (expression == null)
            return null;

        TryParsePath(expression.Body, out var path);

        return path;
    }

    /// <summary>
    ///     Recursively parses an expression tree representing a property accessor to extract a textual representation of it's path. <br />
    ///     The textual representation consists of the properties accessed by the expression tree flattened and separated by a dot character (".").
    /// </summary>
    /// <param name="expression">The expression tree to parse.</param>
    /// <param name="path">The extracted textual representation of the expression's path.</param>
    /// <returns>True if the parse operation succeeds; otherwise, false.</returns>
    private static bool TryParsePath(Expression expression, out string path)
    {
        var noConvertExp = RemoveConvertOperations(expression);
        path = null;

        switch (noConvertExp)
        {
            case MemberExpression memberExpression:
            {
                var currentPart = memberExpression.Member.Name;

                if (!TryParsePath(memberExpression.Expression, out var parentPart))
                    return false;

                path = string.IsNullOrEmpty(parentPart) ? currentPart : string.Concat(parentPart, ".", currentPart);
                break;
            }

            case MethodCallExpression callExpression:
                switch (callExpression.Method.Name)
                {
                    case nameof(Queryable.Select) when callExpression.Arguments.Count == 2:
                    {
                        if (!TryParsePath(callExpression.Arguments[0], out var parentPart))
                            return false;

                        if (string.IsNullOrEmpty(parentPart))
                            return false;

                        if (!(callExpression.Arguments[1] is LambdaExpression subExpression))
                            return false;

                        if (!TryParsePath(subExpression.Body, out var currentPart))
                            return false;

                        if (string.IsNullOrEmpty(parentPart))
                            return false;

                        path = string.Concat(parentPart, ".", currentPart);
                        return true;
                    }

                    case nameof(Queryable.Where):
                        throw new NotSupportedException("Filtering an Include expression is not supported");
                    case nameof(Queryable.OrderBy):
                    case nameof(Queryable.OrderByDescending):
                        throw new NotSupportedException("Ordering an Include expression is not supported");
                    default:
                        return false;
                }
        }

        return true;
    }

    /// <summary>
    ///     Removes all casts or conversion operations from the nodes of the provided <see cref="Expression" />.
    ///     Used to prevent type boxing when manipulating expression trees.
    /// </summary>
    /// <param name="expression">The expression to remove the conversion operations.</param>
    /// <returns>The expression without conversion or cast operations.</returns>
    private static Expression RemoveConvertOperations(Expression expression)
    {
        while (expression.NodeType == ExpressionType.Convert || expression.NodeType == ExpressionType.ConvertChecked)
            expression = ((UnaryExpression)expression).Operand;

        return expression;
    }
}

然后你可以像这样使用它(把它放在 QueryableExtensions class 或类似的东西中):

 /// <summary>
 ///     Specifies related entities to include in the query result.
 /// </summary>
 /// <typeparam name="T">The type of entity being queried.</typeparam>
 /// <param name="source">The source <see cref="IQueryable{T}" /> on which to call Include.</param>
 /// <param name="paths">The lambda expressions representing the paths to include.</param>
 /// <returns>A new <see cref="IQueryable{T}" /> with the defined query path.</returns>
 internal static IQueryable<T> Include<T>(this IQueryable<T> source, params Expression<Func<T, object>>[] paths)
 {
     if (paths != null)
         source = paths.Aggregate(source, (current, include) => current.Include(include.AsPath()));

     return source;
 }

然后在您的存储库中,您可以像在 EF6 中一样正常调用它:

query.Include(t => t.Navigation1, t => t.Navigation2.Select(x => x.Child1));

参考文献:

https://github.com/aspnet/EntityFramework6