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

asp.net c# entity-framework-core include lambda

Question

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?

1
2
11/1/2017 10:09:23 AM

Accepted Answer

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;
    }
}
6
11/1/2017 8:48:53 PM

Popular Answer

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


Related Questions





Related

Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow
Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow