ASP.NET Core  -  Implementando a Unit Of Work : considerações


  Neste artigo vamos apresentar algumas abordagens que são usadas na implementação do padrão Unit Of Work.

Vamos explorar neste artigo o padrão Unit Of Work mostrando e algumas abordagens usadas para implementar este padrão.

Uma palavra sobre Repositórios

Com o advento dos microsserviços atualmente é comum que uma aplicação tenha vários armazenamentos de dados diferentes de fornecedores diferentes ou até mesmo Apis de aplicativos diferentes. Por exemplo, você pode acessar dados de um banco de dados Oracle HR, dados de contabilidade podem estar em um servidor SQL, dados de clientes podem estar em CRM baseado em MySQL e dados de produtos podem ser armazenados em um banco de dados de gerenciamento de inventário hospedado na nuvem.

Seu aplicativo pode ser solicitado a realizar modificações em todos esses sistemas durante o curso de uma transação. Na maioria dos casos, você também pode ter várias estratégias diferentes de acesso a dados, como qualquer número de Object Relational Mappers (ORM) diferentes para interagir com os vários bancos de dados.

O desafio para os desenvolvedores nesses tipos de cenários é que muitas vezes eles não querem expor a lógica de acesso a dados diferentes para sua camada de interface do usuário, ou seja, controlador ou mesmo camadas de lógica de negócios, muitas vezes com o objetivo de abstraí-lo para reduzir dependências diretas.

É neste cenário que o Padrão Repositório se torna extremamente útil.

O padrão de repositório é discutido extensivamente nos livros: Patterns of Enterprise Application Architecture e Domain Driven Design.

O que é o padrão Repositório ?

Este padrão é usado para separar a lógica que recupera os dados e os mapeia para um modelo de entidade da lógica de negócios que atua no modelo. Isso permite que a lógica de negócios seja independente do tipo de dados que compõem a camada de fonte de dados.

O repositório atua como um mediador entre a camada de fonte de dados e as camadas de negócios do aplicativo. Ele consulta a fonte de dados para os dados, mapeia os dados da fonte de dados para uma entidade de negócios e mantém as alterações na entidade de negócio para a fonte de dados.

Benefícios do Padrão de Repositório :

  • Centralização da lógica de acesso aos dados.
  • Ponto de substituição para os testes de unidade.
  • Arquitetura flexível que pode ser adaptada à medida que o design geral do aplicativo evolui

É importante dividir o desenvolvimento de seu aplicativo em camadas. Cada camada pode então ser injetada.

Isso fornece níveis de abstrações para suas várias camadas, pois elas não necessariamente se importam onde os dados de cada camada são persistidos e recuperados apenas de acordo com um contrato de dados explícito.

Unit of Work

Ao implementar um padrão Repository, também é importante entender o padrão Unit of Work.

Martin Fowler fornece a seguinte explicação para o padrão Unit Of Work:

"Uma Unit Of Work acompanha tudo o que você faz durante uma transação de negócios que pode afetar o banco de dados. Ao terminar, ele calcula tudo o que precisa ser feito para alterar o banco de dados como resultado do seu trabalho."

O padrão Unit of Work é um padrão de design que nos ajuda a gerenciar as transações do banco de dados de maneira mais eficiente.  Ela representa uma transação quando usada em camadas de dados one normalmente, a unidade de trabalho reverterá a transação se a operação usada para salvar os dados, o Commit(), não tiver sido invocado antes de ser descartado.

A seguir vou apresentar cenários onde veremos como implementar o padrão Unit Of Work.

Unit Of Work - Com repositórios específicos

O primeiro cenário  usa a abordagem de criar repositórios específicos e para simplificar vamos considerar que estamos tratando com repositórios para Cliente e Produto.

Nesta abordagem criam-se as interfaces IClienteRepository e IProdutoRepository e suas implementações ClienteRepository e ProdutoRepository onde geralmente definimos os métodos para incluir, atualizar e excluir dados.

A implementação do padrão Unit Of Work neste contexto cria a interface IUnitOfWork:

public interface IUnitOfWork
{
   IRepositorio<Cliente> ClienteRepositorio { get; }
   IRepositorio<Produto> ProdutoRepositorio { get; }
   void Commit();
}

E a classe concreta UnitOfWork que implementa a interface :

