简体   繁体   English

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

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

I'm trying to write a repository method for Entity Framework Core 2.0 that can handle returning child collections of properties using.ThenInclude, but I'm having trouble with the second expression.我正在尝试为 Entity Framework Core 2.0 编写一个存储库方法,该方法可以使用 .ThenInclude 处理返回的子属性 collections,但我在使用第二个表达式时遇到了问题。 Here is a working method for.Include, which will return child properties (you supply a list of lambdas) of your entity.这是 for.Include 的工作方法,它将返回实体的子属性(您提供 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();
}

Now here is my attempt at writing a method that will take a Tuple of two Expressions and feed those into a.Include(a => a.someChild).ThenInclude(b => b.aChildOfSomeChild) chain.现在这是我尝试编写一种方法,该方法将采用两个表达式的元组并将它们提供给 a.Include(a => a.someChild).ThenInclude(b => b.aChildOfSomeChild) 链。 This isn't a perfect solution because it only handles one child of a child, but it's a start.这不是一个完美的解决方案,因为它只处理一个孩子的一个孩子,但这是一个开始。

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 returns an error saying "The type cannot be inferred from the usage, try specifying the type explicitly". Intellisense 返回错误消息“无法从用法中推断出类型,请尝试明确指定类型”。 I have a feeling it's because the expression in Item2 needs to be classified as somehow related to Item1, because it needs to know about the child relationship it has.我有一种感觉,因为 Item2 中的表达式需要归类为与 Item1 有某种关联,因为它需要知道它具有的子关系。

Any ideas or better techniques for writing a method like this?编写这样的方法有什么想法或更好的技术吗?

I found this repository method online and it does exactly what I wanted.我在网上找到了这个存储库方法,它完全符合我的要求。 Yared's answer was good, but not all the way there. 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();
    }
}

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

I had the same issue since EF Core doesn't support lazy loading but i tried to get workaround in the following way:我遇到了同样的问题,因为 EF Core 不支持延迟加载,但我尝试通过以下方式解决:

First create an attribute class to mark our desired navigation properties from other properties of a given class.首先创建一个属性类,从给定类的其他属性中标记我们想要的导航属性。

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

Extension methods to filter out navigation properties and apply Include/ThenInclude using string based Eager loading.用于过滤导航属性并使用基于字符串的 Eager 加载应用 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);
            }
        }
    }
}

Sample POCO classes implementing NavigationPropertyAttribute实现NavigationPropertyAttribute示例 POCO 类

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

Usage in Repository在存储库中的使用

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 result for sample class C would be:样本类 C 的 Json 结果将是:

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

Back in EF6 we could write something like this:回到 EF6,我们可以这样写:

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

And it was perfect and simple.它完美而简单。 We could expose it in an repository without dragging references from the EF assembly to other projects.我们可以在存储库中公开它,而无需将引用从 EF 程序集中拖到其他项目中。

This was removed from EF Core, but since EF6 is open-source, the method that transforms the lambda expressions in paths can easily be extracted to use in EF Core so you can get the exact same behavior.这已从 EF Core 中删除,但由于 EF6 是开源的,因此可以轻松提取在路径中转换 lambda 表达式的方法以在 EF Core 中使用,因此您可以获得完全相同的行为。

Here's the complete extension method.这是完整的扩展方法。

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

Then you can use it like this (put it in an QueryableExtensions class or something like that):然后你可以像这样使用它(把它放在一个QueryableExtensions类或类似的东西中):

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

And then in your repository you call it normally like you would do in EF6:然后在您的存储库中,您可以像在 EF6 中一样正常调用它:

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

References:参考资料:

How to pass lambda 'include' with multiple levels in Entity Framework Core? 如何在 Entity Framework Core 中以多个级别传递 lambda“include”?

https://github.com/aspnet/EntityFramework6 https://github.com/aspnet/EntityFramework6

When I need.ThenInclude I add my dbcontext class as dependency injection and write my query directly from dbcontext reference.当我需要时。ThenInclude 我将我的 dbcontext class 添加为依赖项注入,并直接从 dbcontext 引用编写我的查询。 I don't know if it is good or bad practice.我不知道这是好习惯还是坏习惯。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM