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.)