Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
280 views
in Technique[技术] by (71.8m points)

.net - Database abstraction layer design - Using IRepository the right way?

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.

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Answer

0 votes
by (71.8m points)

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).


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

2.1m questions

2.1m answers

60 comments

57.0k users

...