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:

  1. Livraria.Domain - Representa a camada Domain (projeto Class Library)
  2. Livraria.API  - É a nossa API (projeto ASP .NET Core Web App)
  3. Livraria.Infra - Representa a camada de Infraestrutura (projeto Class Library)

Projeto Domain

No projeto Domain temos as pastas :

  1. Entities - Onde estão as entidades Livro e Catalogo;
  2. Interfaces - Onde estão as interfaces dos repositórios específicos ILivroRepository e ICatalogoRepository e do repositório genérico IGenericRepository. Aqui iremos definir também a Unit Of Work.

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:

  1. Context  - Onde vamos criar a classe de contexto que herda de DbContext;
  2. Repositories - Onde vamos fazer a implementação concreta dos repositórios;

Teremos que incluir neste projeto os seguintes pacotes :

  1. Microsoft.EntityFrameworkCore.SqlServer → Fornece o provedor com o banco de dados SQL Server;
  2. Microsoft.EntityFrameworkCore.Tools → Habilita o uso dos comandos para realizar o Migrations;

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
{
    public class LivroRepository : GenericRepository<Livro>, ILivroRepository
    {
        public LivroRepository(LivrariaDbContext context) : base(context)
        {
        }
        public async Task<IEnumerable<Livro>> GetLivrosPorGenero(string genero)
        {
            return _context.Livros.Where(x => x.Genero == genero).ToListAsync();
        }
    }
}

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
{
    public class CatalogoRepository : GenericRepository<Catalogo>, ICatalogoRepository
    {
        public CatalogoRepository(LivrariaDbContext context) : base(context)
        {
        }
    }
}

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
{
    public static class DependencyInjection
    {
        public static IServiceCollection AddServicesInfra(this IServiceCollection services,
           IConfiguration configuration)
        {
            services.AddTransient<ILivroRepository, LivroRepository>();
            services.AddTransient<ICatalogoRepository, CatalogoRepository>();
            services.AddTransient<IUnitOfWork, UnitOfWork>();

            services.AddDbContext<LivrariaDbContext>(opt => opt
                .UseSqlServer(configuration.GetConnectionString("DefaultConnection")));

            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
{
    public interface IUnitOfWork : IDisposable
    {
        ILivroRepository Livros { get; }
        ICatalogoRepository Catalogos { get; }
        int Commit();
    }
}

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 class UnitOfWork : IUnitOfWork
    {
        private readonly LivrariaDbContext _context;
        public ILivroRepository Livros { get; }
        public ICatalogoRepository Catalogos { get; }

        public UnitOfWork(LivrariaDbContext context, ILivroRepository livros,
            ICatalogoRepository catalogos)
        {
            _context = context ?? throw new ArgumentNullException(nameof(context));
            Livros = livros ?? throw new ArgumentNullException(nameof(livros));
            Catalogos = catalogos ?? throw new ArgumentNullException(nameof(catalogos));
        }

        public int Commit()
        {
            return _context.SaveChanges();
        }

        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        protected virtual void Dispose(bool disposing)
        {
            if (disposing)
            {
                _context.Dispose();
            }
        }
    }
}

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.

Isaías 9:6

Referências:


José Carlos Macoratti