Using multiple DbContexts with a generic repository and unit of work

19,790

Solution 1

Don't split your modular data pieces into multiple DbContexts unless there are logical seams for doing so. Entities from DbContextA cannot have automatic navigation or collection properties with entities in DbContextB. If you split the context, your code would have to be responsible for manually enforcing constraints and loading related data between contexts.

For "sake of overview" (a.k.a. keeping your sanity), you can still organize your CLR code and database tables by module. For the POCO's, keep them in different folders under different namespaces. For tables, you can group by schema. (However you probably should also take security considerations into account when organizing by SQL schema. For example, if there are any db users that should have restricted access to certain tables, design the schemas according to those rules.) Then, you can do this when building the model:

ToTable("TableName", "SchemaName"); // put table under SchemaName, not dbo

Only go with a separate DbContext when its entities have no relationships with any entities in your first DbContext.

I also agree with Wiktor in that I don't like your interface & implementation design. I especially don't like public interface IRepository<T>. Also, why declare multiple public DbSet<TableN> TableN { get; set; } in your MyDbContext? Do me a favor, read this article, then read this one.

You can greatly simplify your code with an EF interface design like this:

interface IUnitOfWork
{
    int SaveChanges();
}
interface IQueryEntities
{
    IQueryable<T> Query<T>(); // implementation returns Set<T>().AsNoTracking()
    IQueryable<T> EagerLoad<T>(IQueryable<T> queryable, Expression<Func<T, object>> expression); // implementation returns queryable.Include(expression)
}
interface ICommandEntities : IQueryEntities, IUnitOfWork
{
    T Find<T>(params object[] keyValues);
    IQueryable<T> FindMany<T>(); // implementation returns Set<T>() without .AsNoTracking()
    void Create<T>(T entity); // implementation changes Entry(entity).State
    void Update<T>(T entity); // implementation changes Entry(entity).State
    void Delete<T>(T entity); // implementation changes Entry(entity).State
    void Reload<T>(T entity); // implementation invokes Entry(entity).Reload
}

If you declare MyDbContext : ICommandEntities, you just have to set up a few methods to implement the interface (usually one-liners). You can then inject any of the 3 interfaces into your service implementations: usually ICommandEntities for operations that have side effects, and IQueryEntities for operations that don't. Any services (or service decorators) responsible only for saving state can take a dependency on IUnitOfWork. I disagree that Controllers should take a dependency on IUnitOfWork though. Using the above design, your services should save changes before returning to the Controller.

If having multiple separate DbContext classes in your app ever makes sense, you can do as Wiktor suggests and make the above interfaces generic. You can then dependency inject into services like so:

public SomeServiceClass(IQueryEntities<UserEntities> users,
    ICommandEntities<EstateModuleEntities> estateModule) { ... }

public SomeControllerClass(SomeServiceClass service) { ... }

// Ninject will automatically constructor inject service instance into controller
// you don't need to pass arguments to the service constructor from controller

Creating wide per-aggregate (or even worse per-entity) repository interfaces can fight with EF, multiply boring plumbing code, and over-inject your constructors. Instead, give your services more flexibility. Methods like .Any() don't belong on the interface, you can just call extensions on the IQueryable<T> returned by Query<T> or FindMany<T> from within your service methods.

Solution 2

Your unit of work interface is not generic but the implementation is. The easiest way to clean up this would be to decide and follow the same convention.

For example, make your interface generic also. This way you could register three different interfaces (the same interface with three different generic parameters) to three different implementations:

 container.Bind( typeof<IUnitOfWork<ContextOne>> ).To( typeof<UnitOfWork<ContextOne>> );
 ...

And yes, this is a good idea to inject your unit of works into controllers / services.

Share:
19,790
janhartmann
Author by

janhartmann

Hello! I'm Jan Hartmann. With a wide set of skills, I enjoy building awesome, highly effective and performing web- and mobile and applications. http://dk.linkedin.com/in/janhartmanndk

Updated on June 04, 2022

