Refactoring my repository from using Ardalis to only use .NET 6 library, strugling with "Linq expression could not be translated"

huangapple go评论124阅读模式
英文:

Refactoring my repository from using Ardalis to only use .NET 6 library, strugling with "Linq expression could not be translated"

问题

我正在重构一个.NET 6/entity framework core 7项目,该项目以前通过Ardalis和Ardalis规范来处理仓储调用以添加条件到仓库调用。

这个重构的想法是只使用Linq表达式和IQueryable,而不是Ardalis规范。我有一个通用的抽象仓库类,所有仓库都可以继承它。

在纸上,我对结果感到满意,我的代码编译得很好,但不幸的是,在通过Postman测试我的API路由时,我遇到了错误:“Linq表达式无法被翻译。要么重写查询以便可以被翻译,要么明确地切换到客户端评估,插入一个调用‘AsEnumerable’、‘AsAsyncEnumerable’、‘ToList’或‘ToListAsync’。”

我将向你展示我最初的代码:

public abstract class GenericDbContextRepository<T, TKey> : IRepository<T, TKey> where T : class
{
    protected readonly DbContext GenericDbContext;
    protected readonly DbSet<T> DbSet;

    protected GenericDbContextRepository(DbContext DbContext)
    {
        GenericDbContext = DbContext;
        DbSet = GenericDbContext.Set<T>();
    }

    public async Task<IEnumerable<T>> FindAsync(Expression<Func<T, bool>> expression, IQueryable<T> query)
    {
        var originalQuery = DbSet.AsQueryable();
        var finalQuery = originalQuery.Concat(query);
        return await finalQuery.Where(expression).ToListAsync();
    }
}

例如,我的仓库“UserStoreRepository”会调用FindAsync,并传递以下IQueryable:

public static IQueryable<UserStore> GetUserStoreWithItemsQuery()
{
    IQueryable<UserStore> initialQuery = Enumerable.Empty<UserStore>().AsQueryable();
    return initialQuery
        .OrderByDescending(x => x.CreationDate)
        .Take(1)
        .Include(UserStore => UserStore.Items);
}

在第一次尝试时,我得到了以下错误:

“Linq表达式'EnumerableQuery<UserStore>{}'无法被翻译。要么重写查询以便可以被翻译,要么明确地切换到客户端评估,插入一个调用‘AsEnumerable’、‘AsAsyncEnumerable’、‘ToList’或‘ToListAsync’。”

修复此问题的一个最初的想法是简单地遵循错误消息中的建议,在我的GetUserStoreWithItemsQuery()函数中使用AsEnumerable(),最终代码如下:

public static IQueryable&lt;UserStore&gt; GetUserStoreWithItemsQuery()
{
    IQueryable&lt;UserStore&gt; initialQuery = Enumerable.Empty&lt;UserStore&gt;().AsQueryable();
    
    return initialQuery
        .AsEnumerable()
        .OrderByDescending(x =&gt; x.CreationDate)
        .Take(1)
        .AsQueryable()
        //Include can only be used on IQueryable, not on IEnumerable
        .Include(UserStore =&gt; UserStore.Items);
}

这里Linq表达式的翻译工作,并且我得到了来自我的FindAsync()函数的新错误:

“Linq表达式'DbSet<UserStore>{}'无法被翻译。要么重写查询以便可以被翻译,要么明确地切换到客户端评估,插入一个调用‘AsEnumerable’、‘AsAsyncEnumerable’、‘ToList’或‘ToListAsync’。”

我考虑采用相同的修复方式,使用AsEnumerable():

public async Task&lt;IEnumerable&lt;T&gt;&gt; FindAsync(Expression&lt;Func&lt;T, bool&gt;&gt; expression, IQueryable&lt;T&gt; query)
{
    var originalQuery = DbSet.AsEnumerable();
    var finalQuery = originalQuery.Concat(query.AsEnumerable());
    return await finalQuery.AsQueryable().Where(expression).ToListAsync();
}

但现在我得到了以下错误:

源‘IQueryable’的提供程序没有实现‘IAsyncQueryProvider’。只有实现‘IAsyncQueryProvider’的提供程序才能用于Entity Framework的异步操作。

我可能可以“绕过”并实现‘IAsyncQueryProvider’,但我觉得不应该这样做,我猜想一定有更好、更清晰的方法来实现我尝试的目标,我开始认为切换到Enumerable然后切换回IQueryable可能不是正确的做法,无论是为了代码/数据库调用性能还是代码质量。我欢迎任何建议和修复。

谢谢大家。

英文:

I am refactoring a .NET 6/entity framework core 7 project which used to handle repository calls through Ardalis and Ardalis Specifications to add conditions to repository calls.

The idea of this refactoring is to only use Linq expressions and IQueryable instead of Ardalis specifications. I have a generic abstract repository class which all repositories can inherit of.

On paper, I was happy with the result and my code is compiling fine, unfortunatley when doing some tests on my API routes through Postman, I got the error : "Linq Expression could not be translated. Either rewrite the query in a form that can be translated, or switch to client evaluation explicitly by inserting a call to 'AsEnumerable', 'AsAsyncEnumerable', 'ToList', or 'ToListAsync'"

I will show you the code I had at first:

