[英]How to make IEnumerable and IQueryable async?
我正在嘗試使我的通用基礎存儲庫全部異步,並且我有代碼:
public IEnumerable<T> GetAll()
{
return _dbContext.Set<T>();
}
public IQueryable<T> GetQueryable()
{
return _dbContext.Set<T>();
}
由於沒有諸如SetAsync
類的方法,如何使這些方法異步?
Set<T>()
方法不執行任何異步操作,因此您的方法不需要是異步的。
您可以在源代碼中看到當前的實現: https://github.com/dotnet/efcore/blob/3b0a18b4717c288917dabf8c6bb9d005f1c50bfa/src/EFCore/DbContext.cs#L292 。 它調用 object 緩存,因此不應該有異步操作。
問題本身對我來說很奇怪,因為你為什么想要一個SetAsync
?
我們來看看Set()的定義
public virtual DbSet<TEntity> Set<TEntity>()
where TEntity : class
=> (DbSet<TEntity>)((IDbSetCache)this).GetOrAddSet(DbContextDependencies.SetSource, typeof(TEntity));
讓我們繼續GetOrAddSet 。
object IDbSetCache.GetOrAddSet(IDbSetSource source, Type type)
{
CheckDisposed();
_sets ??= new Dictionary<(Type Type, string Name), object>();
if (!_sets.TryGetValue((type, null), out var set))
{
set = source.Create(this, type);
_sets[(type, null)] = set;
_cachedResettableServices = null;
}
return set;
}
它在名為_set
的字典中進行簡單查找,該字典定義如下:
private IDictionary<(Type Type, string Name), object> _sets;
source
是DbContextDependencies 的 SetSource ,它是一個IDbSetSource
:
public IDbSetSource SetSource { get; [param: NotNull] init; }
DbSetSource是實現 IDbSetSource 的IDbSetSource
:
public class DbSetSource : IDbSetSource
{
private readonly ConcurrentDictionary<(Type Type, string Name), Func<DbContext, string, object>> _cache = new();
...
}
因此, Set<TEntity>()
將執行簡單的 ConcurrentDictionary 查找。 為什么需要它的異步版本?
對於返回數據的方法,您可以使它們異步:
public IAsyncEnumerable<T> GetAll()
{
return _dbContext.Set<T>().AsAsyncEnumerable();
}
或者
public async Task<IList<T>> GetAll()
{
return await _dbContext.Set<T>().ToListAsync();
}
但是你不希望這個方法是異步的。
public IQueryable<T> GetQueryable()
{
return _dbContext.Set<T>();
}
因為它不返回數據或執行數據庫訪問。 它只是給調用者一個存根查詢來使用。 所以調用者將運行一個異步查詢,如:
var orders = await customerRepo.GetQueryable().Where( o => o.CustomerId = 123 ).ToListAsync();
將存儲庫方法返回類型包裝在Task
中。
public Task<IEnumerable<T>> GetAllAsync<T>()
{
return Task.FromResult(_dbContext.Set<T>());
}
public Task<IQueryable<T>> GetQueryableAsync<T>()
{
return Task.FromResult(_dbContext.Set<T>());
}
如果您沒有執行任何實際的異步操作,則可以將返回結果包裝在Task.FromResult
中。
不過,有幾個重要的注意事項。
在您當前的情況下,擁有一個只返回.Set
上下文的存儲庫是一個非常糟糕的主意。 更糟糕的是,您正在返回IQueryable
,這會引入大量額外的不良做法。 對於初學者, IQueryable
接口非常復雜,我很確定您不會(並且很可能不能)為存儲庫的任何其他非 ef實現實現。 其次,它將查詢邏輯泄漏到存儲庫本身之外,這基本上使其過時(或者實際上,甚至更糟 - 問題的根源)。 如果您出於某種原因確實需要存儲庫,則應為所有收集結果返回IEnumerable
。 在這種情況下,你應該有類似的東西:
public Task<IEnumerable<T>> GetAllAsync<T>()
{
return _dbContext.Set<T>().ToListAsync();
}
並完全GetQueryableAsync
。 要么使用GetAllAsync
並對服務層中的結果進行額外的操作,要么使用替代方法 - 在子存儲庫中引入單獨的方法來處理您的特定數據案例。
使用IQueryable
的存儲庫方法不需要在async
操作中與 function async
。 這就是異步運行IQueryable
的方式。
正如上面評論中提到的,引入存儲庫模式的唯一真正原因是促進單元測試。 如果您不利用單元測試,那么我建議您簡單地注入 DbContext 以充分利用 EF。 添加抽象層以可能在以后替換 EF 或為了抽象而抽象只會導致查詢返回整個 object 圖的效率低得多,無論調用者需要什么或將大量代碼寫入存儲庫以稍微返回為每個調用者的需求提供不同但有效的結果。
例如,給定一個類似的方法:
IQueryable<Order> IOrderRepository.GetOrders(bool includeInactive = false)
{
IQueryable<Order> query = Context.Orders;
if(!includeInactive)
query = query.Where(x => x.IsActive);
return query;
}
服務中的消費代碼可以是async
方法,並且可以通過可等待操作與此存儲庫方法完美交互:
var orders = await OrderRepository.GetOrders()
.ProjectTo<OrderSummaryViewModel>()
.Skip(pageNumber * pageSize)
.Take(pageSize)
.ToListAsync();
存儲庫方法本身不需要標記為異步,它只是構建查詢。
返回IEnumerable<T>
的存儲庫方法需要標記為async
:
IEnumerable<Order> async IOrderRepository.GetOrdersAsync(bool includeInactive = false)
{
IQueryable<Order> query = Context.Orders;
if(!includeInactive)
query = query.Where(x => x.IsActive);
return await query.ToListAsync();
}
但是,我絕對不推薦這種方法。 問題是這總是有效地加載所有活動或活動和非活動訂單。 它也不適用於我們可能想要訪問並已預先加載的訂單之外的任何相關實體。
為什么IQueryable
? 靈活性。 通過讓存儲庫返回 IQueryable,調用者可以按照他們需要的方式使用數據,就像他們有權訪問 DbContext 一樣。 這包括自定義過濾條件、排序、處理分頁、簡單地使用.Any()
或Count()
進行存在檢查,以及最重要的投影。 (使用Select
或 Automapper 的ProjectTo
來填充視圖模型,從而產生比返回整個實體圖更有效的查詢和更小的有效負載)存儲庫還可以提供核心級別的過濾規則,例如軟刪除系統(IsActive 過濾器)或多- 租戶系統,強制過濾與當前登錄用戶相關的行。 即使在我有像 GetById 這樣的存儲庫方法的情況下,我也會返回IQueryable
以方便投影或確定要包含哪些關聯實體。 您最終看到的常見替代方案是包含復雜參數的存儲庫,例如用於過濾、排序的Expression<Func<T>>
,然后是用於急切加載的更多參數,包括分頁等。這些最終“泄漏”EF 主義同樣糟糕與使用IQueryable
一樣,因為這些表達式必須符合 EF。 (即表達式不能包含對函數或未映射屬性等的調用)
您可以將IAsyncEnumerator
與GetAsyncEnumerator
function 一起使用。
您可以在此處獲得更多文檔。
文檔中的一個示例:
await foreach (int item in RangeAsync(10, 3).WithCancellation(token))
Console.Write(item + " ");
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.