我正在尝试限制导航属性的元素数量。
根据 EF Core 文档,我可以在导航集合上添加Where、OrderBy、OrderByDescending、ThenBy、ThenByDescending、Skip 或 Take 操作。
我有这个代码:
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;
}
这个效果很好。但是,我试图通过使其可为空来使
recipeCount
参数成为可选的。在使用它之前需要进行 recipeCount.HasValue
检查,因此我创建了一个扩展方法来做到这一点:
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;
}
但是,这样做时,我得到以下异常:
系统.InvalidOperationException: 表达式“cat.Recetas.TakeMaybe(__queryParameters_CantRecetas_0)”在“Include”操作中无效,因为它不表示属性访问:“t => t.MyProperty”。
要定位在派生类型上声明的导航,请使用强制转换 ('t => ((Derived)t).MyProperty') 或 'as' 运算符 ('t => (t as Derived).MyProperty')。
可以通过组合Where、OrderBy(降序)、ThenBy(降序)、Skip 或 Take 操作来过滤集合导航访问。 有关包含相关数据的更多信息,请参阅 https://go.microsoft.com/fwlink/?LinkID=746393。
我不确定为什么会发生这种情况,因为我要么只在方法内部调用 Take 运算符,要么只是按原样返回可枚举值。
文档明确指出,only允许的方法是
Where
,OrderBy
,OrderByDescending
,ThenBy
,ThenByDescending
,Skip
和Take
,但我不确定为什么我做不到我想做的事。我认为这可能与 EF Core 所做的查询翻译魔法有关,但我不太理解它,所以我不确定。
我可以有条件地、一点一点地构建查询:
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;
}
或者甚至做一些像
recipeCount.Value.GetValueOrDefault(int.MaxValue)
这样的黑客行为,但如果我可以重用我为常规查询编写的扩展方法,那将非常方便:
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
};
}
所以,我的问题是,为什么我不能在 Include 中使用自己的扩展方法,即使它们只调用允许的操作或不执行任何操作?
还有什么其他方法可以实现这样的事情?
谢谢!
这是实现目标的另一种方法,它需要使用
Expression
进行一些工作,但用法可以归结为:
async Task<List<Release>> GetCategories(int? recipeCount)
{
var categories = _context.Categories
.IncludeTake(cat => cat.Recipes.AsQueryable(), recipeCount);
return await categories.ToListAsync();
}
该方法的实现是:
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 xpr = Expression.Lambda<Func<T, IQueryable<TInclude>>>(body, parameter);
return queryable.Include(xpr);
}