How to universally create Repositories that inheri

2019-07-23 14:39发布

问题:

I'm currently trying to implement the Repository Pattern on top of my DbContext. The thing is, I eventually end up with a situation where I have to inject several Repositories into a UnitOfWork constructor, like this:

public class UnitOfWork
{
    private DbContext _context;
    ICustomerRepository Customers { get; private set; }
    IEmployeeRepository Employees { get; private set; } 
    public UnitOfWork(DbContext context, ICustomerRepository cust, IEmployeeRepository emp)
    {
        _context = context;
        Customers = cust;
        Employees = emp;
    }    
}

However, since all of them would have to share the same DbContext, I can't see injecting them as an option.

That's why I was thinking about creating a RepositoryFactory class, but since all of the repositories inherit from a generic interface I found it impossible to create a single Create() method, because, after all, they have no real common ancestor that would be a valid return type.

To give you some more insight, that's how the code looks like:

public interface IRepository<TEntity> where TEntity:class
{
    TEntity Get(int id);
    IEnumerable<TEntity> GetAll();
}

public interface ICustomerRepository : IRepository<Customer>
{
    IEnumerable<Customer> GetSeniorCustomers();
}

public class CustomerRepository : ICustomerRepository
{
    private readonly DbContext _context;
    public CustomerRepository(DbContext context) : base(context)
    {
        _context = context;
    }
// ... implementation of ICustomerRepo here
}

Now, that's the current state of things:

And what I'd like to be able to do is:

public UnitOfWork(DbContext context, RepositoryFactory fac)
{
     _context = context;
     Customers = fac.Create(context, RepoType.Customer);
     Employees = fac.Create(context, RepoType.Employee);
}  

I know it doesn't really give me any additional flexibility, but it does keep the code a bit less clunky in my opinion.

But, as I mentioned earlier, I cannot think of a valid return type for the Create() method.

So, I came up with the idea to create multiple methods inside the RepositoryFactory class, instead of a single parameterized one, like this:

public class RepositoryFactory
{
    public ICustomerRepository CreateCustomerRepo(DbContext context){/*...*/}
    public IEmployeeRepository CreateEmployeeRepo(DbContext context){/*...*/} 
}

So the questions are:

  1. Can the thing I'm doing be even called a Factory Method?
  2. If not, is it at least a valid solution? If not, how can I achieve the same thing in a cleaner manner?

By achieve the same thing I mean implement a way of creating those repositories in a managable, concise way.

Thank you for all the help in advance.

回答1:

Your "multiple methods inside the RepositoryFactory class" solution is quite good if you do not care much about Open/Close principle. If this suits your needs, you may go with this.

Can the thing I'm doing be even called a Factory Method?

Yes; those are sort of factory methods. Anyway, do not care too much as long as it fulfill your requirements without creating new problem.

If not, is it at least a valid solution?

As said above, if it suits your needs; yes it is valid.

If not, how can I achieve the same thing in a cleaner manner?

Another alternative is factory method something like below:

public T CreateRepository<T>() where T : IRepositoryId
{
    IRepositoryId repository = null;
    if(typeof(T) == typeof(ICustomerRepository))
        repository = new CustomerRepository(context);
    ......
    ......
    else
        throw new XyzException("Repository type is not handled.");
    return (T)repository;
}

public interface IRepositoryId
{
    Guid RepositoryId { get; }
}

Your existing IRepository interface is generic. It creates problems while implementing above method with that interface. So, just create other interface as shown above. Derive each repository from this interface.



回答2:

Firstly, be clear about your goal. The repository pattern has (at least) 3 key reasons for existence:

1) To abstract away the data layer. As far as EF is concerned, if this is your goal then the repository pattern is no longer beneficial. Trying to abstract apps from Entity Framework is far more trouble than it is worth. You will end up with either/both a crippled DAL where the awesomeness that EF can provide is not available or inefficient/slow, or a very complex bunch of repository methods with expressions and other nastiness as parameters. Attempting to abstract your application away from EF (in case you might want to change to another ORM for instance) is pointless. Accept EF as you would accept the fact you're writing your application in .Net. To abstract out EF to a point it could be replaced you may as well not use it because you won't see any of the benefits EF can actually provide.

