Multiple database contexts when using repository p

2020-06-25 06:57发布

问题:

I am a bit lost right now... I've never seen this much divergent information regarding solution to the problem. But let us start from the beginning.

I am using ASP.NET MVC with Repositories injected to Controllers, thanks to the Ninject. I have 2 simple Entities: Admin with a list of created blog entries and Entries with one virtual Admin field.

Admin:

public class Admin
{
    [Key, ScaffoldColumn(false)]
    public int Id { get; set; }

    [Required(ErrorMessage = "Zły login.")]
    [StringLength(20), MinLength(3)]
    [RegularExpression(@"^[a-zA-Z0-9]*$", ErrorMessage = "Special characters are not allowed.")]
    public string Login { get; set; }

    [Required(ErrorMessage = "Złe hasło.")]
    [StringLength(20, MinimumLength = 3)]
    [DataType(DataType.Password)]
    [Display(Name = "Hasło")]
    public string Password { get; set; }

    public virtual List<Entry> CreatedEntries { get; set; } // napisane aktualności przez danego admina
}

Entry:

public class Entry
{
    [Key, ScaffoldColumn(false)]
    public int Id { get; set; }

    [StringLength(200, MinimumLength = 2)]
    [DataType(DataType.Text)]
    [Display(Name = "Tytuł")]
    public string Title { get; set; }

    [Required, StringLength(2000), MinLength(3)]
    [Display(Name = "Treść")]
    [UIHint("tinymce_jquery_full"), AllowHtml]
    public string Text { get; set; }

    public virtual Admin Admin { get; set; }
}

You probably know where it is going, since this problem is... "classic" on stackoverflow.

In the Controller I want to bind one object to another:

entry.Admin = repAdmins.GetAdmin(User.Identity.Name);

repEntries.AddEntry(entry);

In the repository:

public void AddEntry(Entry entry)
    {
        db.Entries.Add(entry);
        db.SaveChanges();
    }

Of course I can't do that, because of famous "An entity object cannot be referenced by multiple instances of IEntityChangeTracker", which is a result of having separate database contexts in each repository.

When I was searching for a solution I already knew that probably the best way to solve it is to use one common context. And then I discovered Unit Of Work pattern. But here's when the real problems starts.

  1. On many sites the solution to this is a bit different.
  2. The repositories must have common generic interface (which I don't want to use, because I don't need to have each CRUD operation on each Entity, plus sometimes I need to have extra methods like "IfExists", etc.)
  3. On few sites I've read that this whole abstraction is not needed, since abstraction is already provided with Entity Framework and UoW is implemented in DbContext (whatever that means)
  4. The Unit Of Work pattern (at least from examples on the internet) seems to be a real pain for me...

I need some guidance... I learn ASP.NET MVC for only a year. For me it seems like it's a "triumph of form over content". Because... What I simply need is to bind one object to another. I'm starting to think that it was better when I simply had a context object in the Controller and I didn't need to build Eiffel Tower to achieve what's mentioned above :\ However I like idea of repositories...

回答1:

The following example shows how to use the same context within multiple repositories. To simplify it, I did not use interfaces and nor did I use a container to inject dependencies.

Controller class:

public class HomeController : Controller
{
    Context context;
    AdminRepository adminRepository;
    EntryRepository entryRepository;

    public HomeController()
    {
        context = new Context();
        adminRepository = new AdminRepository(context);
        entryRepository = new EntryRepository(context);
    }
    // GET: Home
    public ActionResult Index()
    {
        string login = "MyLogin";
        Admin admin = adminRepository.GetAdmin(login);
        Entry entry = new Entry() { Admin = admin};
        entryRepository.AddEntry(entry);
        return View(entry);
    }
}

Repositories:

public class AdminRepository
{
    Context context;
    public AdminRepository(Context context)
    {
        this.context = context;

        // This seeds the database
        Admin admin = new Admin() { Login = "MyLogin" };
        this.context.Admins.Add(admin);
        this.context.SaveChanges();
    }

    public Admin GetAdmin(string login)
    {
        return context.Admins.Where(a => a.Login == login).FirstOrDefault();
    }
}

public class EntryRepository
{
    Context context;
    public EntryRepository(Context context)
    {
        this.context = context;
    }

    public void AddEntry(Entry entry){
        context.Entrys.Add(entry);
        context.SaveChanges();
    }
}

Context class:

public class Context : DbContext
{
    public Context()
    {
        Database.SetInitializer<Context>(new DropCreateDatabaseAlways<Context>());
        Database.Initialize(true);
    }

    public DbSet<Admin> Admins { get; set; }
    public DbSet<Entry> Entrys { get; set; }
}

Modified Models:

public class Admin
{
    public int Id { get; set; }
    public string Login { get; set; }

}

public class Entry
{
    public int Id { get; set; }
    public virtual Admin Admin { get; set; }
}


回答2:

I'll open by simply answering the question straight-out. Simply, your repository should take the context as a dependency (it should have a constructor that accepts a param of type DbContext). Your context should be managed by Ninject, and then injected into your repository and/or your controller. That way, everything always uses the same context. You should do all this in "request" scope, so that the context is specific to the current request.

That said, I'd like to hit some of your other points. First, a repository is just a method of access. It really shouldn't be dependent on the entity. It's okay to have methods that you don't intend to use on a particular entity: just don't use them. However, if you do want to enforce this, you can always use generic constraints and interfaces. For example, let's say you don't want update available on a particular entity. You could have interfaces like:

public interface ICreateable
{
}

public interface IUpdateable : ICreateable
{
}

Then, your entity that should not be updated will implement only ICreateable while other entities (which allow update) would implement IUpdateable (which by interface inheritance, also implement ICreateable). Finally, you would add constraints on your repository methods:

public void Create<TEntity>(TEntity entity)
    where TEntity : class, ICreateable

public void Update<TEntity>(TEntity entity>)
    where TEntity : class, IUpdateable

Since, the entity in question only implements ICreatable, it will not be eligible to be used as a type param to Update, so there's then no way to utilize that method.

Next, the advice to not use the repository/UoW patterns with Entity Framework is indeed because Entity Framework already implements these patterns. The repository pattern exists as a way to contain all the database querying logic (constructing SQL statements and such) in one place. That is the "abstraction" we're talking about here. In other words, instead of directly constructing SQL statements in your application code, that code is abstracted away into a repository. However, this is exactly what Entity Framework does, which is why you don't need to do it again. The Unit of Work pattern exists as a method to orchestrate the work of multiple repositories, allowing things like transactions. However, again, Entity Framework does all this.

The only reason to add any further abstraction is if you want to abstract the actual provider, i.e. Entity Framework itself. For example, you could have an interface like IRepository and then create implementations like EntityFrameworkRepository, NHibernateRepository, WebApiRepository, etc. Your application would only ever depend on IRepository, and you could then sub in different implementations as needed. If you're not going to do this, or you will always be using Entity Framework, then you might as well just use your context directly. Any further abstraction is just something else to maintain with no benefit at all to your application.

Finally, yes, the Unit of Work pattern is a real pain to everyone, not just you. Which is why I forgo it entirely. I use what I call a "truly generic repository", which utilizes generic methods and interfaces to handle any entity I want to throw at it. That means it acts not only as a repository but also a unit of work as well. You only need one instance per context and it's provider-agnostic. For more information check out the article I wrote on the subject over on my website.