Database abstraction layer design - Using IReposit

2019-01-21 05:09发布

问题:

I'm in the process of designing my ASP.NET MVC application and I ran across a couple of interesting thoughts.

Many samples I have seen describe and use the Repository pattern (IRepository) so this is the way I did it while I was learning MVC.

Now I know what it's all doing, I starting to look at my current design and wonder if it's the best way to go.

Currently I have a basic IUserRepository, which defines methods such as FindById(), SaveChanges(), etc.

Currently, whenever I want to load/query the user table in the DB, I do something along the lines of the following:

    private IUserRepository Repository;

    public UserController()
        : this(new UserRepository())
    { }

    [RequiresAuthentication]
    [AcceptVerbs(HttpVerbs.Get)]
    public ActionResult Edit(string ReturnUrl, string FirstRun)
    {
        var user = Repository.FindById(User.Identity.Name);

        var viewModel = Mapper.Map<User, UserEditViewModel>(user);
        viewModel.FirstRun = FirstRun == "1" ? true : false;

        return View("Edit", viewModel);
    }

    [AcceptVerbs(HttpVerbs.Post), ValidateAntiForgeryToken(Salt = "SaltAndPepper")]
    public ActionResult Edit(UserEditViewModel viewModel, string ReturnUrl)
    {
        //Map the ViewModel to the Model
        var user = Repository.FindById(User.Identity.Name);

        //Map changes to the user
        Mapper.Map<UserEditViewModel, User>(viewModel, user);

        //Save the DB changes
        Repository.SaveChanges();

        if (!string.IsNullOrEmpty(ReturnUrl))
            return Redirect(ReturnUrl);
        else
            return RedirectToAction("Index", "User");
    }

Now I don't fully understand how MVC works in regards to creating a controller when a user creates a link (not sure if there is 1 controller per user or 1 controller per application), so I'm not positive of the best course of action.

I found a great question regarding the use of a generic repository interface IRepository<T> here and have also seem the idea of a static RepositoryFactory on a number of blogs. Basically only 1 instance of the repository is kept ever and it is obtained via this factory

So my question revolves around how people do it in there apps, and whats considered good practice.

Do people have individual repositorys based on each table (IUserRepository)?
Do they use a generic IRepository<T>?
Do they use a static repository factory?
Or something else completely?

EDIT: I just realised I should probably ask as well:

Is having a private IRepository on each controller a good way to go? or should I instantiate a new IRepository every time I want to use it?

BOUNTY EDIT: I'm starting a bounty to get some more perspectives (not that Tim's wasn't helpful).

I'm more curious to know what people do in their MVC apps or what they think is a good idea.

回答1:

Some very obvious problems with the idea of a generic IRepository<T>:

  • It assumes that every entity uses the same type of key, which is not true in almost any non-trivial system. Some entities will use GUIDs, others may have some kind of natural and/or composite key. NHibernate can support this fairly well but Linq to SQL is pretty bad at it - you have to write a good deal of hackish code to do automatic key mapping.

  • It means that each repository can only deal with exactly one entity type and only supports the most trivial operations. When a repository is relegated to such a simple CRUD wrapper it has very little use at all. You might as well just hand the client an IQueryable<T> or Table<T>.

  • It assumes that that you perform exactly the same operations on every entity. In reality this is going to be very far from the truth. Sure, maybe you want to get that Order by its ID, but more likely you want to get a list of Order objects for a specific customer and within some date range. The notion of a totally generic IRepository<T> doesn't allow for the fact that you'll almost certainly want to perform different types of queries on different types of entities.

The whole point of the repository pattern is to create an abstraction over common data access patterns. I think some programmers get bored with creating repositories so they say "Hey, I know, I'll create one über-repository that can handle any entity type!" Which is great except that the repository is pretty much useless for 80% of what you're trying to do. It's fine as a base class/interface, but if that's the full extent of the work that you do then you're just being lazy (and guaranteeing future headaches).


Ideally I might start with a generic repository that looks something like this:

public interface IRepository<TKey, TEntity>
{
    TEntity Get(TKey id);
    void Save(TEntity entity);
}

You'll notice that this doesn't have a List or GetAll function - that's because it's absurd to think that it's acceptable to retrieve the data from an entire table at once anywhere in the code. This is when you need to start going into specific repositories:

public interface IOrderRepository : IRepository<int, Order>
{
    IEnumerable<Order> GetOrdersByCustomer(Guid customerID);
    IPager<Order> GetOrdersByDate(DateTime fromDate, DateTime toDate);
    IPager<Order> GetOrdersByProduct(int productID);
}

And so on - you get the idea. This way we have the "generic" repository for if we ever actually need the incredibly simplistic retrieve-by-id semantics, but in general we're never actually going to pass that around, certainly not to a controller class.


Now as for controllers, you have to do this right, otherwise you've pretty much negated all the work you just did in putting together all the repositories.

A controller needs to take its repository from the outside world. The reason you created these repositories is so you can do some kind of Inversion of Control. Your ultimate goal here is to be able to swap out one repository for another - for example, to do unit testing, or if you decide to switch from Linq to SQL to Entity Framework at some point in the future.

An example of this principle is:

public class OrderController : Controller
{
    public OrderController(IOrderRepository orderRepository)
    {
        if (orderRepository == null)
            throw new ArgumentNullException("orderRepository");
        this.OrderRepository = orderRepository;
    }

    public ActionResult List(DateTime fromDate, DateTime toDate) { ... }
    // More actions

    public IOrderRepository OrderRepository { get; set; }
}

In other words the Controller has no idea how to create a repository, nor should it. If you have any repository-construction going on in there, it's creating coupling that you really don't want. The reason that the ASP.NET MVC sample controllers have parameterless constructors that creates concrete repositories is that the sites need to be able to compile and run without forcing you to set up an entire Dependency Injection framework.

But in a production site, if you aren't passing in the repository dependency through a constructor or public property, then you're wasting your time having repositories at all, because the controllers are still tightly coupled to the database layer. You need to be able to write test code like this:

[TestMethod]
public void Can_add_order()
{
    OrderController controller = new OrderController();
    FakeOrderRepository fakeRepository = new FakeOrderRepository();
    controller.OrderRepository = fakeRepository; //<-- Important!
    controller.SubmitOrder(...);
    Assert.That(fakeRepository.ContainsOrder(...));
}

You can't do this if your OrderController is going off and creating its own repository. This test method isn't supposed to do any data access, it just makes sure that the controller is invoking the correct repository method based on the action.


This isn't DI yet, mind you, this is just faking/mocking. Where DI comes into the picture is when you decide that Linq to SQL isn't doing enough for you and you really want the HQL in NHibernate, but it's going to take you 3 months to port everything over, and you want to be able to do this one repository at a time. So, for example, using a DI Framework like Ninject, all you have to do is change this:

Bind<ICustomerRepository>().To<LinqToSqlCustomerRepository>();
Bind<IOrderRepository>().To<LinqToSqlOrderRepository>();
Bind<IProductRepository>().To<LinqToSqlProductRepository>();

To:

Bind<ICustomerRepository>().To<LinqToSqlCustomerRepository>();
Bind<IOrderRepository>().To<NHibernateOrderRepository>();
Bind<IProductRepository>().To<NHibernateProductRepository>();

And there you are, now everything that depends on IOrderRepository is using the NHibernate version, you've only had to change one line of code as opposed to potentially hundreds of lines. And we're running the Linq to SQL and NHibernate versions side by side, porting functionality over piece by piece without ever breaking anything in the middle.


