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. |
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 :
É
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: