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
- What is the best (or most practical) design for this
- 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:
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.
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.