C# 如何为.编写存储库方法,然后将其包含在EF Core 2中

C# 如何为.编写存储库方法,然后将其包含在EF Core 2中,c#,lambda,.net-core,entity-framework-core,repository-pattern,C#,Lambda,.net Core,Entity Framework Core,Repository Pattern,我正在尝试为Entity Framework Core 2.0编写一个repository方法,该方法可以使用.ThenClude处理返回属性的子集合,但第二个表达式有问题。以下是.Include的工作方法,它将返回实体的子属性(您提供了lambda列表) public T GetSingle(Expression<Func<T, bool>> predicate, params Expression<Func<T, object>>[] incl

我正在尝试为Entity Framework Core 2.0编写一个repository方法,该方法可以使用.ThenClude处理返回属性的子集合,但第二个表达式有问题。以下是.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();
}
public T GetSingle(表达式谓词,参数表达式[]includeProperties)
{
IQueryable查询=_context.Set();
foreach(includeProperty中的var includeProperty)
{
query=query.Include(includeProperty);
} 
返回query.Where(谓词).FirstOrDefault();
}
现在,我尝试编写一个方法,该方法将获取两个表达式的元组,并将它们馈送到.Include(a=>a.someChild).然后Include(b=>b.aChildOfSomeChild)链中。这不是一个完美的解决方案,因为它只处理一个孩子的一个孩子,但这只是一个开始

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();
}
public T GetSingle(表达式谓词,参数元组[]includeProperties)
{
IQueryable查询=_context.Set();
foreach(includeProperty中的var includeProperty)
{
query=query.Include(includeProperty.Item1),然后Include(includeProperty.Item2);
}
返回query.Where(谓词).FirstOrDefault();
}
Intellisense返回一个错误,提示“无法从用法推断类型,请尝试显式指定类型”。我有一种感觉,这是因为Item2中的表达式需要被归类为与Item1有某种关联,因为它需要知道它的子关系


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

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

首先创建一个属性类,从给定类的其他属性中标记所需的导航属性

[AttributeUsage(AttributeTargets.Property, Inherited = false)]
public class NavigationPropertyAttribute : Attribute
{
    public NavigationPropertyAttribute()
    {
    }
}
使用基于字符串的即时加载筛选出导航属性并应用Include/thenclude的扩展方法

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);
            }
        }
    }
}
存储库中的使用

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

我在网上找到了这个存储库方法,它正是我想要的。亚瑞德的回答是好的,但并非完全正确

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

回到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;
    }
}
然后在存储库中,您可以像在EF6中一样正常地调用它:

query.Include(t => t.Navigation1, t => t.Navigation2.Select(x => x.Child1));
参考资料:


这已经被问了好几次了,因为它实际上是使用EF6指定所需Include to repository方法的标准。听到一些EFC团队成员将模式更改为
Include
/
然后Include
的决定背后的原因是什么会很有趣,这显然不能以这种方式表示,更重要的是,EFC的替代品是什么。@IvanStoev你在EFC中找到替代品了吗?@DavidG根据他们自己提供的扩展的源代码,他们的愿景中的替代品是
Func include=null
参数。@IvanStoev Hmm,害怕类似的东西,不幸的是不适合我的用例。非常感谢您的回复。(顺便说一句,这个扩展不是微软做的,我想他们只是喜欢链接到它)@DavidG我想你是对的。我假设“他们”是基于微软的,名字是:),但现在我看到它和OP的自我回答是一样的。无论如何,如果你想保留旧的语法,EF6的源代码是可用的,可以相对容易地翻译成
包含
/
然后包含
语法。这里甚至有这样的帖子,只是不记得确切的帖子。虽然这样做有效,但它公开了一个实体框架类,这可能并不可取。有时候,知道什么时候保持简单易读是值得的。实在的和过度的抽象肯定会走得太远。而且,我也愿意接受一些替代方案,因为这个问题没有太多的答案。非常好的方法,你在哪里找到的?我想我在这里找到了:@SebastianR,但如果你把它变成一个通用的抽象类,后代存储库从中继承,你可以让每个子存储库公开IEnumerable,对吗?
query.Include(t => t.Navigation1, t => t.Navigation2.Select(x => x.Child1));
/// <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;
    }
}
 /// <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;
 }
query.Include(t => t.Navigation1, t => t.Navigation2.Select(x => x.Child1));