2) To make business logic easier to test. IMO This is still a valid argument for repositories with Entity Framework. Yes, EF DbContexts can be mocked, but they are still a messy business. Mocking repositories is no harder than any other dependency.

3) TO serve as a domain gatekeeper. Patterns such as DDD look to lock away actions against data in domain objects and services. The repository pattern can assist with this when using EF to help contain methods that are responsible for manipulating the domain. For pure DDD I wouldn't recommend them, though I wouldn't recommend using Entities as DDD domain objects either. I do use the repository pattern to manage the C.R. and D. aspects of CRUD and rely on view models to encapsulate domain logic around U.

The repository pattern I've found to be the most use in serving points 2 & 3 is to do away with the very common concept of generic repositories, and rather treat the repository more in-line with how you would treat a Controller in MVC. Except instead of between View and Model, between Model and Data. The repository is a class that serves a Controller (in MVC) in that it is responsible for creating, reading, (at a core level) and deleting entities. This pattern works very well in conjunction with unit of work. (https://github.com/mehdime/DbContextScope is the implementation I adopt.)

In Creating an entity it is responsible for ensuring all required (non-null-able) values and references are provided, returning an entity associated to the DbContext, ready to go. Effectively an entity factory. You could argue separation of concerns, though given the repository already has access to the DbContext to retrieve related entities it's a case of being pretty much the best place for the job.

In Reading entities it provides a base reference to further query entities by providing an IQueryable<TEntity>, enforcing core level rules such as .Where(x => x.IsActive) for soft-delete scenarios, or filters for authentication/authorization/tenancy based on dependencies that can reveal the current user for example. By exposing IQueryable you keep the repository implementation simple and give the consumer (Controller) control over how the data is consumed. This can leverage deferred execution to:

  • Select only the data needed for the view models.
  • Perform counts and exists checks. (.Any())
  • Customize filtering logic across the entity structure for the specific use case.
  • Perform pagination.
  • Grab as much (.ToList(), .Take()) or as little (.SingleOrDefault(), .FirstOrDefault()) data as needed.

Read methods are extremely easy to mock, and keep the repository implementation footprint quite small. Consumers need to be aware that they are dealing with Entities, and deal with the nuances of EF and it's proxies, but the consumer is the guardian of the Unit of Work (lifespan of the DbContext) so hiding this fact away from it is rather moot. Passing complex query expressions into repository methods as parameters leaves consumers equally responsible for knowing about EF's nuances. An expression that calls a private method fed to a generic repository to go in a Where clause will break things just as fast. Going down this route trips point #1 above, don't abstract away EF from your app.

In Deleting an entity it ensures that entities and their associations are properly managed, whether hard or soft deletes.

I avoid generic repositories because just as a Controller (and view) will deal with any number of related domain view models, this means that they will need to deal with a number of related data entities. Operations against any one entity will invariably be tied to operations against other dependents. With generic repositories that separation means a) an ever growing number of dependencies and b) generic methods that do trivial crap and a lot of custom code to handle the meaningful stuff, or complex code to try and facilitate it in a generic (base) way. By having a single repository per controller, and perhaps some truly common shared repositories for common entities. (look-ups for example) my repositories are designed explicitly to serve one area of an application and have only one reason to change. Sure, there may be 2 or more screens that require the same behavior from a repository, but as those aspects of an application or service mature, their repositories can mature/optimize as needed without side-effects. SRP and KISS easily trump DNRY.

Generic classes in general have their uses, but in almost any case where I see developers write them, I would argue that it is premature optimization. Start with non-generic implementations, then as a product matures, optimize generics into the code rather than attempting to design architecture around them. The result is almost invariably the realization that you need to de-optimize them out or get "clever" about working around discovered limitations where the patterns hinder development.

Anyhow, some food for thought other than "repositories aren't needed with EF" :)