Comments

  • janhartmann
    janhartmann almost 2 years

    My application is getting larger and so far I have a single MyDbContext which has all the tables I need in my application. I wish (for the sake of overview) to split them up into multiple DbContext, like MainDbContext, EstateModuleDbContext, AnotherModuleDbContext and UserDbContext.

    I am unsure how this is done probably as I am right now using dependecy injection (ninject) to place my DbContext on my UnitOfWork class like:

    kernel.Bind(typeof(IUnitOfWork)).To(typeof(UnitOfWork<MyDbContext>));
    

    Should I drop this approach with dependency injection and explicit set the DbContext I wish to use on my services like:

    private readonly EstateService _estateService;
    
    public HomeController()
    {
        IUnitOfWork uow = new UnitOfWork<MyDbContext>();
        _estateService = new EstateService(uow);
    }
    

    Instead of:

    private readonly EstateService _estateService;
    
    public HomeController(IUnitOfWork uow)
    {
        _estateService = new EstateService(uow);
    }
    

    Or this there another better approach? Also as a side question, I dont like passing the uow to my service - is there another (better) approach?

    Code

    I have this IDbContext and MyDbContext:

    public interface IDbContext
    {
        DbSet<T> Set<T>() where T : class;
    
        DbEntityEntry<T> Entry<T>(T entity) where T : class;
    
        int SaveChanges();
    
        void Dispose();
    }
    
    public class MyDbContext : DbContext, IDbContext
    {
        public DbSet<Table1> Table1 { get; set; }
        public DbSet<Table2> Table1 { get; set; }
        public DbSet<Table3> Table1 { get; set; }
        public DbSet<Table4> Table1 { get; set; }
        public DbSet<Table5> Table1 { get; set; }
        /* and so on */
    
        static MyDbContext()
        {
            Database.SetInitializer<MyDbContext>(new CreateDatabaseIfNotExists<MyDbContext>());
        }
    
        public MyDbContext()
            : base("MyDbContext")
        {
        }
    
        protected override void OnModelCreating(DbModelBuilder modelBuilder)
        {
    
        }
    }
    

    Then I have this IRepository and the implementation:

    public interface IRepository<T> where T : class
    {
        IQueryable<T> GetAll();
    
        void Add(T entity);
    
        void Delete(T entity);
    
        void DeleteAll(IEnumerable<T> entity);
    
        void Update(T entity);
    
        bool Any();
    }
    
    public class Repository<T> : IRepository<T> where T : class
    {
        private readonly IDbContext _context;
        private readonly IDbSet<T> _dbset;
    
        public Repository(IDbContext context)
        {
            _context = context;
            _dbset = context.Set<T>();
        }
    
        public virtual IQueryable<T> GetAll()
        {
            return _dbset;
        }
    
        public virtual void Add(T entity)
        {
            _dbset.Add(entity);
        }
    
        public virtual void Delete(T entity)
        {
            var entry = _context.Entry(entity);
            entry.State = EntityState.Deleted;
            _dbset.Remove(entity);
        }
    
        public virtual void DeleteAll(IEnumerable<T> entity)
        {
            foreach (var ent in entity)
            {
                var entry = _context.Entry(ent);
                entry.State = EntityState.Deleted;
                _dbset.Remove(ent);
            }
        }
    
        public virtual void Update(T entity)
        {
            var entry = _context.Entry(entity);
            _dbset.Attach(entity);
            entry.State = EntityState.Modified;
        }
    
        public virtual bool Any()
        {
            return _dbset.Any();
        }
    }
    

    And the IUnitOfWork and implemention which handles the work done with the DbContext

    public interface IUnitOfWork : IDisposable
    {
        IRepository<TEntity> GetRepository<TEntity>() where TEntity : class;
    
        void Save();
    }
    
    public class UnitOfWork<TContext> : IUnitOfWork where TContext : IDbContext, new()
    {
        private readonly IDbContext _ctx;
        private readonly Dictionary<Type, object> _repositories;
        private bool _disposed;
    
        public UnitOfWork()
        {
            _ctx = new TContext();
            _repositories = new Dictionary<Type, object>();
            _disposed = false;
        }
    
        public IRepository<TEntity> GetRepository<TEntity>() where TEntity : class
        {
            // Checks if the Dictionary Key contains the Model class
            if (_repositories.Keys.Contains(typeof(TEntity)))
            {
                // Return the repository for that Model class
                return _repositories[typeof(TEntity)] as IRepository<TEntity>;
            }
    
            // If the repository for that Model class doesn't exist, create it
            var repository = new Repository<TEntity>(_ctx);
    
            // Add it to the dictionary
            _repositories.Add(typeof(TEntity), repository);
    
            return repository;
        }
    
        public void Save()
        {
            _ctx.SaveChanges();
        }
    
        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }
    
        protected virtual void Dispose(bool disposing)
        {
            if (this._disposed) return;
    
            if (disposing)
            {
                _ctx.Dispose();
            }
    
            this._disposed = true;
        }
    }