asp.netasp.net-mvcarchitecture

Advantage of creating a generic repository vs. specific repository for each object?


We are developing an ASP.NET MVC application, and are now building the repository/service classes. I'm wondering if there are any major advantages to creating a generic IRepository interface that all repositories implement, vs. each Repository having its own unique interface and set of methods.

For example: a generic IRepository interface might look like (taken from this answer):

public interface IRepository : IDisposable
{
    T[] GetAll<T>();
    T[] GetAll<T>(Expression<Func<T, bool>> filter);
    T GetSingle<T>(Expression<Func<T, bool>> filter);
    T GetSingle<T>(Expression<Func<T, bool>> filter, List<Expression<Func<T, object>>> subSelectors);
    void Delete<T>(T entity);
    void Add<T>(T entity);
    int SaveChanges();
    DbTransaction BeginTransaction();
}

Each Repository would implement this interface, for example:

The alternate that we've followed in prior projects would be:

public interface IInvoiceRepository : IDisposable
{
    EntityCollection<InvoiceEntity> GetAllInvoices(int accountId);
    EntityCollection<InvoiceEntity> GetAllInvoices(DateTime theDate);
    InvoiceEntity GetSingleInvoice(int id, bool doFetchRelated);
    InvoiceEntity GetSingleInvoice(DateTime invoiceDate, int accountId); //unique
    InvoiceEntity CreateInvoice();
    InvoiceLineEntity CreateInvoiceLine();
    void SaveChanges(InvoiceEntity); //handles inserts or updates
    void DeleteInvoice(InvoiceEntity);
    void DeleteInvoiceLine(InvoiceLineEntity);
}

In the second case, the expressions (LINQ or otherwise) would be entirely contained in the Repository implementation, whoever is implementing the service just needs to know which repository function to call.

I guess I don't see the advantage of writing all the expression syntax in the service class and passing to the repository. Wouldn't this mean easy-to-messup LINQ code is being duplicated in many cases?

For example, in our old invoicing system, we call

InvoiceRepository.GetSingleInvoice(DateTime invoiceDate, int accountId)

from a few different services (Customer, Invoice, Account, etc). That seems much cleaner than writing the following in multiple places:

rep.GetSingle(x => x.AccountId = someId && x.InvoiceDate = someDate.Date);

The only disadvantage I see to using the specific approach is that we could end up with many permutations of Get* functions, but this still seems preferable to pushing the expression logic up into the Service classes.

What am I missing?


Solution

  • This is an issue as old as the Repository pattern itself. The recent introduction of LINQ's IQueryable, a uniform representation of a query, has caused a lot of discussion about this very topic.

    I prefer specific repositories myself, after having worked very hard to build a generic repository framework. No matter what clever mechanism I tried, I always ended up at the same problem: a repository is a part of the domain being modeled, and that domain is not generic. Not every entity can be deleted, not every entity can be added, not every entity has a repository. Queries vary wildly; the repository API becomes as unique as the entity itself.

    A pattern I often use is to have specific repository interfaces, but a base class for the implementations. For example, using LINQ to SQL, you could do:

    public abstract class Repository<TEntity>
    {
        private DataContext _dataContext;
    
        protected Repository(DataContext dataContext)
        {
            _dataContext = dataContext;
        }
    
        protected IQueryable<TEntity> Query
        {
            get { return _dataContext.GetTable<TEntity>(); }
        }
    
        protected void InsertOnCommit(TEntity entity)
        {
            _dataContext.GetTable<TEntity>().InsertOnCommit(entity);
        }
    
        protected void DeleteOnCommit(TEntity entity)
        {
            _dataContext.GetTable<TEntity>().DeleteOnCommit(entity);
        }
    }
    

    Replace DataContext with your unit-of-work of choice. An example implementation might be:

    public interface IUserRepository
    {
        User GetById(int id);
    
        IQueryable<User> GetLockedOutUsers();
    
        void Insert(User user);
    }
    
    public class UserRepository : Repository<User>, IUserRepository
    {
        public UserRepository(DataContext dataContext) : base(dataContext)
        {}
    
        public User GetById(int id)
        {
            return Query.Where(user => user.Id == id).SingleOrDefault();
        }
    
        public IQueryable<User> GetLockedOutUsers()
        {
            return Query.Where(user => user.IsLockedOut);
        }
    
        public void Insert(User user)
        {
            InsertOnCommit(user);
        }
    }
    

    Notice the public API of the repository does not allow users to be deleted. Also, exposing IQueryable is a whole other can of worms - there are as many opinions as belly buttons on that topic.