Search code examples
asp.net-coreentity-framework-corerepositoryservice-layer

Should we passing expression to repository layer in ASP.NET Core?


I'm building repository pattern in ASP.NET Core. As far as I know, repository is a abstract layer to perform data access logic.

I have 2 ways approach for validating existing account as below:

Approach #1: using generic repository method and pass expression to repository

var isAccountExisting = await _accountRepository.AnyAsync(x => x.Id == accountId);

if (!isAccountExisting)
{
     throw new EntityNotFoundException(nameof(Account), accountId.Value);
}

Approach #2: create a method for checking that.

var isAccountExisting = await _accountRepository.IsExisting(accountId);

if (!isAccountExisting)
{
     throw new EntityNotFoundException(nameof(Account), accountId.Value);
}

From my point of view, I see that:

  1. Very dynamic but it makes repository layer tight to EF Core
  2. We can reuse this method all over our application, but for this simple check, should we create a new method here? It could be end up a tons up method like this (IsPhoneValid, IsEmailExisting,...)

So my question: what is better?


Solution

  • Well, there is no right question, it depends on the complexity of your project, your testing strategy, the qualifications of the development team, plans to modify the code in the future, etc. In my practice, I use both of these approaches and it suits me.

    My point is that if you have some expressions in your repository, it doesn't mean that you are tied to EF. It doesn't even mean that you should use EF. There may be different data sources, this is normal.

    In real work, passing expressions in a repository can be very useful if you need to deal with some sort of complex filtering or business rules. So, for example, your generic repository might look like this:

    public class GenericRepository<TEntity> : IGenericRepository<TEntity>
        where TEntity : class
    {
        private readonly IDbContext _context;
        private readonly DbSet<TEntity> _dbSet;
    
        public GenericRepository(IDbContext context)
        {
            _context = context;
            _dbSet = _context.Set<TEntity>();
        }
    
        public virtual async Task<IEnumerable<TEntity>> GetAllAsync(
            Expression<Func<TEntity, bool>>? predicate = null,
            CancellationToken token = default,
            params Expression<Func<TEntity, object>>[] navigationProperties)
        {
            return await GetQuery(predicate, navigationProperties).ToListAsync(token);
        }
    
        public virtual async Task<TEntity?> FirstOrDefaultAsync(
            Expression<Func<TEntity, bool>>? predicate = null,
            CancellationToken token = default,
            params Expression<Func<TEntity, object>>[] navigationProperties)
        {
            return await GetQuery(predicate, navigationProperties).FirstOrDefaultAsync(token);
        }
    
        public virtual async Task<bool> AnyAsync(
            Expression<Func<TEntity, bool>>? predicate = null,
            CancellationToken token = default,
            params Expression<Func<TEntity, object>>[] navigationProperties)
        {
            return await GetQuery(predicate, navigationProperties).AnyAsync(token);
        }
    
        private IQueryable<TEntity> GetQuery(
            Expression<Func<TEntity, bool>>? predicate = null,
            params Expression<Func<TEntity, object>>[] navigationProperties)
        {
            IQueryable<TEntity> dbQuery = _dbSet.AsNoTracking();
    
            if (predicate != null)
            {
                dbQuery = dbQuery.Where(predicate);
            }
    
            return navigationProperties.Aggregate(
                dbQuery,
                (current, navigationProperty) => current.Include(navigationProperty));
        }
    }
    

    Using the Specification pattern and combining the Generic Repository with Unit Of Work you can achieve high-level abstractions like this one:

    public class OrderService : IOrderService
    {
        private readonly IUnitOfWork _uow;
        private readonly IOrderSpecifications _orderSpec;
     
        public OrderService(IUnitOfWork uow, IOrderSpecifications orderSpec)
        {
            _uow = uow;
            _orderSpec = orderSpec;
        }
        
        public async Task<ProgramObject?> GetAcceptedOrderAsync(Guid id, CancellationToken token)
        {
            return await _uow.Orders.FirstOrDefaultAsync(_orderSpec.IdEquals(id).And(_orderSpec.IsApproved().OrElse(_orderSpec.IsInCorrectionProcess())), token);
        }
    }
    

    Here we have clean and concise code that is easy to cover with unit tests. In addition, business rules are separated into specifications and can be reused and also covered by unit tests. And all this is possible by passing expressions in the repository as parameters.

    And you can have your normal repositories as well, extending the Generic repository if needed. In this case it might look like this:

    public interface IOrderRepository : IGenericRepository<Order>
    {
        Task<IList<Guid>?> GetIdsAsync(
            Expression<Func<Order, bool>> predicate,
            CancellationToken token,
            params Expression<Func<Order, object>>[] navigationProperties);
    
    // Other methods go below...
    }
    

    What I'm saying is that you need to consider your specific case and choose your own approach after weighing the pros and cons.