Skip to content

GuilhermeBley/CatalogoCleanArchitecture

Repository files navigation

Catalogo

CLEAN ARCHITECTURE

  • ASP NET
  • Solution with clean architecture
  • Use Dapper
  • Unit Of Work

Unit Of Work With Dapper

The objective is make a Unit Of Work, which can execute a SaveChanges from various Repositories, persistency the data.

With the Dependency Injection is managed a shared connection on the repositories. In Catalogo.CrossCutting.IoC we could see this:

services
  .AddSingleton<IConfiguration>(configuration)
  .AddTransient<IConnectionFactory, ConnectionFactory>()
  .AddScoped<ICategoriaRepository, CategoriaRepository>()
  .AddScoped<IProdutoRepository, ProdutoRepository>()
  .AddScoped<IProdutoService, ProdutoService>()
  .AddScoped<ICategoriaService, CategoriaService>()
  
  // UoW
  .AddScoped<UnitOfWorkRepository>()
  .AddScoped<IUnitOfWorkRepository>(x => x.GetRequiredService<UnitOfWorkRepository>())
  .AddScoped<IUnitOfWork>(x => x.GetRequiredService<UnitOfWorkRepository>());

The persistence of data execution is make in Catalogo.Application.UoW.IUnitOfWork, which have delivered to Services. The connection to access the data was inserted in the interface Catalogo.Infrastructure.Context.UnitOfWork.Repository, which is shared to Repositories.

namespace Catalogo.Infrastructure.Context
{
  /// <summary>
  /// Give a shared connection to repositorys
  /// </summary>
  public interface IUnitOfWorkRepository : IUnitOfWork
  {
      /// <summary>
      /// Avaliable connection
      /// </summary>
      IDbConnection Connection { get; }

      /// <summary>
      /// Avaliable Transaction
      /// </summary>
      IDbTransaction Transaction { get; }
  }
}

namespace Catalogo.Application.UoW
{
  /// <summary>
  /// Unit of work give a shared transactions to repositorys
  /// </summary>
  public interface IUnitOfWork : IDisposable
  {
      /// <summary>
      /// Identifier of unit
      /// </summary>
      Guid Identifier { get; }

      /// <summary>
      /// Necessary to create and open a new connection
      /// </summary>
      /// <returns>async result of <see cref="IUnitOfWork"/> opened</returns>
      Task<IUnitOfWork> OpenConnectionAsync();

      /// <summary>
      /// Creates a connection (if method <see cref="OpenConnectionAsync"/> haven't executed) and transaction
      /// </summary>
      /// <remarks>
      ///     <para>Starts a transaction to the repositorys</para>
      ///     <para>Use <see cref="SaveChangesAsync"/>> after execute</para>
      /// </remarks>
      /// <returns>async result of <see cref="IUnitOfWork"/> opened</returns>
      Task<IUnitOfWork> BeginTransactionAsync();

      /// <summary>
      /// Commits if is ok or roll back if throw a exception
      /// </summary>
      /// <returns>async</returns>
      Task SaveChangesAsync();
  }
}

And a unique class implements a both (namespace Catalogo.Infrastructure.Context).

/// <summary>
/// Manage connections and transactions
/// </summary>
public class UnitOfWorkRepository : IUnitOfWorkRepository
{
    private DbConnection _connection { get; set; }
    public DbTransaction _transaction { get; set; }

    public IDbConnection Connection => _connection ?? throw new DataException("Connection is closed.");

    public IDbTransaction Transaction => _transaction;

    public Guid Identifier { get; } = Guid.NewGuid();

    private readonly IConnectionFactory _connectionFactory;

    public UnitOfWorkRepository(IConnectionFactory connectionFactory)
    {
        _connectionFactory = connectionFactory;
    }

    public async Task<IUnitOfWork> BeginTransactionAsync()
    {
        await OpenConnectionAsync();

        if (_transaction is null)
            _transaction = await  _connection.BeginTransactionAsync();

        return this;
    }

    public async Task SaveChangesAsync()
    {
        try
        {
            await _transaction?.CommitAsync();
        }
        catch
        {
            await _transaction?.RollbackAsync();
            throw;
        }
        finally
        {
            _transaction?.Dispose();
            _transaction = null;
        }
    }

    public void Dispose()
    {
        if (_transaction is not null)
        {
            _transaction.Dispose();
            _transaction = null;
        }

        if (_connection is not null)
        {
            _connection.Dispose();
            _connection = null;
        }
    }

    public async Task<IUnitOfWork> OpenConnectionAsync()
    {
        if (_connection is null)
        {
            _connection = _connectionFactory.CreateConn();
            await _connection.OpenAsync();
        }

        return this;
    }
}

To use this implementation two parts needed.

  • Repository

Needs a connection and transaction to execute the commands, and in all of the connections in DataBase should be use this way:

[...]
public class CategoriaRepository : RepositoryBase, ICategoriaRepository
{
      private readonly IUnitOfWorkRepository _uoW;

      /// <summary>
      /// Connection to execute commands and querys
      /// </summary>
      protected IDbConnection _connection => _uoW.Connection;

      /// <summary>
      /// Shared execution
      /// </summary>
      protected IDbTransaction _transaction => _uoW.Transaction;

      public RepositoryBase(IUnitOfWorkRepository uoW)
      {
          _uoW = uoW;
      }

      public async Task<int> CreateAsync(Produto product)
        {
            return
                await _connection.ExecuteAsync(
                    "INSERT INTO catalagodapper.produto (Nome, Descricao, Preco, ImagemUrl, Estoque, DataCadastro, IdCategoria) VALUES (@Nome, @Descricao, @Preco, @ImagemUrl, @Estoque, @DataCadastro, @IdCategoria);",
                    product,
                    _transaction
                );
        }
}
[...]
  • Services

The Services manage a open connection to Repositories, having two ways, the simple execution, without transaction, or with it.

public class CategoriaService : ICategoriaService
{
    private readonly IUnitOfWork _uow;
    private ICategoriaRepository _categoryRepository;
    private readonly IMapper _mapper;

    public CategoriaService(
        IUnitOfWork uow,
        ICategoriaRepository categoryRepository,
        IMapper mapper)
    {
        _uow = uow;
        _categoryRepository = categoryRepository;
        _mapper = mapper;
    }

    public async Task Add(CategoriaDTO categoryDto)
    {
        var categoryEntity = _mapper.Map<Categoria>(categoryDto);

        categoryEntity.Validate();

        using (await _uow.BeginTransactionAsync())
        {
            if ((await _categoryRepository.GetByName(categoryEntity.Nome)) is not null)
                throw new ConflictException($"Category with name {categoryEntity.Nome} already exists.");

            await _categoryRepository.CreateAsync(categoryEntity);
            await _uow.SaveChangesAsync();
        }
    }
}