public class UnitOfWork : IUnitOfWork, IDisposable
{
  private AppContexto _contexto = null;
  private Repositorio<Cliente> clienteRepositorio = null;
  private Repositorio<Produto> produtoRepositorio = null;
     public UnitOfWork()
     {
           _contexto = new AppContexto();
     }
     public void Commit()
     {
          _contexto.SaveChanges();
      }
     public IRepositorio<Cliente> ClienteRepositorio
     {
        get
        {
          if (clienteRepositorio == null)
            {
                    clienteRepositorio = new Repositorio<Cliente>(_contexto);
                }
                return clienteRepositorio;
            }            
        }
        public IRepositorio<Produto> ProdutoRepositorio
        {
            get
            {
                if (produtoRepositorio == null)
                {
                    produtoRepositorio = new Repositorio<Produto>(_contexto);
                }
                return produtoRepositorio;
            }
        }
       private bool disposed = false;
        protected virtual void Dispose(bool disposing)
        {
            if (!this.disposed)
            {
                if (disposing)
                {
                    _contexto.Dispose();
                }
            }
            this.disposed = true;
        }
        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }
 }

Acontece que esta implementação esta realizando um forte acoplamento ao instanciar os repositorios especificos ao usar a palavra-chave new.

A princípio para um projeto pequeno pode até ser tolerado mas precisamos melhorar essa implementação.

Melhorando a implementação do Unit Of Work

Uma opção para evitar o acoplamento seria usar a injeção de dependência do ASP.NET Core para injetar apenas os repositórios que você precisa em vez de instanciá-los dentro da classe UnitOfWork. Para isso, você pode registrar seus repositórios no contêiner de injeção de dependência do ASP.NET Core na classe Program (ou no método ConfigureServices do arquivo Startup.cs)


    // ...
    builder.Services.AddScoped<IRepositorio<Cliente>, Repositorio<Cliente>>();
    builder.Services.AddScoped<IRepositorio<Produto>, Repositorio<Produto>>();
    builder.Services.AddScoped<IUnitOfWork, UnitOfWork>();
    // ...
 

Isso registra as implementações dos repositórios e a interface IUnitOfWork no contêiner de injeção de dependência. Observe registramos as implementações dos repositórios usando o método AddScoped. Isso significa que o contêiner de injeção de dependência criará uma nova instância dessas classes sempre que for solicitada em um novo request. Essa é a opção mais adequada para a maioria dos casos de uso em aplicativos ASP.NET Core.

Agora, dentro da classe UnitOfWork, você pode usar a injeção de dependência para obter as instâncias necessárias dos repositórios.

Veja como ficaria a implementação atualizada da classe:

public class UnitOfWork : IUnitOfWork, IDisposable
{
    private readonly AppContexto _contexto;
    private readonly IRepositorio<Cliente> _clienteRepositorio;
    private readonly IRepositorio<Produto> _produtoRepositorio;
    public UnitOfWork(AppContexto contexto, 
                      IRepositorio<Cliente> clienteRepositorio, 
                      IRepositorio<Produto> produtoRepositorio)
    {
        _contexto = contexto;
        _clienteRepositorio = clienteRepositorio;
        _produtoRepositorio = produtoRepositorio;
    }
    public void Commit()
    {
        _contexto.SaveChanges();
    }
    public IRepositorio<Cliente> ClienteRepositorio => _clienteRepositorio;
    public IRepositorio<Produto> ProdutoRepositorio => _produtoRepositorio;
    private bool disposed = false;
    protected virtual void Dispose(bool disposing)
    {
        if (!this.disposed)
        {
            if (disposing)
            {
                _contexto.Dispose();
            }
        }
        this.disposed = true;
    }
    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }
}

Observe que agora a classe UnitOfWork recebe as instâncias dos repositórios por meio do construtor. Essas instâncias são obtidas por meio da injeção de dependência e não há necessidade de instanciá-las dentro da classe UnitOfWork.

Além disso, agora as propriedades ClienteRepositorio e ProdutoRepositorio apenas retornam as instâncias dos repositórios que foram injetadas no construtor.

Dessa forma, sempre que a classe IUnitOfWork for solicitada, o ASP.NET Core irá instanciar uma nova instância da classe UnitOfWork, passando as instâncias de AppContexto, IRepositorio<Cliente> e IRepositorio<Produto> como parâmetros para o construtor. Isso remove o forte acoplamento que a classe UnitOfWork tinha com as classes de repositório.

