I have a repository that gets a lambda expression for \'include\'.
public TEntity FirstOrDefault(Expression> predicate, para
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;
}
}
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);
}
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);