简体   繁体   中英

How to make IEnumerable and IQueryable async?

I am trying to make my generic base repository all async and I have the code:

public IEnumerable<T> GetAll()
{
    return _dbContext.Set<T>();
}

public IQueryable<T> GetQueryable()
{
    return _dbContext.Set<T>();
}

Since there are not methods such as SetAsync , how can I make these methods asynchronous?

The Set<T>() method does not do any asynchronous operations so your methods don't need to be asynchronous.

You can see the current implementation in the source code: https://github.com/dotnet/efcore/blob/3b0a18b4717c288917dabf8c6bb9d005f1c50bfa/src/EFCore/DbContext.cs#L292 . It calls out to an object cache, so there should be no operations that are asynchronous.

Question itself is quite weird for me, because why would you want to have a SetAsync ?

Let's look at the definition of Set()

public virtual DbSet<TEntity> Set<TEntity>()
    where TEntity : class
    => (DbSet<TEntity>)((IDbSetCache)this).GetOrAddSet(DbContextDependencies.SetSource, typeof(TEntity));

Let's continue with the 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;
}

It does a simple lookup in a dictionary called _set , which is defined as this:

private IDictionary<(Type Type, string Name), object> _sets;

The source is the SetSource of the DbContextDependencies which is a IDbSetSource :

public IDbSetSource SetSource { get; [param: NotNull] init; }

DbSetSource is the class which implements IDbSetSource :

public class DbSetSource : IDbSetSource
{

    private readonly ConcurrentDictionary<(Type Type, string Name), Func<DbContext, string, object>> _cache = new();
   ...
}

So, Set<TEntity>() will perform a simple ConcurrentDictionary lookup. Why would you need an async version of it?

For the methods that return data you can make them async:

public IAsyncEnumerable<T> GetAll()
{
    return _dbContext.Set<T>().AsAsyncEnumerable();
}

or

public async Task<IList<T>> GetAll()
{
    return await _dbContext.Set<T>().ToListAsync();
}

But you don't want this method to be async.

public IQueryable<T> GetQueryable()
{
    return _dbContext.Set<T>();
}

because it doesn't return data or perform database access. It just gives the caller a stub query to use. So the caller would run an async query like:

var orders = await customerRepo.GetQueryable().Where( o => o.CustomerId = 123 ).ToListAsync();

Wrap your repository methods return type in a 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>());
}

If you're not doing any actual asynchronous operations, you can wrap the return result in Task.FromResult .

A couple of IMPORTANT notes, though.

In your current case, having a repository which just returns the .Set of a context is quite of a bad idea . What's even worse is that you're returning IQueryable , which introduces tons of additional bad practices. For starters, the IQueryable interface is quite complex and I am pretty sure you would not(and most probably can't) implement for any other non-ef implementation of the repository. Secondly, it leaks the query logic outside the repository itself, which basically renders it obsolete(or actually, even worse - a source of problems). If you really-really need a repository for some reason, you should return IEnumerable for all collection results. In that case, you should have something like:

public Task<IEnumerable<T>> GetAllAsync<T>()
{
    return _dbContext.Set<T>().ToListAsync();
}

And ditch the GetQueryableAsync entirely. Either use the GetAllAsync and do additional manipulation over the result in your service layer or an alternative approach - introduce separate methods in the child repositories to handle your specific data cases.

Repository methods that work with IQueryable do not need to be async to function in async operations. It is how the IQueryable is consumed that functions asynchronously.

As mentioned in the above comments, the only real reason to introduce a Repository pattern is to facilitate unit testing. If you do not leverage unit testing then I would recommend simply injecting a DbContext to gain the most out of EF. Adding layers of abstraction to possibly substitute out EF at a later time or abstracting for the sake of abstraction will just lead to far less efficient querying returning entire object graphs regardless of what the callers need or writing a lot of code into a repository to return slightly different but efficient results for each caller's needs.

for instance, given a method like:

IQueryable<Order> IOrderRepository.GetOrders(bool includeInactive = false)
{
    IQueryable<Order> query = Context.Orders;
    if(!includeInactive)
        query = query.Where(x => x.IsActive);

    return query;
}

The consuming code in a service can be an async method and can interact with this repository method perfectly fine with an await-able operation:

var orders = await OrderRepository.GetOrders()
    .ProjectTo<OrderSummaryViewModel>()
    .Skip(pageNumber * pageSize)
    .Take(pageSize)
    .ToListAsync();

The repository method itself doesn't need to be marked async, it just builds the query.

A repository method that returns IEnumerable<T> would need to be marked 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();
}

Hoewver, I definitely don't recommend this approach. The issue is that this is always effectively loading all active, or active & inactive orders. It also is not accommodating for any related entities off the order that we might want to also access and have eager loaded.

Why IQueryable ? Flexibility. By having a repository return IQueryable, callers can consume data how they need as if they had access to the DbContext. This includes customizing filtering criteria, sorting, handling pagination, simply doing an exists check with .Any() , or a Count() , and most importantly, projection. (Using Select or Automapper's ProjectTo to populate view models resulting in far more efficient queries and smaller payloads than returning entire entity graphs) The repository can also provide the core-level filtering rules such as with soft-delete systems (IsActive filters) or with multi-tenant systems, enforcing the filtering for rows associated to the currently logged in user. Even in the case where I have a repository method like GetById, I return IQueryable to facilitate projection or determine what associated entities to include. The common alternative you end up seeing is repositories containing complex parameters like Expression<Func<T>> for filtering, sorting, and then more parameters for eager loading includes, pagination, etc. These end up "leaking" EF-isms just as bad as using IQueryable because those expressions must conform to EF. (Ie the expressions cannot contain calls to functions or unmapped properties, etc.)

You can use IAsyncEnumerator with GetAsyncEnumerator function.

You can have more documentation here .

An example from the documentation:

await foreach (int item in RangeAsync(10, 3).WithCancellation(token))
  Console.Write(item + " ");

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM