I’m trying to limit the amount of elements of a navigation property.
According to the EF Core docs, I could add a Where, OrderBy, OrderByDescending, ThenBy, ThenByDescending, Skip, or Take operation on the navigation collection.
I’ve got this code:
public async Task<List<CategoryWithRecipeDto>> GetCategories(int recipeCount)
{
var categories = _context.Categories
.Include(cat => cat.Recipes.Take(recipeCount));
var dtos = await categories
.Select(cat => _mapper.Map<CategoryWithRecipeDto>(cat))
.ToListAsync();
return dtos;
}
This works fine. However, I’m trying to make the recipeCount
argument optional by making it nullable. That requires a recipeCount.HasValue
check before I can use it, so I created an extension method to do just that:
public static IEnumerable<T> TakeMaybe<T>(this IEnumerable<T> enumerable, int? count)
{
if (count.HasValue)
return enumerable.Take(count.Value);
return enumerable;
}
// And changing GetCategories as follows:
public async Task<List<CategoryWithRecipeDto>> GetCategories(int? recipeCount)
{
var categories = _context.Categories
.Include(cat => cat.Recipes.TakeMaybe(recipeCount));
var dtos = await categories
.Select(cat => _mapper.Map<CategoryWithRecipeDto>(cat))
.ToListAsync();
return dtos;
}
However, doing this, I get the following exception:
System.InvalidOperationException:
The expression ‘cat.Recetas.TakeMaybe(__queryParameters_CantRecetas_0)’ is invalid inside an ‘Include’ operation, since it does not represent a property access: ‘t => t.MyProperty’.
To target navigations declared on derived types, use casting (‘t => ((Derived)t).MyProperty’) or the ‘as’ operator (‘t => (t as Derived).MyProperty’).
Collection navigation access can be filtered by composing Where, OrderBy(Descending), ThenBy(Descending), Skip or Take operations.
For more information on including related data, see https://go.microsoft.com/fwlink/?LinkID=746393.
Which I’m not sure why it’s happening, since I’m either only calling the Take operator inside of the method or just returning the enumerable as is.
The docs do explicitly say that the only allowed methods are Where
, OrderBy
, OrderByDescending
, ThenBy
, ThenByDescending
, Skip
and Take
, but I’m not sure exactly why I can’t do what I’m trying to do. I think it could have something to do with the query translation magic that EF Core does, but I don’t really understand it so I’m not sure.
I could just build the query conditionally, piece by piece:
public async Task<List<CategoryWithRecipeDto>> GetCategories(int? recipeCount)
{
var categories = _context.Categories;
if (recipeCount.HasValue)
categories = categories.Include(cat => cat.Recipes.Take(recipeCount.Value));
else
categories = categories.Include(cat => cat.Recipes);
var dtos = await categories
.Select(cat => _mapper.Map<CategoryWithRecipeDto>(cat))
.ToListAsync();
return dtos;
}
Or even do something hacky like recipeCount.Value.GetValueOrDefault(int.MaxValue)
, but it would be really convenient if I could reuse the extension methods that I wrote for regular queries:
public static IEnumerable<T> Paginate<T>(this IEnumerable<T> enumerable, int? offset, int? length)
{
if (offset.HasValue)
enumerable = enumerable.Skip(offset.Value);
if (length.HasValue)
enumerable = enumerable.Take(length.Value);
return enumerable;
}
public static IEnumerable<T> Order<T, T1>(this IEnumerable<T> enumerable, Func<T, T1> keySelector, Ordering order)
{
return order switch
{
Ordering.Ascending => enumerable.OrderBy(keySelector),
Ordering.Descending => enumerable.OrderByDescending(keySelector),
_ => enumerable
};
}
So, my question is, why exactly can’t I use my own extension methods inside Include, even though they only either call allowed operations or do nothing?
And what other way could I implement something like this?
Thanks!
2
Here is alternative way of achieving the goal, it requires working little bit with Expression
s, but then the usage comes down to this:
async Task<List<Release>> GetCategories(int? recipeCount)
{
var categories = _context.Categories
.IncludeTake(cat => cat.Recipes.AsQueryable(), recipeCount);
return await categories.ToListAsync();
}
The implementation of the method is:
public static IIncludableQueryable<T, IQueryable<TInclude>> IncludeTake<T, TInclude>(
this IQueryable<T> queryable,
Expression<Func<T, IQueryable<TInclude>>> includeExpression,
int? count)
where T : class
{
// If count does not have value, return with default.
if (!count.HasValue)
{
return queryable.Include(includeExpression);
}
// If count is defined, then we need to "inject" take method
// into query.
var methodCall = (MethodCallExpression)includeExpression.Body;
var lambda = methodCall.Arguments[0];
var takeMethod = typeof(Queryable).GetMethods()
.First(x => x.Name == "Take" &&
x.GetParameters().Any(x => x.ParameterType == typeof(int)));
var body = Expression.Call(
takeMethod.MakeGenericMethod(typeof(TInclude)),
methodCall,
Expression.Constant(count.Value)
);
var parameter = includeExpression.Parameters[0];
var newExpression = Expression.Lambda<Func<T, IQueryable<TInclude>>>(body, parameter);
return queryable.Include(newExpression);
}