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?
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 Include
s. 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;
}
}
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);
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);
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With