How to pass lambda 'include' with multiple levels in Entity Framework Core?

前端 未结 3 484
终归单人心
终归单人心 2020-12-06 08:25

I have a repository that gets a lambda expression for \'include\'.

public TEntity FirstOrDefault(Expression> predicate, para         


        
相关标签:
3条回答
  • 2020-12-06 09:02

    Entity Framework core sacrificed ease of parametrization for a more comprehensible API. Indeed, in EF6 it was much easier to pass multi-level Include expressions to a method. In ef-core that's virtually impossible.

    But the Include method accepting a property path as string still exists, so if we can convert the old-style multi-level Include expression to a path, we can feed the path into this string-based Include.

    Fortunately, this is exactly what happened under the hood in EF6. And since EF6 is open source, I didn't have to reinvent the wheel but could easily borrow their code to achieve what we want. The result is an extension method AsPath that returns a lambda expression as a property path. You can use it inside your method to convert the includes parameter to a sequence of strings by which you can add the Includes. For example, the expression ...

     include => include.PlanSolutions.Select(ps => ps.Solution)
    

    ... will be converted into PlanSolutions.Solution.

    As said: credits to EF6 for the core part of the source. The only major modification is that my method throws exceptions in two of the most commonly attempted unsupported features: filtering and ordering an Include. (Still not supported in ef-core).

    public static class ExpressionExtensions
    {
        public static string AsPath(this LambdaExpression expression)
        {
            if (expression == null) return null;
    
            var exp = expression.Body;
            string path;
            TryParsePath(exp, out path);
            return path;
        }
    
        // This method is a slight modification of EF6 source code
        private static bool TryParsePath(Expression expression, out string path)
        {
            path = null;
            var withoutConvert = RemoveConvert(expression);
            var memberExpression = withoutConvert as MemberExpression;
            var callExpression = withoutConvert as MethodCallExpression;
    
            if (memberExpression != null)
            {
                var thisPart = memberExpression.Member.Name;
                string parentPart;
                if (!TryParsePath(memberExpression.Expression, out parentPart))
                {
                    return false;
                }
                path = parentPart == null ? thisPart : (parentPart + "." + thisPart);
            }
            else if (callExpression != null)
            {
                if (callExpression.Method.Name == "Select"
                    && callExpression.Arguments.Count == 2)
                {
                    string parentPart;
                    if (!TryParsePath(callExpression.Arguments[0], out parentPart))
                    {
                        return false;
                    }
                    if (parentPart != null)
                    {
                        var subExpression = callExpression.Arguments[1] as LambdaExpression;
                        if (subExpression != null)
                        {
                            string thisPart;
                            if (!TryParsePath(subExpression.Body, out thisPart))
                            {
                                return false;
                            }
                            if (thisPart != null)
                            {
                                path = parentPart + "." + thisPart;
                                return true;
                            }
                        }
                    }
                }
                else if (callExpression.Method.Name == "Where")
                {
                    throw new NotSupportedException("Filtering an Include expression is not supported");
                }
                else if (callExpression.Method.Name == "OrderBy" || callExpression.Method.Name == "OrderByDescending")
                {
                    throw new NotSupportedException("Ordering an Include expression is not supported");
                }
                return false;
            }
    
            return true;
        }
    
        // Removes boxing
        private static Expression RemoveConvert(Expression expression)
        {
            while (expression.NodeType == ExpressionType.Convert
                   || expression.NodeType == ExpressionType.ConvertChecked)
            {
                expression = ((UnaryExpression)expression).Operand;
            }
    
            return expression;
        }
    }
    
    0 讨论(0)
  • 2020-12-06 09:10
        public TEntity FirstOrDefault(Expression<Func<TEntity, bool>> predicate, params Expression<Func<TEntity, object>>[] includePaths)
            {
    DbSet = Context.Set<TEntity>();
        var query = includePaths.Aggregate(DbSet, (current, item) => EvaluateInclude(current, item));
                 return query.Where(predicate).FirstOrDefault();
            }
    
        private IQueryable<T> EvaluateInclude(IQueryable<T> current, Expression<Func<T, object>> item)
                    {
                        if (item.Body is MethodCallExpression)
                        {
                            var arguments = ((MethodCallExpression)item.Body).Arguments;
                            if (arguments.Count > 1)
                            {
                                var navigationPath = string.Empty;
                                for (var i = 0; i < arguments.Count; i++)
                                {
                                    var arg = arguments[i];
                                    var path = arg.ToString().Substring(arg.ToString().IndexOf('.') + 1);
    
                                    navigationPath += (i > 0 ? "." : string.Empty) + path;
                                }
                                return current.Include(navigationPath);
                            }
                        }
    
                        return current.Include(item);
                    }
    
    0 讨论(0)
  • 2020-12-06 09:23

    The accepted answer is a bit outdated. In newer versions of Entity Framework Core you should be able to use the ThenInclude method as described here.

    The sample for this post would become

    var plan = _unitOfWork.PlanRepository
                .Include(x => x.PlanSolutions)
                .ThenInclude(x => x.Solution)
                .FirstOrDefault(p => p.Id == id);
    
    0 讨论(0)
提交回复
热议问题