Lembre-se de que a interface IRepositorio<T> também deve ser registrada no container de injeção de dependência do ASP.NET Core, como mostrado acima.

Esta interface poderia ser definida da seguinte forma:

public interface IRepository<T> where T : class
{
    Task<T> Get(int id);
    Task<IEnumerable<T>> GetAll();
    Task Add(T entity);
    void Delete(T entity);
    void Update(T entity);
}

E a classe concreta Repository<T> poderia ser implementada usando o código abaixo:

ublic class Repository<T> : IRepository<T> where T : class 
{
    protected readonly AppDbContext _context;
    public Repository(AppDbContext context)
    {
        _context = context;
    }
    public async Task<T> Get(int id)
    {
        return await _context.Set<T>().FindAsync(id);
    }
    public async Task<IEnumerable<T>> GetAll()
    {
        return await _context.Set<T>().ToListAsync();
    }
    public async Task Add(T entity)
    {
        await _context.Set<T>().AddAsync(entity);
    }
    public void Delete(T entity)
    {
        _context.Set<T>().Remove(entity);
    }
    public void Update(T entity)
    {
        _context.Set<T>().Update(entity);
    }
}   

O problema com esta implementação é que embora sempre que a classe IUnitOfWork for solicitada, o ASP.NET Core irá instanciar uma nova instância da classe UnitOfWork, passando as instâncias de AppContexto, IRepositorio<Cliente> e IRepositorio<Produto> como parâmetros para o construtor

Se eu tiver muitos repositórios todos eles serão injetados e talvez eu não queira usar todos mas apenas alguns
e assim não quero ficar instanciando um monte de instancias de repositórios que não serão usados na minha aplicação.

Uma abordagem para evitar que muitas instâncias de repositórios sejam criadas e injetadas desnecessariamente seria  usar o padrão de design de fábrica (Factory) para criar as instâncias de repositórios somente quando elas são necessárias.

Usando o padrão Factory

Nesta implementação podemos criar uma interface de fábrica para cada tipo de repositório:

public interface IClienteRepositorioFactory
{
    IRepositorio<Cliente> Create(AppContexto contexto);
}
public interface IProdutoRepositorioFactory
{
    IRepositorio<Produto> Create(AppContexto contexto);
}

Em seguida, podemos  criar as implementações dessas interfaces, que criam as instâncias de repositórios, por exemplo:

public class ClienteRepositorioFactory : IClienteRepositorioFactory
{
   public IRepositorio<Cliente> Create(AppContexto contexto)
   {
     return new Repositorio<Cliente>(contexto);
   }
}

public class ProdutoRepositorioFactory : IProdutoRepositorioFactory
{
   public IRepositorio<Produto> Create(AppContexto contexto)
   {
     return new Repositorio<Produto>(contexto);
   }
}

Em seguida, podemos alterar a classe UnitOfWork para receber as instâncias das fábricas de repositórios por meio do construtor, em vez das instâncias de repositórios diretamente:

public class UnitOfWork : IUnitOfWork, IDisposable
{
    private readonly AppContexto _contexto;
    private readonly IClienteRepositorioFactory _clienteRepositorioFactory;
    private readonly IProdutoRepositorioFactory _produtoRepositorioFactory;
    private IRepositorio<Cliente> _clienteRepositorio;
    private IRepositorio<Produto> _produtoRepositorio;
    public UnitOfWork(AppContexto contexto, IClienteRepositorioFactory clienteRepositorioFactory, 
IProdutoRepositorioFactory produtoRepositorioFactory)
    {
        _contexto = contexto;
        _clienteRepositorioFactory = clienteRepositorioFactory;
        _produtoRepositorioFactory = produtoRepositorioFactory;
    }
    public void Commit()
    {
        _contexto.SaveChanges();
    }
    public IRepositorio<Cliente> ClienteRepositorio => _clienteRepositorio ??= 
                                 _clienteRepositorioFactory.Create(_contexto);
    public IRepositorio<Produto> ProdutoRepositorio => _produtoRepositorio ??= 
                                 _produtoRepositorioFactory.Create(_contexto);
    private bool _disposed = false;
    protected virtual void Dispose(bool disposing)
    {
        if (!_disposed)
        {
            if (disposing)
            {
                _contexto.Dispose();
            }
        }
        _disposed = true;
    }
    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }
}

