Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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

public TEntity FirstOrDefault(Expression<Func<TEntity, bool>> predicate, params Expression<Func<TEntity, object>>[] includePaths)
    {
        return Context.Set<TEntity>().Includes(includePaths).FirstOrDefault(predicate);
    }

In previous versions of EF I used it in services layers like:

var plan = _unitOfWork.PlanRepository
            .FirstOrDefault(
                p => p.Id == id, 
                include => include.PlanSolutions.Select(ps => ps.Solution)
            );

Where 'PlanSolutions' is a collection and the 'Solution' is a property nested from 'PlanSolution'.

But now this code gets an error:

InvalidOperationException: The property expression 'include => {from PlanSolutions ps in [include].PlanSolutions select [ps].Solution}' is not valid. The expression should represent a property access: 't => t.MyProperty'. For more information on including related data, see http://go.microsoft.com/fwlink/?LinkID=746393.

Now it seems that I can't use 'Select' method for getting multiple levels include, but I also can't use 'ThenInclude' method that Microsoft suggests, because the query by itself located inside of the repository where service doesn't have an access to. Is there any way to heal it?

like image 991
Igor Avatar asked Nov 01 '17 10:11

Igor


3 Answers

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;
    }
}
like image 65
Gert Arnold Avatar answered Oct 22 '22 18:10

Gert Arnold


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);
like image 43
emp Avatar answered Oct 22 '22 17:10

emp


    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);
                }
like image 38
h3n Avatar answered Oct 22 '22 16:10

h3n