I have seen a lot of Repository pattern implementations with Unit of Work. The simplest way of implementing this I came across is using hard-coded repos (such as this one), while the way recommended by Microsoft improves on this with a generic repo interface, but the repos themselves are still stored in the instance.
It is possible to improve on this by combining UoW with using a generated data access layer that uses a base repository implementation, something similar to what they implemented here). A problem with this approach is if you want to implement it inside the UoW, you will have to hard-code the base class implementation. In the examples, I use EFCore, but my question is not fixed on that.
So here is my BaseRepo, where IDbEntry
is just a simple interface requiring an Id:
public class RepoBase<TContext, TEntry> : IRepository<TEntry>
where TContext:DbContext
where TEntry:class, IDbEntry
{
private TContext context;
public RepoBase(TContext context)
{
this.context = context;
}
// skipping generic CRUD method implementations...
}
Here I used a UoW interface that can generate the generic repo on demand:
public interface IUnitOfWork
{
Task SaveChangesAsync(CancellationToken token = default);
IRepository<TEntity> GetRepo<TEntity>()
where TEntity : class, IDbEntry;
}
Here is the implementation, note that the base repo also implements IRepository<TEntry>
:
public class UnitOfWork<TContext> : IUnitOfWork
where TContext : DbContext
{
private readonly TContext context;
public UnitOfWork(TContext context)
{
this.context = context;
}
public async Task SaveChangesAsync(CancellationToken token = default)
{
// skipping lot of additional stuff here like audit...
await this.context.SaveChangesAsync(token)
.ConfigureAwait(false);
}
IRepository<TEntity> IUnitOfWork.GetRepo<TEntity>()
{
return new BaseRepo<TContext, TEntity>(this.context);
}
}
This is not efficient enough, because in case of any change to the base implementation, you have to modify the UoW and you can not unit test the UoW implementation either, since it will use a real base repo implementation. So, I was wondering if I can make this work using an injected base repository implementation. Here are my modifications:
First I added an interface for the base repo that extends the original IRepo with the ability to register a DataContext for the UoW after instantiation:
public interface IRepoBase<TContext, TEntry> : IRepository<TEntry>
where TContext : DbContext
where TEntry : class, IDbEntry
{
void RegisterSharedContext(TContext context);
}
Since I wanted to use this with the built-in .net core DI, I registered it:
services.AddScoped(typeof(IRepoBase<,>), typeof(RepoBase<,>));
Since I do not know which type will the UoW required to locate, I can only resolve the base repo inside the generic method call. To do this, I need to inject a IServiceProvider
instance into my UoW:
public UnitOfWork(TContext context, IServiceProvider services)
{
this.context = context;
this.services = services;
}
and use it to resolve the base repo:
IRepository<TEntity> IRepoManager.GetRepo<TEntity>()
{
var baseRepo = services.GetRequiredService<IRepoBase<TContext, TEntity>>();
baseRepo.RegisterSharedContext(this.context);
return baseRepo;
}
This is better because now I only need to change the DI registration to change the base repo implementation and theoretically I can unit test the UoW as well. But, to do that, I have to mock an IServiceProvider
(which is a practice I have never encountered) not to mention that mixing DI and service locator is highly discouraged in the Microsoft DI guidelines. I'm also not a fan of registering a context after instantiation as this is another necessary step that other developers simply have to remember for this to work.
I'm interested in the opinion of more experienced .Net developers: how can I approach this problem or is this something I should be concerned with in the first place?
As a side note, I know that EFCore implements UoW and implementing one yourself is only advised in somewhat special situations.