Observe que as propriedades ClienteRepositorio e ProdutoRepositorio agora verificam se as instâncias de repositórios já foram criadas (por meio do operador ??=) e, se não, elas chamam as fábricas de repositórios para criá-las. Dessa forma, as instâncias de repositórios são criadas somente quando são necessárias.

Finalmente, você pode registrar as dependências necessárias para as fábricas de repositórios no container de injeção de dependência da Asp.Net Core.

Usando a injeção de dependência a nosso favor

Para melhorar ainda mais nossa implementação, podemos utilizar a injeção de dependência (DI) para fornecer as instâncias necessárias para a classe UnitOfWork, em vez de criar as instâncias diretamente na classe.

Para fazer isso, podemos registrar nossas dependências no contêiner de DI do ASP.NET Core, no método na classe Program (ou no ConfigureServices do arquivo Startup.cs):

builder.Services.AddScoped<IUnitOfWork, UnitOfWork>();
builder.Services.AddScoped(typeof(IRepositorio<>), typeof(Repositorio<>));
builder.Services.AddScoped<AppContexto>();


O método AddScoped adiciona uma instância que dura o tempo de vida de uma solicitação HTTP e é compartilhada por todos os objetos naquela solicitação. Ele também garante que a mesma instância será usada para todos os serviços que compartilham o mesmo escopo.

Aqui estamos registrando o IUnitOfWork para a implementação da classe UnitOfWork, o IRepositorio para a implementação genérica da classe Repositorio e o AppContexto para sua própria classe.

Agora podemos alterar a classe UnitOfWork para aceitar as instâncias de IRepositorio e AppContexto por meio de seu construtor, em vez de criá-los internamente:

public class UnitOfWork : IUnitOfWork, IDisposable
{
    private readonly AppContexto _contexto;
    private readonly Dictionary<Type, object> _repositorios = new Dictionary<Type, object>();
    public UnitOfWork(AppContexto contexto, IServiceProvider serviceProvider)
    {
        _contexto = contexto;
        var tiposRepositorios = typeof(Repositorio<>).Assembly.GetTypes()
            .Where(t => !t.IsAbstract && t.IsClass && t.GetInterfaces()
                .Any(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IRepositorio<>)));
        foreach (var tipo in tiposRepositorios)
        {
            var tipoEntidade = tipo.GetInterfaces().Single(i => i.IsGenericType && i.GetGenericTypeDefinition()
                                                              == typeof(IRepositorio<>)).GetGenericArguments()[0];

            var instanciaRepositorio = serviceProvider.GetRequiredService(typeof(IRepositorio<>)
                                                             .MakeGenericType(tipoEntidade));

            _repositorios.Add(tipoEntidade, instanciaRepositorio);
        }
    }
    public IRepositorio<TEntity> Repositorio<TEntity>() where TEntity : class
    {
        if (_repositorios.ContainsKey(typeof(TEntity)))
            return (IRepositorio<TEntity>)_repositorios[typeof(TEntity)];
        var novoRepositorio = Activator.CreateInstance(typeof(Repositorio<>)
                                      .MakeGenericType(typeof(TEntity)), _contexto);
 
        _repositorios.Add(typeof(TEntity), novoRepositorio);
        return (IRepositorio<TEntity>)novoRepositorio;
    }
    public void Commit()
    {
        _contexto.SaveChanges();
    }
    private bool disposed = false;
    protected virtual void Dispose(bool disposing)
    {
        if (!this.disposed)
        {
            if (disposing)
            {
                _contexto.Dispose();
            }
        }
        this.disposed = true;
    }
    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }
}

Agora, em vez de ter um campo para cada tipo de repositório, temos um dicionário _repositorios que armazena instâncias de IRepositorio para cada tipo de entidade.

Ao injetar IServiceProvider em nosso construtor, podemos obter instâncias de IRepositorio para todas as entidades registradas no contêiner DI e isso é feito por meio do recurso Reflection.

Vimos algumas abordagens na implementação do padrão Unit Of Work que podemos considerar dependendo do cneário e dos requisitos do nosso projeto.

E estamos conversados...

"Bendito seja o Deus e Pai de nosso Senhor Jesus Cristo que, segundo a sua grande misericórdia, nos gerou de novo para uma viva esperança, pela ressurreição de Jesus Cristo dentre os mortos"
1 Pedro 1:3

Referências:


José Carlos Macoratti