ASP.NET
Core -
Repository e Unit Of Work - I
![]() |
Hoje vou apresentar novamente o padrão Repository e Unit Of Work usando a abordagem feita no Domain Driven Design. |
O DDD (Domain Driven Design) é uma abordagem ao design de software que se baseia no conceito de domínio e visa facilitar a criação de aplicativos complexos, conectando as partes relacionadas do software em um modelo focado no negócio que esta em constante evolução.
O DDD apresenta diversos conceitos e práticas que são usadas para criar softwares complexos de forma robusta e sustentável. Assim o DDD tem os seus fundamentos baseados em:
O objetivo é reduzir a complexidade aplicando o design orientado a objetos de forma a ter um código claro e conciso, essa seria a melhor “documentação” que expressa o design do produto segundo o DDD.
Neste artigo veremos a implementação do padrão Repository e do padrão Unit Of Work em um contexto do DDD.
Vamos iniciar então com a definição do que é o padrão Repository.
O que é um Repository ?
Um Repositório é usado para gerenciar a persistência e recuperação agregadas. Ele faz a mediação entre a camada de acesso a dados e o domínio e desacopla a camada de domínio da camada de dados de forma eficaz. Ele faz isso fornecendo acesso semelhante a uma coleção aos dados subjacentes.
O repositório oferece uma interface de coleção, fornecendo métodos para adicionar, modificar, remover e buscar objetos de domínio. Isso permite que o domínio permaneça agnóstico em relação ao mecanismo de persistência subjacente. Isso permite que ambas as camadas evoluam independentemente, mantendo alta coesão com baixo acoplamento.
Na figura abaixo temos uma comparação das abordagens sem usar repositório, usando o repositório e usando o repositório e o padrão unit of work:
O padrão repositório abstrai a tecnologia e arquitetura subjacentes da camada de persistência. Embora essa abstração seja altamente desejável, a realidade é que a escolha da tecnologia de persistência influencia o resto da pilha de algumas maneiras. Normalmente, cada repositório é responsável por persistir um aggregate root ou raiz agregada.
O padrão repositório torna a recuperação de dados explícita usando métodos de consulta
nomeados e limitando o acesso ao nível agregado (aggregate).
Ele não oferece uma interface aberta para o modelo de dados e isso torna mais
fácil expressar a intenção da operação explícita em termos do modelo de domínio.
Também torna mais fácil ajustar as consultas, pois elas estão contidas apenas no
repositório.
A implementação do padrão Repositório divide a camada do repositório em
unidades menores de código facilmente testável. Como a camada de domínio depende
da interface do repositório em vez da implementação concreta, temos que
os testes de unidade são mais fáceis de serem realizados graças a injeção de
repositórios simulados durante o teste.
No contexto do padrão de
repositório, as aggregate roots ou raízes agregadas são os
únicos objetos que o código do cliente carrega do repositório. O repositório encapsula o acesso aos objetos filhos - da perspectiva de um chamador, ele os carrega automaticamente, ao mesmo tempo que a raiz é carregada ou quando eles são realmente necessários (como no carregamento lento). |
Desta forma, cabe aqui apresentar dois conceitos importantes usados no DDD : Aggregate e Aggregate Root.
O que é um Aggregate e um Aggregatet Root ?
Os domínios são modelados usando o conceito de Entity e Value Objects. Acontece que você não pode modelar um domínio complexo apenas usando Entities e Value Objects.
Isso ocorre porque é difícil manter a consistência das alterações em objetos em um modelo com associações complexas. Como uma solução para este problema, Eric Evans recomenda que agrupemos as entidades e objetos de valor em agregados (aggregates) e que definamos os limites em torno de cada um. Neste processo devemos escolher uma entidade para ser a raiz de cada agregado o - Aggregate Root - e permitir que objetos externos contenham referências apenas para a raiz.
Podemos ver um aggregate como um encapsulamento de entidades e objetos de valor (objetos de domínio) que conceitualmente pertencem um ao outro. Ele também contém um conjunto de operações nas quais esses objetos de domínio podem ser operados. Assim ao invés de permitir que cada entidade ou objeto de valor execute todas as ações por conta própria, o agregado coletivo de itens é atribuído a um agregado raiz (aggregate root)
Como um exemplo concreto, que é recorrente para ilustrar esse conceito, um agregado pode ser um Carro, onde os objetos de domínio encapsulados podem ser Motor, Rodas, Cor e Luzes; da mesma forma, no contexto da fabricação de um carro, as operações podem ser: PintarModelo, InstalarRodas, InstalarMotor e InstalarLuzes, etc. As regras de negócios envolvidas podem ser:
Espero que com isso o conceito de aggregate e aggregate root seja bem assimilado.
ASP .NET Core Web API - Livraria
Como exemplo prático para mostrar a implementação do padrão Repository e do padrão Unit Of Work vamos partir de uma aplicação ASP .NET Core Web API bem simples que gerencia informações de Livros.
A solução Livraria contém 3 projetos:
Projeto Domain
No projeto Domain temos as pastas :
A idéia aqui é fazer uma comparação entre as implementações genérica e específica do padrão Repository e por último implementar o Unit Of Work.
Para simplificar o exemplo não estamos usando Value Objects e as Entidades podem assim ser consideradas como os sendo os aggregates.
Vejamos o código das entidades do modelo de domínio:
'1- Livro
public class Livro
{
public int Id { get; set; }
public string Titulo { get; set; }
public string Autor { get; set; }
public string Editora { get; set; }
public string Genero { get; set; }
public int Preco { get; set; }
}
|
2- Catalogo
public class Catalogo
{
public int CatalogoId { get; set; }
public string Nome { get; set; }
public Collection<Livro> Livros { get; set; }
}
|
As duas entidades acima representam um Livro e um objeto de domínio de Catalogo. A entidade do catálogo representa uma coleção de livros que formam um catálogo. O catálogo e as entidades do livro também representam uma raiz agregada (Aggregate Root) dentro deste contexto limitado.
Cada raiz agregada (Aggregate Root) tem sua própria interface de repositório ILivroRepository e ICatalogoRepository que usa para salvar e recuperar seu estado persistente do banco de dados.
Obs: Se você quiser reforçar o limite da raiz agregada você pode criar as interfaces dos repositórios na mesma pasta das entidades que representam as raízes agregadas (aggregate root).
Agora temos o código para os repositórios específicos:
1- ILivroRepository
using Livraria.Domain.Entities;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace Livraria.Domain.Interfaces
{
public interface ILivroRepository
{
Task<Livro> Get(int id);
Task<IEnumerable<Livro>> GetAll();
Task<int> Add(Livro entity);
Task<bool> Delete(int id);
Task<bool> Update(Livro entity);
}
}
|
2- ICatalogoRepository
using Livraria.Domain.Entities;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace Livraria.Domain.Interfaces
{
public interface ICatalogoRepository
{
Task<Catalogo> Get(int id);
Task<IEnumerable<Catalogo>> GetAll();
Task<int> Add(Catalogo entity);
Task<bool> Delete(int id);
Task<bool> Update(Catalogo entity);
}
}
|
Como você pode ver, as duas interfaces do repositório específicos são exatamente semelhantes, exceto pelo tipo de entidade que gerenciam. Podemos refatorar essas interfaces para usar uma interface de repositório genérica definindo a interface IGenericRepository.
Agora o código do repositório genérico:
3- IGenericRepository
using System.Collections.Generic;
using System.Threading.Tasks;
namespace Livraria.Domain.Interfaces
{
public interface IGenericRepository<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);
}
}
|
Após isso podemos refatorar o código das interfaces específicas da seguinte forma:
1- ICatalogoRepository
using Livraria.Domain.Entities;
namespace Livraria.Domain.Interfaces
{
public interface ICatalogoRepository : IGenericRepository<Catalogo>
{
}
}
|
2- ILivroRepository
using Livraria.Domain.Entities;
namespace Livraria.Domain.Interfaces
{
public interface ILivroRepository : IGenericRepository<Livro>
{
}
}
|
Temos que o código ficou mais enxuto.
Camada de Infraestrutura : Contexto e Repositórios
No projeto Livraria.Infra vamos criar duas pastas:
Teremos que incluir neste projeto os seguintes pacotes :
Isso será necessário pois na nossa implementação a instância do Repository usa o EF Core para se conectar com o banco de dados SQL Server e realizar as operações.
Antes de criar nossas classes de repositório concreto, precisamos implementar uma classe de contexto que herda de DbContext para conectar ao banco de dados. A implementação do contexto (DbContext) é uma representação de uma sessão entre o repositório e o banco de dados sendo usado para consultar e salvar instâncias das entidades em nossa fonte de dados. Ele também fornece funcionalidade adicional, como controle de transações, rastreamento de alterações, etc.
Na pasta Context vamos criar a classe LivrariaDbContext :
public class LivrariaDbContext : DbContext
{
public LivrariaDbContext(DbContextOptions<LivrariaDbContext> options) : base(options)
{ }
public DbSet<Livro> Livros { get; set; }
}
|
Agora que temos a classe DbContext representando uma conexão com o banco de dados, podemos criar as implementações concretas necessárias do Repositório Genérico e também as usadas pelas duas entidades raiz agregadas Livro e Catalogo.
Vamos iniciar criando a classe GenericRepository que vai implementar a interface IGenericRepository, o nosso repositório genérico. Vamos criar uma classe abstrata para servir de classe base para a implementação dos repositórios específicos.
1- GenericRepository
using Livraria.Domain.Interfaces;
using Livraria.Infra.Context;
using Microsoft.EntityFrameworkCore;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace Livraria.Infra.Repositories
{
public abstract class GenericRepository<T> : IGenericRepository<T> where T : class
{
protected readonly LivrariaDbContext _context;
protected GenericRepository(LivrariaDbContext 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);
}
}
}
|
Na implementação injetamos no construtor uma instância do contexto representando por LivrariaDbContext que representa a classe de contexto e a sessão entre o repositório e o banco de dados.
A seguir vamos implementar os repositórios específicos.
2- LivroRepository
using Livraria.Domain.Entities; using Livraria.Domain.Interfaces; using Livraria.Infra.Context; using System.Collections.Generic; using System.Linq; namespace Livraria.Infra.Repositories |
No repositório de livros temos a definição de um método específico para consultar os livros por gênero. Assim temos que ajustar o código da interface ILivroRepository definindo o contrato para o método GetLivrosPorGenero():
public interface ILivroRepository : IGenericRepository<Livro> { Task<IEnumerable<Livro>> GetLivrosPorGenero(string genero); } |
3- CatalogoRepository
using Livraria.Domain.Entities; using Livraria.Domain.Interfaces; using Livraria.Infra.Context;
namespace Livraria.Infra.Repositories |
As classes de repositório agora herdam da classe da classe de repositório abstrata genérica e implementam funcionalidades específicas para essa entidade. Esta é uma das melhores vantagens do padrão de repositório, pois agora podemos usar consultas nomeadas que retornam dados de negócios específicos.
Note que em ambas implementações, no construtor das classes, estamos obtendo a instância do contexto definido na classe base GenericRepository.
Para que a injeção de dependência feita nas implementações funcione temos que registrar os serviços dos repositórios e do contexto no contêiner de injeção de dependência nativo representado pela interface IServiceCollection.
Para isso vamos criar no projeto Livraria.Infra a classe DependencyInjection e criar o método de extensão para IServiceCollection chamado AddServicesInfra :
AddServicesInfra
using Livraria.Domain.Interfaces; using Livraria.Infra.Context; using Livraria.Infra.Repositories; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection;
namespace Livraria.Infra services.AddDbContext<LivrariaDbContext>(opt => opt return services; |
Neste código registramos os serviços dos repositórios e do contexto, definindo o provedor do banco de dados e a string de conexão que será obtida a partir do arquivo appsettings.json no projeto Livraria.API. Também já estou registrando o serviço da Unit Of Work que iremos implementar a seguir.
Com isso poderemos usar este método na classe Startup do projeto Livraria.API para poder acessar as instâncias dos repositórios e do contexto.
Repositório Genérico ou Repositório Específico
Há um longo debate sobre a eficácia de um repositório específico por raiz
agregada em comparação com um repositório genérico. Esse debate é tão antigo
quanto o próprio padrão de repositório. Um repositório é uma parte essencial do
domínio que está sendo modelado. Este é o principal motivo pelo qual as
interfaces do repositório para as entidades estão na camada de domínio.
Cada entidade é única e possui características únicas que impactam seu comportamento de persistência. Por ex. Algumas entidades não podem ser excluídas, algumas entidades não podem ser adicionadas e nem todas as entidades precisam de um repositório. As consultas variam muito e são específicas para cada entidade.
Assim, a API do repositório torna-se tão única quanto a própria entidade. No entanto, é mais fácil configurar um repositório genérico inicialmente e refatorá-lo para ser específico conforme o design e os requisitos evoluem.
Assim podemos criar repositórios específicos, mas abstrair as operações comuns em um repositório genérico e abstrato com métodos substituíveis. Essa foi a abordagem usada.
Antes de implementar os controladores no projeto Livraria.API vamos tratar com o padrão Unit Of Work.
O padrão Unit Of Work
O padrão Unit Of Work ou Unidade de Trabalho é um padrão de projeto, e, de acordo com Martin Fowler, o padrão de unidade de trabalho "mantém uma lista de objetos afetados por uma transação e coordena a escrita de mudanças e trata possíveis problemas de concorrência".
As camadas UntiOfWork e Repositório encapsulam a camada de dados das camadas do aplicativo e do modelo de domínio para que seja desacoplada dos níveis superiores. A implementação desses padrões pode facilitar o uso de repositórios fictícios que simulam o acesso ao banco de dados
O padrão Unit of Work esta presente em quase todas as ferramentas OR/M atuais (digo quase pois não conheço todas) e geralmente você não terá que criar a sua implementação personalizada a menos que decida realmente fazer isso por uma questão de força maior.
O padrão de unidade de trabalho acompanha todas as mudanças nos agregados. Depois que todas as atualizações dos agregados em um escopo são concluídas, as alterações rastreadas são reproduzidas no banco de dados em uma transação para que o banco de dados reflita as alterações desejadas.
Assim, a Unit Of Work rastreia uma transação de negócios e a traduz em uma transação de banco de dados, em que as etapas são executadas coletivamente como uma única unidade. Para garantir que a integridade dos dados não seja comprometida, a transação é confirmada ou revertida discretamente, evitando o estado indeterminado.
Podemos elencar as principais utilidades deste padrão a seguir:
A implementação da Unit Of Work é feita criando a interface na camada Domain e fazendo a sua implementação na camada de Infraestrutura.
Assim vamos criar a interface IUnitOfWork na pasta Interfaces do projeto Livraria.Domain:
using System;
namespace Livraria.Domain.Interfaces |
Na implementação usamos a interface IDisposable de forma a poder liberar os recursos não gerenciados de maneira determinística. No entanto, Dispose não remove o próprio objeto da memória. O objeto será removido quando o coletor de lixo achar conveniente.
Para realizar a implementação vamos criar a a classe UnitOfWork na pasta Repositories do projeto Livraria.Infra :
using Livraria.Domain.Interfaces; using Livraria.Infra.Context; using System; namespace Livraria.Infra.Repositories public UnitOfWork(LivrariaDbContext context, ILivroRepository livros,
public int Commit() |
A classe concreta implementa a IUnitOfWork onde vemos que as instâncias dos repositórios e do contexto são injetadas no seu construtor.
Assim, a Unit Of Work vincula todas as entidades envolvidas em um processo de negócios lógico em uma única transação e confirma a transação ou a reverte.
Dessa forma implementamos o padrão de repositório e o padrão de unidade de trabalho com as abstrações apropriadas e suas implementações concretas. Agora podemos usar isso para consultar e realizar atualizações transacionais das raízes agregadas no banco de dados. Também conectamos a injeção de dependência para fornecer esses serviços à camada API e com isso podemos injetar a Unit Of Wok em nossos controladores, onde a mágica finalmente acontece.
Na próxima parte do artigo iremos implementar os controladores.
Porque um menino nos nasceu, um filho se nos deu, e o principado está sobre os seus ombros, e se chamará o seu nome: Maravilhoso, Conselheiro, Deus Forte, Pai da Eternidade, Príncipe da Paz.
Referências:
C# - Obtendo a data e a hora por TimeZone
C# - O Struct Guid - Macoratti.net
C# - Checando Null de uma forma mais elegante e concisa
DateTime - Macoratti.net
Null o que é isso ? - Macoratti.net
Formatação de data e hora para uma cultura ...
C# - Calculando a diferença entre duas datas
NET - Padrão de Projeto - Null Object Pattern
C# - Fundamentos : Definindo DateTime como Null ...
C# - Os tipos Nullable (Tipos Anuláveis)