public abstract class GenericDbContextRepository&lt;T, TKey&gt; : IRepository&lt;T, TKey&gt; where T : class
{
    protected readonly DbContext GenericDbContext;
    protected readonly DbSet&lt;T&gt; DbSet;

    protected GenericDbContextRepository(DbContext DbContext)
    {
        GenericDbContext = DbContext;
        DbSet = GenericDbContext.Set&lt;T&gt;();
    }

    public async Task&lt;IEnumerable&lt;T&gt;&gt; FindAsync(Expression&lt;Func&lt;T, bool&gt;&gt; expression, IQueryable&lt;T&gt; query)
    {
        var originalQuery = DbSet.AsQueryable();
        var finalQuery = originalQuery.Concat(query);
        return await finalQuery.Where(expression).ToListAsync();
    }
}

For example, my repository "UserStoreRepository" would call FindAsync and it would pass the following IQueryable :

public static IQueryable&lt;UserStore&gt; GetUserStoreWithItemsQuery()
{
    IQueryable&lt;UserStore&gt; initialQuery = Enumerable.Empty&lt;UserStore&gt;().AsQueryable();
    return initialQuery
        .OrderByDescending(x =&gt; x.CreationDate)
        .Take(1)
        .Include(UserStore =&gt; UserStore.Items);
}

On first try, I got the following error:
> " The Linq Expression 'EnumerableQuery<UserStore>{}' could not be translated. Either rewrite the query in a form that can be translated, or switch to client evaluation explicitly by inserting a call to 'AsEnumerable', 'AsAsyncEnumerable', 'ToList', or 'ToListAsync'"

One of my first idea to fix this issue was simply to follow the recommendation in the error message and use AsEnumerable() in my GetUserStoreWithItemsQuery() function, I ended up with the following code:

public static IQueryable&lt;UserStore&gt; GetUserStoreWithItemsQuery()
{
    IQueryable&lt;UserStore&gt; initialQuery = Enumerable.Empty&lt;UserStore&gt;().AsQueryable();
    
    return initialQuery
        .AsEnumerable()
        .OrderByDescending(x =&gt; x.CreationDate)
        .Take(1)
        .AsQueryable()
        //Include can only be used on IQueryable, not on IEnumerable
        .Include(UserStore =&gt; UserStore.Items);
}

The Linq expression's translation here worked and I ended up with a new error from my FindAsync() function :

> " The Linq Expression 'DbSet<UserStore>{}' could not be translated. Either rewrite the query in a form that can be translated, or switch to client evaluation explicitly by inserting a call to 'AsEnumerable', 'AsAsyncEnumerable', 'ToList', or 'ToListAsync'"

I thought of applying the same fix with AsEnumerable():

public async Task&lt;IEnumerable&lt;T&gt;&gt; FindAsync(Expression&lt;Func&lt;T, bool&gt;&gt; expression, IQueryable&lt;T&gt; query)
{
    var originalQuery = DbSet.AsEnumerable();
    var finalQuery = originalQuery.Concat(query.AsEnumerable());
    return await finalQuery.AsQueryable().Where(expression).ToListAsync();
}

But now I have the following error :

> The provider for the source 'IQueryable' doesn't implement 'IAsyncQueryProvider'. Only providers that implement 'IAsyncQueryProvider' can be used for Entity Framework asynchronous operations.

I probably could "bypass" and implement 'IAsyncQueryProvider' but I feel like I should not have to, I am guessing there must be a better and cleaner way to achieve what I am trying to, and I am starting to think switching to Enumerable then back to IQueryable is probably not the good way of doing things, whether for code/database call performance and code quality. I welcome any advices and fixes.

Thank you all.

答案1

得分: 2

在你的特定情况下,FindAsync 方法可能类似于以下方式,请查看:

public virtual Task<IEnumerable<TEntity>> FindAsync(
    Expression<Func<TEntity, bool>>? predicate = null,
    CancellationToken token = default,
    params Expression<Func<TEntity, object>>[] navigationProperties)
{
    IQueryable<TEntity> dbQuery = _dbSet.AsNoTracking();

    if (predicate != null)
    {
        dbQuery = dbQuery.Where(predicate);
    }

    return await navigationProperties
        .Aggregate(dbQuery, (current, navigationProperty) => current.Include(navigationProperty))
        .ToListAsync(token);
}
英文:

I think, in your particular case, the FindAsync method could be similar to this one, please take a look:

public virtual Task&lt;IEnumerable&lt;TEntity&gt;&gt; FindAsync(
    Expression&lt;Func&lt;TEntity, bool&gt;&gt;? predicate = null,
    CancellationToken token = default,
    params Expression&lt;Func&lt;TEntity, object&gt;&gt;[] navigationProperties)
{
    IQueryable&lt;TEntity&gt; dbQuery = _dbSet.AsNoTracking();

    if (predicate != null)
    {
        dbQuery = dbQuery.Where(predicate);
    }

    return await navigationProperties
        .Aggregate(dbQuery, (current, navigationProperty) =&gt; current.Include(navigationProperty))
        .ToListAsync(token);
}

huangapple
  • 本文由 发表于 2023年8月9日 18:40:36
  • 转载请务必保留本文链接:https://go.coder-hub.com/76866958.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定