I am pro-repository, though I have moved away from generic repository patterns. Instead I align my repositories with the business function they serve. The repositories are not aimed at abstracting away the ORM, as this isn't anything I expect to change out, and at the same time I avoid making a repository too granular. (I.e. CRUD) Instead my repositories serve two-to-three key purposes:
- Data retrieval
- Data creation
- Hard deletion
For data retrieval, the repository always returns
IQueryable<TEntity>. For data creation it returns TEntity. The repository handles my base-level filtering such as authorization "active" state for systems that use soft-delete patterns, and "current" state for systems that use historical data. Data creation is responsible just for ensuring that required references are resolved and associated and that the entity is set up and ready to go.
Data update is the responsibility of the business logic working with the entities(s) in question. This can include things like validation rules. I don't try encapsulating that in a repository method.
Deletion in most of my systems is soft-delete so it would fall under data update. (IsActive = false) In the case of hard-deletes this would be a one-liner in the Repository.
Why repositories? Test-ability. Sure, DbContext can be mocked, but it is simpler to mock a class that returns
IQueryable<TEntity>. They also play nice with the UoW pattern, personally I use Mehdime's DbContextScope pattern to scope out the unit of work at the level I want (I.e. Controllers in MVC) and let my controllers and helper service classes utilize the repositories without needing to pass references to the UoW/dbContext around. Using IQueryable means that you don't need a lot of wrapper methods in the repository, and your code can optimize how the data is going to be consumed. For instance the repository doesn't need to expose methods like "Exists" or "Count", or try to wrap entities with other POCOs in cases where you want sub-sets of data. They don't even need to handle eager-loading options for related data you may or may not need. By passing IQueryable, the calling code can:
.Include() // Generally avoided, instead I use .Select()
.Select(x => new ViewModel or Anon. Type)
.FirstOrDefault() / .SingleOrDefault() / .ToList()
Very flexible, and from a testing P.o.V. my mocked repository simply needs to return Lists of populated entity objects.
As for generic repositories, I've moved away from these for the most part because when you end up with a repository per table your controllers/services end up with references to several repositories to do one business action. In most cases only one or two of these repositories are actually doing write operations (provided you're using navigation properties properly) while the rest are supporting those with Reads. I'd rather have something like an OrdersRepository that is capable of reading and creating orders, and reading any relevant lookups etc. (lightweight customer objects, products, etc.) for reference when creating an order, than hitting 5 or 6 different repositories. It may violate DNRY purists, but my argument for that is that the repository's purpose is to serve the creation of Orders which includes the related references. I don't need to go to a
Repository<Product> to get products where for the basis of an order I only need an entity with a handful of fields. My OrderRepository might have a
.GetProducts() method returning
IQueryable<ProductSummary> which I find nicer than a
Repository<Product> that ends up having several "Get" methods to try and serve different areas of the application's needs, and/or some complex pass-in filtering expression.
I opt for simpler code that's easy to follow, test, and tune. It can be abused in bad ways, but I'd rather have something where abuses are easy to spot and correct than try to "lock down" the code in a way that cannot be abused, fail at it, and then have something that is a nightmare to get to actually do what the client pays to have it do in the end. :)