So to summarize all the points I've made:

  1. Don't rely strictly on a generic IRepository<T> interface. Most of the functionality you want from a repository is specific, not generic. If you want to include an IRepository<T> at the upper levels of the class/interface hierarchy, that's fine, but controllers should depend on specific repositories so you don't end up having to change your code in 5 different places when you find that the generic repository is missing important methods.

  2. Controllers should accept repositories from the outside, not create their own. This is an important step in eliminating coupling and improving testability.

  3. Normally you'll want to wire up the controllers using a Dependency Injection framework, and many of them can be seamlessly integrated with ASP.NET MVC. If that's too much for you to take in, then at the very least you should be using some kind of static service provider so you can centralize all of the repository-creation logic. (In the long run, you'll probably find it easier to just learn and use a DI framework).



回答2:

Do people have individual repositorys based on each table (IUserRepository)? I tend to have a repository for each aggregate, not each table.

Do they use a generic IRepository? if possible, yes

Do they use a static repository factory? I prefer the injection of a Repository instance through an IOC container



回答3:

Here is how I'm using it. I'm using IRepository for all the operations that are common to all of my repositories.

public interface IRepository<T> where T : PersistentObject
{
    T GetById(object id);
    T[] GetAll();
    void Save(T entity);
}

and I use also a dedicated an ITRepository for each aggregate for the operation that are distinct to this repository. For example for user I will use IUserRepository to add method that are distinct to UserRepository:

public interface IUserRepository : IRepository<User>
{
    User GetByUserName(string username);
}

The implementation will look like this:

public class UserRepository : RepositoryBase<User>, IUserRepository
{
    public User GetByUserName(string username)
    {
        ISession session = GetSession();
        IQuery query = session.CreateQuery("from User u where u.Username = :username");
        query.SetString("username", username);

        var matchingUser = query.UniqueResult<User>();

        return matchingUser;
    }
}


public class RepositoryBase<T> : IRepository<T> where T : PersistentObject
{
    public virtual T GetById(object id)
    {
        ISession session = GetSession();
        return session.Get<T>(id);
    }

    public virtual T[] GetAll()
    {
        ISession session = GetSession();
        ICriteria criteria = session.CreateCriteria(typeof (T));
        return criteria.List<T>().ToArray();
    }

    protected ISession GetSession()
    {
        return new SessionBuilder().GetSession();
    }

    public virtual void Save(T entity)
    {
        GetSession().SaveOrUpdate(entity);
    }
}

Than in the UserController will look as:

public class UserController : ConventionController
{
    private readonly IUserRepository _repository;
    private readonly ISecurityContext _securityContext;
    private readonly IUserSession _userSession;

    public UserController(IUserRepository repository, ISecurityContext securityContext, IUserSession userSession)
    {
        _repository = repository;
        _securityContext = securityContext;
        _userSession = userSession;
    }
}

Than the repository is instantiated using dependency injection pattern using custom controller factory. I'm using StructureMap as my dependency injection layer.

The database layer is NHibernate. ISession is the gateway to the the database in this session.

I'm suggest you to look on CodeCampServer structure, you can learn a lot from it.

Another project that you can learn fro it is Who Can Help Me. Which I don't dig enough in it yet.



回答4:

Do people have individual repositorys based on each table (IUserRepository)?

Yes, this was the better choice for 2 reasons :

  • my DAL is based on Linq-to-Sql (but my DTOs are interfaces based on the LTS entities)
  • the performed operations are atomic (adding is an atomic operation, saving is another one, etc.)

Do they use a generic IRepository?

Yes, exclusively, inspired on DDD Entity/Value scheme I created IRepositoryEntity / IRepositoryValue and a generic IRepository for other stuff.

Do they use a static repository factory?

Yes and no : I use an IOC container invoked via a static class. Well... we can say it's a kind of factory.

Note : I designed this architecture on my own and a colleague of mine found it so great that we are currently creating our whole company framework on this model (yes it's a young company). This is definitely something worth trying, even if I feel that frameworks of that kind will be released by major actors.



回答5:

You can find an excellent Generic Repopsitory library that was written to enable it to be used as a WebForms asp:ObjectDataSource on codeplex: MultiTierLinqToSql

Each of my controllers have private repositories for the actions they need to support.