过滤包含扩展方法 EF Core

问题描述 投票:0回答:1

我正在尝试限制导航属性的元素数量。

根据 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 中使用自己的扩展方法,即使它们只调用允许的操作或不执行任何操作?

还有什么其他方法可以实现这样的事情?

谢谢!

c# linq entity-framework-core
1个回答
0
投票

这是实现目标的另一种方法,它需要使用

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);
}
© www.soinside.com 2019 - 2024. All rights reserved.