2

I'm implementing an ASP.NET MVC application and need to implement the Unit Of Work with repositories pattern. My implementation is designed as follows:

  • The UnitOfWork object is in charge of issuing COMMITs and ROLLBACKs as necessary.
  • The UnitOfWork object contains a Transaction property, obtaied from the internal DB connection. This object provides atomicity to the operations inside the UnitOfWork.
  • The UnitOfWork object contains the repositories as properties injected at runtime.
  • Each repository needs to be provided the IDbTransaction object that UnitOfWork created to support atomicity.

So now I find myself in the strange situation of, in UnitOfWork, having to inject repositories that need a property of UnitOfWork itself in order to be instantiated. So my question is: How do I do this? Or maybe something in the design has to be changed?

I'm using SQL Server at the moment and I use Dapper for the SQL calls. Also, I'm thinking of using Autofac as DI framework.

What I've done so far is to implement UOW and a sample repository. Code as follows.

IRepository.cs:

public interface IRepository<TObj, TKey>
{
    Task<TObj> DetallesAsync(TKey id);
    Task<TKey> AgregarAsync(TObj obj);
}

DbRepository.cs:

public abstract class DbRepository
{
    private readonly IDbConnection _connection;
    private readonly IDbTransaction _transaction;

    protected IDbConnection Connection
    {
        get => _connection;
    }

    protected IDbTransaction Transaction
    {
        get => _transaction;
    }

    public DbRepository(IDbTransaction transaction)
    {
        _transaction = transaction;
        _connection = _transaction.Connection;
    }
}

RolRepository.cs:

public class MSSQLRolRepository : DbRepository, IRolRepository
{
    public MSSQLRolRepository(IDbTransaction transaction)
        : base(transaction)
    {

    }

    public async Task<int> AgregarAsync(Rol obj)
    {
        var result = await Connection.ExecuteScalarAsync<int>(MSSQLQueries.RolAgregar, param: obj, transaction: Transaction);
        return result;
    }

    public async Task<Rol> DetallesAsync(int id)
    {
        var param = new { Id = id };
        var result = await Connection.QuerySingleOrDefaultAsync<Rol>(MSSQLQueries.RolDetalles, param: param, transaction: Transaction);
        return result;
    }

    public async Task<Rol> DetallesPorNombreAsync(string nombre)
    {
        var param = new { Nombre = nombre };
        var result = await Connection.QuerySingleOrDefaultAsync<Rol>(MSSQLQueries.RolDetallesPorNombre, param: param, transaction: Transaction);
        return result;
    }

    public async Task<Rol[]> ListarAsync(int pagina, int itemsPorPagina)
    {
        var param = new { Pagina = pagina, ItemsPorPagina = itemsPorPagina };
        var result = await Connection.QueryAsync<Rol>(MSSQLQueries.RolListar, param: param, transaction: Transaction);
        return result.ToArray();
    }

    public async Task<Rol[]> ListarTodosAsync()
    {
        var result = await Connection.QueryAsync<Rol>(MSSQLQueries.RolListar, transaction: Transaction);
        return result.ToArray();
    }
}

IUnitOfWork.cs:

public interface IUnitOfWork : IDisposable
{
    IDbTransaction Transaction { get; }
    IDenunciaRepository DenunciaRepository { get; }
    IUsuarioRepository UsuarioRepository { get; }
    IRolRepository RolRepository { get; }
    void Commit();
    void Rollback();
}

MSSQLUnitOfWork.cs:

public class MSSQLUnitOfWork : IUnitOfWork
{
    private bool _already_disposed = false;
    private IDbConnection _connection;
    private IDbTransaction _transaction;
    private IDenunciaRepository _denuncia_repository;
    private IUsuarioRepository _usuario_repository;
    private IRolRepository _rol_repository;

    public IDbTransaction Transaction
    {
        get => _transaction;
    }

    public IDenunciaRepository DenunciaRepository
    {
        get => _denuncia_repository;
    }

    public IUsuarioRepository UsuarioRepository
    {
        get => _usuario_repository;
    }

    public IRolRepository RolRepository
    {
        get => _rol_repository;
    }

    public MSSQLUnitOfWork()
    {
        var connection_string = ConfigurationManager.ConnectionStrings["MSSQL"].ConnectionString;
        _connection = new SqlConnection(connection_string);
        _connection.Open();
        _transaction = _connection.BeginTransaction();
        //TODO: Crear repos con transacción
    }

    public void Commit()
    {
        _transaction.Commit();
    }

    public void Rollback()
    {
        _transaction.Rollback();
    }

    protected virtual void Dispose(bool disposeManagedObjects)
    {
        if (!_already_disposed)
        {
            if (disposeManagedObjects)
            {
                _transaction?.Dispose();
                _connection?.Dispose();
            }
            _already_disposed = true;
        }
    }

    public void Dispose()
    {
        Dispose(true);
    }
}
Amit Joshi
  • 15,448
  • 21
  • 77
  • 141
Léster
  • 1,177
  • 1
  • 17
  • 39
  • 1
    You have an issue with IoC where `MSSQLUnitOfWork` depends on implementation details of `IDbConnection` and `IDbTransaction`. It shouldn't. – maxbeaudoin Mar 12 '20 at 20:21

2 Answers2

1

I recommend you 3 different things.

  1. Start, Commit and Rollback your data transactions within the repository where you are instantiatign the UnitOfWork - the least I recommend

  2. Create a Service class where you can create an instance of UnitOfWork and pass it the instance or DBContext to the Repositories that you involve in the transactions

  3. Create Repository instance within the UnitOfWork class tha knows the current DBContext then you can access from UnitOfWork the repository operations and starts and ends the transactions in the same context. More recommended

Something like:

UnitOfWorkInstance.MyRepositoryA.AddAsync(...);
UnitOfWorkInstance.MyRepositoryB.AddAsync(...);
UnitOfWorkInstance.Commit();
svladimirrc
  • 216
  • 1
  • 6
  • 2
    In the end, I changed the code to not require injecting repos in the UoW instance (instead, the UoW is in charge of instancing them) and it works properly. Thank you. – Léster Mar 27 '20 at 15:34
  • @Léster you came up with the same pattern we use since ages. I'm not completely happy with it (UoW creating instances of all repos), but it works well. – stmax May 27 '21 at 08:42
0

I find myself in the strange situation of, in UnitOfWork, having to inject repositories that need a property of UnitOfWork itself in order to be instantiated.

It looks like you have a circular dependency and circular dependency is always a bad design and you should break it. If you followed Single Responsibility Principle it should not happens. If it happens the service may have too many responsibilities and you should break it in multiple services or sometime it is because you have too small service and these services should be reunited.

In your case it looks like IUnitOfWork has too many responsibility. What's the goal of this service? what are its responsibilities?

For me, this service should not have any repositories, this service doesn't need any. If any other service needs such a repository, they just have to add a dependency on it. Also the repository don't need to have a dependency on IUnitOfWork but only on IDbTransaction. Furthermore IDbTransaction and IDbConnection should be configured in your dependency injector. If these service need to be instanciated by IUnitOfWork for any reason you can do something like

builder.RegisterType<MSSQLUnitOfWork>()
       .As<IUnitOfWork>()
       .InstancePerLifetimeScope()
builder.Register(c => c.Resolve<IUnitOfWork>().Transation)
       .As<IDbTransaction>();
builder.Register(c => c.Resolve<IUnitOfWork>().Connection)
       .As<IDbConnection>();
Léster
  • 1,177
  • 1
  • 17
  • 39
Cyril Durand
  • 15,834
  • 5
  • 54
  • 62