C# – Unit Of Work with multiple database context

.net corecdesign-patternsrepository-patternunit-of-work

I have created an application (net core 2 & ef core) with Unit Of Work and Generic repository pattern. I used to have one database context but due to some business logic I had to create a second database with some same entities.

For the sake of simplicity, I will use only one Entity, the ProductEntity and I will use only 1 repository method the Get by Id.

In the business logic I must be able to retrieve the Product from the two contexts, do some stuff and then update the contexts, but with a clean UoW design.

The repository is implemented like this

public interface IRepository<TEntity>
    where TEntity : class, new()
{      
    TEntity Get(int id);     
}

public interface IProductsRepository : IRepository<ProductEntity>
{

}

public class ProductsRepository : Repository<ProductEntity>, IProductsRepository
{
    public ProductsRepository(DbContext context) : base(context)
    {

    }
}

Implementation of UOW with one db context

public interface IUnitOfWork : IDisposable
{
    IProductsRepository ProductsRepository { get; }
    int Complete();
}

public class UnitOfWork : IUnitOfWork
{
    private readonly DbContext _context;

    public UnitOfWork(MainContext context)
    {
        // injecting the main database
        _context = context;
    }

    private IProductsRepository _productsRepository;
    public IProductsRepository ProductsRepository => _productsRepository ?? (_productsRepository = new ProductsRepository(_context));

    public int Complete()
    {
        return _context.SaveChanges();
    }

    public void Dispose()
    {
        _context?.Dispose();
    }
}

I am using the default framework of .NET Core for DI, so at my Startup.cs file I have the following

// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
    // ...

    // main database 
    services.AddDbContext<MainContext>(options => options.UseSqlServer(Configuration.GetConnectionString("MainDatabaseConnection"), providerOptions => providerOptions.CommandTimeout(30)));

    // unit of work            
    services.AddTransient<IUnitOfWork, UnitOfWork>();
}

To solve the problem I have created a second UnitOfWork with hardcoded context and I am using the same entities/repositories

My implementation with two db contexts

public interface IUnitOfWorkSecondary : IDisposable
{
    IProductsRepository ProductsRepository { get; }
    int Complete();
}

public class UnitOfWorkSecondary : IUnitOfWorkSecondary
{
    private readonly DbContext _context;

    public UnitOfWork(SecondaryDatabaseContext context)
    {
        // injecting the secondary database
        _context = context;
    }

    // same as above
}

So in a business object I am doing the following

public class Program
{
    private readonly IUnitOfWork _unitOfWork;
    private readonly IUnitOfWorkSecondary _unitOfWorkSecondary;

    public Program(IUnitOfWork unitOfWork, IUnitOfWorkSecondary unitOfWorkSecondary){
        _unitOfWork = unitOfWork;
        _unitOfWorkSecondary = unitOfWorkSecondary;
    }

    public static void Method1(int productId)
    {        
        var mainProduct = _unitOfWork.ProductsRepository.Get(productId);
        var secondaryProduct = _unitOfWorkSecondary.ProductsRepository.Get(productId);

        mainProduct.Name = "Hello Main";
        secondaryProduct.Name = "Hello Secondary";

        _unitOfWork.Complete();
        _unitOfWorkSecondary.Complete();
    }    
}

The Startup.cs is modified to

// main database 
services.AddDbContext<MainContext>(options => options.UseSqlServer(Configuration.GetConnectionString("MainDatabaseConnection"), providerOptions => providerOptions.CommandTimeout(30)));

// secondary database
services.AddDbContext<SecondaryContext>(options => options.UseSqlServer(Configuration.GetConnectionString("SecondaryDatabaseConnectiont"), providerOptions => providerOptions.CommandTimeout(30)));

// unit of work            
services.AddTransient<IUnitOfWork, UnitOfWork>();
services.AddTransient<IUnitOfWorkSecondary , UnitOfWorkSecondary>();

My questions

  1. What is the best (or most practical) design for this
  2. What if the databases are more than 2 ?

Best Answer


Personally, what I would do in a scenario such as this, would be to implement a cache layer in the middle of your UoW and DbContexts. This would provide several succinct benefits from an architectural point of view as follows:

  1. Quick Access for Visited Data - Any values which have been accessed already would be cached, as is the definition of the word!
  2. Layer of Abstraction for any DbContexts - With this method, you would essentially be creating a layer of abstraction between the UoW and DbContexts by removing the direct dependency on the contexts, and instead placing the dependency on the cache layer.
  3. Easy Extension Point - You will also gain the ability to easily plugin any additional DbContexts which are created by simply adding them to the cache layer as a data source.

Pseudo-Example

This interface would define a method by which a context is added to the Cache, and an additional method which you would call from within the consuming business logic.

public interface IDbContextCache<TContext> where TContext : new(), DbContext
{
    void AddContext<TContext>(TContext dbContext);
    TItem GetProduct<TItem>(int productId);
}

Internally you would want the implementation of the interface to utilize a data structure from the System.Collections.Concurrent namespace for storing the values when they are retreived by the GetProduct method.

Additionally I would implement private methods within the Cache implementation class, for defining the logic by which the DbContext instances are queried. Personally I would utilize the ThreadPool Class when accessing each of the registered contexts. This will allow you to ensure that the operation are non-blocking for any UI thread that may or may not be present in your scenario, but in my opinion it is never a bad idea to ensure your code is non-blocking where not required by business requirements!

Finally you would just modify your startup class to register the new IDbContextCache type as a singleton, and modify your methods which utilize the contexts to instead have the cache instance injected into their constructors or methods.