Back in EF6 we could write something like this:
query.Include(t => t.Navigation1, t => t.Navigation2.Select(x => x.Child1));
And it was perfect and simple. We could expose it in an repository without dragging references from the EF assembly to other projects.
This was removed from EF Core, but since EF6 is open-source, the method that transforms the lambda expressions in paths can easily be extracted to use in EF Core so you can get the exact same behavior.
Here's the complete extension method.
/// <summary>
/// Provides extension methods to the <see cref="Expression" /> class.
/// </summary>
public static class ExpressionExtensions
{
/// <summary>
/// Converts the property accessor lambda expression to a textual representation of it's path. <br />
/// The textual representation consists of the properties that the expression access flattened and separated by a dot character (".").
/// </summary>
/// <param name="expression">The property selector expression.</param>
/// <returns>The extracted textual representation of the expression's path.</returns>
public static string AsPath(this LambdaExpression expression)
{
if (expression == null)
return null;
TryParsePath(expression.Body, out var path);
return path;
}
/// <summary>
/// Recursively parses an expression tree representing a property accessor to extract a textual representation of it's path. <br />
/// The textual representation consists of the properties accessed by the expression tree flattened and separated by a dot character (".").
/// </summary>
/// <param name="expression">The expression tree to parse.</param>
/// <param name="path">The extracted textual representation of the expression's path.</param>
/// <returns>True if the parse operation succeeds; otherwise, false.</returns>
private static bool TryParsePath(Expression expression, out string path)
{
var noConvertExp = RemoveConvertOperations(expression);
path = null;
switch (noConvertExp)
{
case MemberExpression memberExpression:
{
var currentPart = memberExpression.Member.Name;
if (!TryParsePath(memberExpression.Expression, out var parentPart))
return false;
path = string.IsNullOrEmpty(parentPart) ? currentPart : string.Concat(parentPart, ".", currentPart);
break;
}
case MethodCallExpression callExpression:
switch (callExpression.Method.Name)
{
case nameof(Queryable.Select) when callExpression.Arguments.Count == 2:
{
if (!TryParsePath(callExpression.Arguments[0], out var parentPart))
return false;
if (string.IsNullOrEmpty(parentPart))
return false;
if (!(callExpression.Arguments[1] is LambdaExpression subExpression))
return false;
if (!TryParsePath(subExpression.Body, out var currentPart))
return false;
if (string.IsNullOrEmpty(parentPart))
return false;
path = string.Concat(parentPart, ".", currentPart);
return true;
}
case nameof(Queryable.Where):
throw new NotSupportedException("Filtering an Include expression is not supported");
case nameof(Queryable.OrderBy):
case nameof(Queryable.OrderByDescending):
throw new NotSupportedException("Ordering an Include expression is not supported");
default:
return false;
}
}
return true;
}
/// <summary>
/// Removes all casts or conversion operations from the nodes of the provided <see cref="Expression" />.
/// Used to prevent type boxing when manipulating expression trees.
/// </summary>
/// <param name="expression">The expression to remove the conversion operations.</param>
/// <returns>The expression without conversion or cast operations.</returns>
private static Expression RemoveConvertOperations(Expression expression)
{
while (expression.NodeType == ExpressionType.Convert || expression.NodeType == ExpressionType.ConvertChecked)
expression = ((UnaryExpression)expression).Operand;
return expression;
}
}
Then you can use it like this (put it in an QueryableExtensions
class or something like that):
/// <summary>
/// Specifies related entities to include in the query result.
/// </summary>
/// <typeparam name="T">The type of entity being queried.</typeparam>
/// <param name="source">The source <see cref="IQueryable{T}" /> on which to call Include.</param>
/// <param name="paths">The lambda expressions representing the paths to include.</param>
/// <returns>A new <see cref="IQueryable{T}" /> with the defined query path.</returns>
internal static IQueryable<T> Include<T>(this IQueryable<T> source, params Expression<Func<T, object>>[] paths)
{
if (paths != null)
source = paths.Aggregate(source, (current, include) => current.Include(include.AsPath()));
return source;
}
And then in your repository you call it normally like you would do in EF6:
query.Include(t => t.Navigation1, t => t.Navigation2.Select(x => x.Child1));
References:
How to pass lambda 'include' with multiple levels in Entity Framework Core?
https://github.com/aspnet/EntityFramework6