3

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?

6 Answers6

7

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();
David Browne - Microsoft
  • 80,331
  • 6
  • 39
  • 67
3

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?

Peter Csala
  • 17,736
  • 16
  • 35
  • 75
  • Well I want to make my application async, I have API controller layer, service layer, repository layer. So in order to return async Task in API controller, I need to have async await all way back to repository layer – los pollos hermanos Feb 18 '21 at 15:13
  • @lospolloshermanos You can do that by calling async methods on the DbSet, like `AddAsync` or `FindAsync` ([1](https://learn.microsoft.com/en-us/dotnet/api/system.data.entity.dbset-1)). Or you can use Async LINQ operators like `ListAsync`([2](https://learn.microsoft.com/en-us/ef/core/miscellaneous/async)) – Peter Csala Feb 18 '21 at 15:17
2

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.

juunas
  • 54,244
  • 13
  • 113
  • 149
1

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. (I.e. the expressions cannot contain calls to functions or unmapped properties, etc.)

Steve Py
  • 26,149
  • 3
  • 25
  • 43
  • 1
    Hi Steve, thanks for the brief. Do you advice then to use IQueryable or not? Because in one sentence you said IQueryable is good and flexible, while in the other you said "just as bad as using IQueryable". – los pollos hermanos Feb 19 '21 at 12:48
  • "Bad" only relative to if you hold the perception of it "leaking" EF. If the purpose of abstracting EF with a repository is to hide the dependency on EF and it's rules, you either have to give up much of the benefit EF can provide through Linq or your substitute expressions etc. ultimately have to impose the exact same rules and domain knowledge (leaks) that EF requires. I am 100% pro-`IQueryable` when it comes to the repository pattern. I only recommend repositories though as an enabler for unit testing. – Steve Py Feb 19 '21 at 22:51
0

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.

zhulien
  • 5,145
  • 3
  • 22
  • 36
  • I found your notes very pertinent: leaking IQeuryable outside the repository is a very bad practice and defeats the purpose of using the repository abstraction – Benzara Tahar Feb 18 '21 at 12:24
  • I'm not really sure what is bad practice in using IQueryable and Repository pattern, I am using IQueryable to be able to simplify database queries in service layers @zhulien – los pollos hermanos Feb 18 '21 at 15:10
  • @lospolloshermanos What's the point of having a repository if you're going to expose `IQueryable` from it? Furthermore, if you decide to change the underlying data storage mechanism, do you think you can implement `IQueryable` on it? I highly doubt it. Try it on a simple memory "store" and you'll see what I am talking about. It is an interface which builds an expression tree. You're implementing patterns just for the sake of implementing them and not because you need them or know what they actually give you as benefit. In short, this type of "architecture" will bite you soon enough. – zhulien Feb 18 '21 at 15:19
  • `IQueryable` is *not* bad practice, it facilitates flexibility and performance while effectively gives you a unit testing boundary that is easier to mock than a DbContext/DbSet. If you aren't unit testing, I wouldn't bother with a repository. However, the alternatives to `IQueryable` either means adding a LOT of very similar methods into a massive class, having the repository return entire saturated sets of entities all of the time (I.e. DbSet.ToList) or introducing complex parameters for filtering, sorting, includes or projections, etc which "leaks" EF since they must comply with EF rules. – Steve Py Feb 18 '21 at 21:02
  • 1
    ... In the context that the only reason to introduce a repository is to facilitate unit testing. It should not be to abstract your data layer. If you implement EF you should commit to it in order to leverage all of the capabilities it can provide. Abstracting it for the sake of "maybe swap it out later" is just imposing a real limitation on your data later for a "might be" requirement that never eventuates. Crippling EF with abstractions can often lead to performance problems that fuel the argument to switch it out. :/ – Steve Py Feb 18 '21 at 21:08
  • @StevePy `IQueryable` gives you a boundary that is easier to mock?! This is the first time that I hear someone actually state that. This is wrong for so many reasons. Have you tried implementing it, even once? Try it over a simple List. Furthermore, you're talking about unit testing of repository while in the same time returning `IQueryable` from it, which you use to BUILD QUERIES UPON in your *service layer*. In that case, what exactly are you unit testing in your repository?! – zhulien Feb 18 '21 at 21:17
  • @StevePy `IQueryable` is a query builder over expression trees. `IEnumerable` is an interface over iterables which basically EVERY form of a "list" implements. I would gladly accept an example of how `IQueryable` gives me "flexibility"(without introducing logic leakage) and easier mocking environment. – zhulien Feb 18 '21 at 21:20
  • The point is exactly that you ***aren't*** unit testing your repository, you are unit testing the business logic calling the repository so the goal is to make the repository easy to mock. Mocking `IQueryable` is easy, and there are implementations of containers to facilitate mocking a container suited to `async` operations. There is no EF to "leak" as you either use the DbContext, or an easily mock-able substitute. Abstracting your business logic away from the fact you use EF is a poor reason to add abstraction. You lose the majority of the benefit that EF can provide. – Steve Py Feb 18 '21 at 21:59
0

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 + " ");
Ygalbel
  • 5,214
  • 1
  • 24
  • 32