c# – Unit Of Work with multiple database context

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.