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
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.
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.
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.
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.
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:
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
Get help from others!
Recent Answers
Recent Questions
© 2024 TransWikia.com. All rights reserved. Sites we Love: PCI Database, UKBizDB, Menu Kuliner, Sharing RPP