0
\$\begingroup\$

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.

\$\endgroup\$
1
  • \$\begingroup\$ I updated my answer with some example code \$\endgroup\$
    – Anders
    Dec 6 '21 at 19:19
1
\$\begingroup\$

In most business applications a unit of work spans over several entity types. In systems that i design i separate unit of work and repositories entirely. When the unit of work is done using DI i call savechanges on the db context and commit transaction.

The unit of work is created using a scoped DbContext and this way i make sure all repositories are working against the same DbContext.

Also exposing the service provider like you do is dangerously close to service locator pattern. Which is a antipattern. Its ok close to the metal. But should be avoided from domain classes. Such as unit of work

Here is an example on a unit of work from my system that I have designed. It operatates on two entity types Invoices and payments. And mutates both behind the scene

public class MapPaymentsWithInvoicesCommandHandler : ICommandHandler<MapPaymentsWithInvoicesCommand>
{
    private readonly IPaymentRepository _paymentRepository;
    private readonly IInvoiceRepository _invoiceRepository;
    private readonly IPayment _payment;

    public MapPaymentsWithInvoicesCommandHandler(
        IPaymentRepository paymentRepository,
        IInvoiceRepository invoiceRepository,
        IPayment payment)
    {
        _paymentRepository = paymentRepository;
        _invoiceRepository = invoiceRepository;
        _payment = payment;
    }

    public async Task Handle(MapPaymentsWithInvoicesCommand command)
    {
        var invoices = await _invoiceRepository.ListInvoicesForMatchJob(command.AccountId, command.AccountId);
        var payments = await _paymentRepository.ListUnmatchedPaymentsByAsync(command.AccountId, (PaymentType)command.PaymentType);
        
        await _payment.MatchInvoicesWithPaymentsAsync(invoices, payments);
    }
}

This is the close to the metal logic that executes the unit of work.

    private async Task ExecuteCommand(Func<ExecutionContext, Task<Command>> commandFactory, IServiceProvider serviceProvider, int? id =null)
    {
        var ctx = serviceProvider.GetRequiredService<IBusinessContextController>(); //IBusinessContextController / (Used as IBusinessContext from domain) abstracts and wraps a DbContext
        var repository = serviceProvider.GetRequiredService<ICommandRepository>();
        var queuedCommand = id.HasValue;
        Command cmd = null;

        try
        {
            var cmdCtx = ctx.ExecutionContext = new ExecutionContext { Id = id, BatchId = Guid.NewGuid() };
            cmd = await commandFactory(cmdCtx);

            await ExecuteCommandInternalAsync(cmd, serviceProvider); //Here we execute domain logic


            /*Here we start transaction after unit of work ends.
            This is because unit of work does not mutate any data. 
            This way we can have short transaction with minimal locks.                
            This however requires that you use EF concurennecy tokens.
            You can also start transaction early from unit of work if needed*/
            await ctx.StartTransactionAsync(); 


            if (queuedCommand)
                await repository.UpdateStateAsync(CommandState.Completed);
            
            await ctx.SaveChangesAsync(); //Here we save as part of the unit of work
            await ctx.CommitTransactionAsync(); //Make sure we keep everything in sync with a transaction
        }
        catch (Exception e)
        {
            serviceProvider.GetRequiredService<ILogger<CommandRunner>>().LogError(e, $"Error executing command {cmd?.GetType().FullName} ({id})");
            ctx.RollbackTransaction();
            if (queuedCommand)
            {
                await repository.UpdateStateAsync(CommandState.Error);
                await ctx.SaveChangesAsync();
            }
            else
                throw;
        }

    }

This way I keep the service locator out of the domain and only close to the metal.

\$\endgroup\$
4
  • \$\begingroup\$ Upon reading on it, I see your point on the Service Locator Pattern. But what is UoW, if not a more intelligent SL? Also, you are using a DDD approach, which I do not. \$\endgroup\$
    – rTECH
    Dec 8 '21 at 22:03
  • \$\begingroup\$ I wouldn't call it a service locator. It's more a domain specific strategy. But as you can see my uow does not contain any service providers, i instead use the contractor to inject my dependencies. I think this is a better aprouch for domain code. Keep service providers close to the metal and away from the domain. \$\endgroup\$
    – Anders
    Dec 8 '21 at 23:04
  • \$\begingroup\$ I understand that your way of doing it promotes a looser coupling between the components than what I have here. By "close to the metal" you mean away from the your domain classes or closer to the db context from an architectural point of view? I still cannot see the difference between a standard SL and your service provider. Could you give an example implementation to that as well so that I can understand it better? \$\endgroup\$
    – rTECH
    Dec 12 '21 at 17:52
  • 1
    \$\begingroup\$ Close to the metal is the bolierplate code in my case the ExecuteCommand code. This is the only place i directly reference the service provider. From the domain i use standard constructor injection. \$\endgroup\$
    – Anders
    Dec 12 '21 at 20:18

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.