EF and repository pattern - ending up with multipl

2019-02-01 14:55发布

问题:

Most of my knowledge of ASP.NET MVC 3 comes from reading through the book Pro ASP.NET MVC 3 Framework by Adam Freeman and Steven Senderson. For my test application I have tried to stick to their examples very closely. I am using the repository pattern plus Ninject and Moq which means that unit testing work quite well (i.e. without needing to pull data from the database).

In the book repositories are used like this:

public class EFDbTestChildRepository
{
    private EFDbContext context = new EFDbContext();

    public IQueryable<TestChild> TestChildren
    {
        get { return context.TestChildren; }
    }

    public void SaveTestChild(TestChild testChild)
    {
        if (testChild.TestChildID == 0)
        {
            context.TestChildren.Add(testChild);
        }
        else
        {
            context.Entry(testChild).State = EntityState.Modified;
        }
        context.SaveChanges();
    }
}

And here is the DbContext that goes with it:

public class EFDbContext : DbContext
{
    public DbSet<TestParent> TestParents { get; set; }
    public DbSet<TestChild> TestChildren { get; set; }
}

Please note: to keep things simple in this extracted example I have left out the interface ITestChildRepository here which Ninject would then use.

In other sources I have seen a more general approach for the repository where one single repository is enough for the whole application. Obviously in my case I end up with quite a list of repositories in my application - basically one for each entity in my domain model. Not sure about the pros and cons about the two approaches - I just followed the book to be on the safe side.

To finally get to my question: each repository has its own DbContext - private EFDbContext context = new EFDbContext();. Do I risk ending up with multiple DbContexts within one request? And would that lead to any significant performance overhead? How about a potential for conflicts between the contexts and any consequences to the data integrity?

Here is an example where I ended up with more than one repository within a controller.

My two database tables are linked with a foreign key relationship. My domain model classes:

public class TestParent
{
    public int TestParentID { get; set; }
    public string Name { get; set; }
    public string Comment { get; set; }

    public virtual ICollection<TestChild> TestChildren { get; set; }
}

public class TestChild
{
    public int TestChildID { get; set; }
    public int TestParentID { get; set; }
    public string Name { get; set; }
    public string Comment { get; set; }

    public virtual TestParent TestParent { get; set; }
}

The web application contains a page that allows the user to create a new TestChild. On it there is a selectbox that contains a list of available TestParents to pick from. This is what my controller looks like:

public class ChildController : Controller
{
    private EFDbTestParentRepository testParentRepository = new EFDbTestParentRepository();
    private EFDbTestChildRepository testChildRepository = new EFDbTestChildRepository();

    public ActionResult List()
    {
        return View(testChildRepository.TestChildren);
    }

    public ViewResult Edit(int testChildID)
    {
        ChildViewModel cvm = new ChildViewModel();
        cvm.TestChild = testChildRepository.TestChildren.First(tc => tc.TestChildID == testChildID);
        cvm.TestParents = testParentRepository.TestParents;
        return View(cvm);
    }

    public ViewResult Create()
    {
        ChildViewModel cvm = new ChildViewModel();
        cvm.TestChild = new TestChild();
        cvm.TestParents = testParentRepository.TestParents;
        return View("Edit", cvm);
    }

    [HttpPost]
    public ActionResult Edit(TestChild testChild)
    {
        try
        {
            if (ModelState.IsValid)
            {
                testChildRepository.SaveTestChild(testChild);
                TempData["message"] = string.Format("Changes to test child have been saved: {0} (ID = {1})",
                                                        testChild.Name,
                                                        testChild.TestChildID);
                return RedirectToAction("List");
            }
        }
        catch (DataException)
        {
            //Log the error (add a variable name after DataException)
            ModelState.AddModelError("", "Unable to save changes. Try again, and if the problem persists see your system administrator.");
        }

        // something wrong with the data values
        return View(testChild);
    }
}

It's not enough to have an EFDbTestChildRepository available but I also need an EFDbTestParentRepository. Both of them are assigned to private variables of the controller - and voila, it seems to me that two DbContexts have been created. Or is that not correct?

To avoid the issue I tried using EFDbTestChildRepository to get to the TestParents. But that obviously will only bring up those that are already hooked up to at least one TestChild - so not what I want.

Here is the code for the view model:

public class ChildViewModel
{
    public TestChild TestChild { get; set; }
    public IQueryable<TestParent> TestParents { get; set; }
}

Please let me know if I forgot to include some relevant code. Thanks so much for your advice!

回答1:

There won't be a performance problem (unless we are talking about nanoseconds, instantiating a context is very cheap) and you won't have damaged your data integrity (before that happens you'll get exceptions).

But the approach is very limited and will work only in very simple situations. Multiple contexts will lead to problems in many scenarios. As an example: Suppose you want to create a new child for an existing parent and would try it with the following code:

var parent = parentRepo.TestParents.Single(p => p.Id == 1);
var child = new Child { TestParent = parent };
childrenRepo.SaveTestChild(child);

This simple code won't work because parent is already attached to the context inside of parentRepo but childrenRepo.SaveTestChild will try to attach it to the context inside of childrenRepo which will cause an exception because an entity must not be attached to another context. (Here is actually a workaround because you could set the FK property instead of loading the parent: child.TestParentID = 1. But without a FK property it would be a problem.)

How to solve such a problem?

One approach could be to extend the EFDbTestChildRepository by a new property:

public IQueryable<TestParent> TestParents
{
    get { return context.TestParents; }
}

In the example code above you could then use only one repository and the code would work. But as you can see, the name "EFDbTest Child Repository" doesn't really fit anymore to the purpose of the new repository. It should be now "EFDbTest ParentAndChild Repository".

I would call this the Aggregate Root approach which means that you create one repository not for only one entity but for a few entities which are closely related to each other and have navigation properties between them.

An alternative solution is to inject the context into the repositories (instead of creating it in the repositories) to make sure that every repository uses the same context. (The context is often abstracted into a IUnitOfWork interface.) Example:

public class MyController : Controller
{
    private readonly MyContext _context;
    public MyController()
    {
        _context = new MyContext();
    }

    public ActionResult SomeAction(...)
    {
        var parentRepo = new EFDbTestParentRepository(_context);
        var childRepo = new EFDbTestChildRepository(_context);

        //...
    }

    protected override void Dispose(bool disposing)
    {
        _context.Dispose();
        base.Dispose(disposing);
    }
}

This gives you a single context per controller you can use in multiple repositories.

The next step might be to create a single context per request by dependency injection, like...

private readonly MyContext _context;
public MyController(MyContext context)
{
    _context = context;
}

...and then configuring the IOC container to create a single context instance which gets injected into perhaps multiple controllers.



回答2:

Do I risk ending up with multiple DbContexts within one request?

Yes. Each instance of a repository is going to instantiate its own DbContexts instances. Depending on the size and use of the application, this may not be a problem although it is not a very scalable approach. There are several ways of handling this though. In my web projects I add the DbContext(s) to the Request's Context.Item collection, this way it is available to all classes that require it. I use Autofac (similar to Ninject) to control what DbContexts are created within specific scenarios and how they are stored, e.g. I have a different 'session manager' for a WCF context to the one for a Http context.

And would that lead to any significant performance overhead?

Yes, but again not massively if the application is relatively small. As it grows though, you may notice the overhead.

How about a potential for conflicts between the contexts and any consequences to the data integrity?

One of the reasons for using an ORM like this is so that changes can be maintained within the DbContext. If you are instantiating multiple context instances per request you lose this benefit. You wouldn't notice conflicts or any impact of the integrity per se unless you were handling a lot of updates asynchronously.



回答3:

As promised I post my solution.

I came across your question because I was having trouble with the IIS application pool memory growing beyond limits and having multiple DBContexts was one of my suspects. In retrospect it is fair to say that there were other causes for my trouble. However, it challenged me to find a better layer based design for my repository.

I found this excellent blog: Correct use of Repository and Unit Of Work patterns in ASP.NET MVC leading me to the right direction. The redesign is based on the UnitOfWork pattern. It enables me to have just one constructor parameter for all my controllers instead of "never ending constructor parameters". And after that, I was able to introduce proactive caching as well, which solved a great deal of the earlier mentioned trouble I was having.

Now I only have these classes:

  • IUnitOfWork
  • EFUnitOfWork
  • IGenericRepository
  • EFGenericRepository

See the referred blog for complete information and implementation of these classes. Just to give an example, IUnitOfWork contains repository definitions for all entities that I need, like:

namespace MyWebApp.Domain.Abstract
{
  public interface IUnitOfWork : IDisposable
  {
    IGenericRepository<AAAAA> AAAAARepository { get; }
    IGenericRepository<BBBBB> BBBBBRepository { get; }
    IGenericRepository<CCCCC> CCCCCRepository { get; }
    IGenericRepository<DDDDD> DDDDDRepository { get; } 
    // etc.

    string Commit();
  }
}

The Dependency Injection (DI) is just one statement (I use Ninject):

ninjectKernel.Bind<IUnitOfWork>().To<EFUnitOfWork>();

The Controllers-constructors are maintainable:

public class MyController : BaseController
{
  private MyModel mdl = new MyModel();

  private IUnitOfWork _context; 

  public MyController(IUnitOfWork unitOfWork)
  {
    _context = unitOfWork;

    // intialize whatever needs to be exposed to the View:
    mdl.whatever = unitOfWork.SomeRepository.AsQueryable(); 
  }

 // etc.

Within the Controller I can use _context to access all repositories, if needed. The nice part of it, is that it needs just a single Commit()-call to save changed data for all repositories:

_context.Commit();