TransWikia.com

Unit Of Work with multiple database context

Software Engineering Asked on October 29, 2021

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 ?

2 Answers

It is unclear what your motive is for using two contexts, and if/how you expect these to behave in conjunction with one another. The solutions are fairly straightforward, but which solution would apply to your case is not clear. So I've tried to address your possible concerns here.

  1. When the update to main context fails, should the update to the secondary context automatically fail? And vice versa?

If that is the case, then you need these contexts to work under a shared transaction scope. In the comments, Ewan already provided you with a link that showcases how to do this.

  1. Do the entities that are in both contexts always have the same structure? If one changes, will the other need to be changed the same way?

Because if that is not the case, then you should not be reusing the same entity classes for these two contexts. Just because two things are currently the same doesn't mean that they invariably always will be.

And when they won't, then they should be separated from the get go. You're really going to regret having to separate them after you've developed your codebase.

  1. Are both contexts assumed to be up to date with one another?

In your example, do you need to actively account for the possibility of finding an entity in one context and not finding it in the other? Because your code currently blindly assumes that you're going to find both. Unless you have a very good (and so far unmentioned) reason to rely on that, this seems like a bad approach.

  1. Do both contexts have the exact same entity tables? Will this always be the case?

If you've answered "yes" to both questions 2 and 4, then you can abstract a reusable interface that both contexts implement, which I'll call IBaseContext for now.

public interface IBaseContext
{
    DbSet<Product> Products { get; set; }

    int SaveChanges();
}

Note: this interface can contain SaveChanges, which will "happen" to match the method from DbContext. The compiler allows this, as long as all implementations of IBaseContext have such a int SaveChanges() available. Regardless of whether they inherit it from DbContext or have defined it for themselves.

This opens the door to reusably using your db contexts. This is very relevant for your question:

What if the databases are more than 2 ?

When the contexts have a reusable interface, that means you can write reusable logic that can handle any context that implements this reusable interface.

This means you could, for example, opt for a generic unit of work:

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

public class UnitOfWork<TContext> : IUnitOfWork where TContext : IBaseContext
{
    public UnitOfWork(TContext context, IProductsRepository productsRepository)
    {
        _context = context;

        ProductsRepository = productsRepository;
        ProductsRepository.Context = context;
    }
 
    private TContext _context;

    public IProductsRepository ProductsRepository { get; private set; }

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

This unit of work will work for any implementation of IBaseContext, provided that the context has been registered in your service provider.

Note that this slightly complicates the repository DI logic. You can't rely on DI by itself to figure out which context you want to inject when. But you can change your repositories to allow their context being set after the fact by their unit of work.

There are other ways of doing the same thing, that may be cleaner in your opinion, but it will get incrementally more complicated depending on how clean you want it. I just used the easiest to read example here.

If you cannot create a reusable interface on your context types, then what you're currently doing is what you're going to have to do. When you can't do it reusably, you have to write each case manually.

Answered by Flater on October 29, 2021


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.

Answered by Flipurbit on October 29, 2021

Add your own answers!

Ask a Question

Get help from others!

© 2024 TransWikia.com. All rights reserved. Sites we Love: PCI Database, UKBizDB, Menu Kuliner, Sharing RPP