ASP .NET Core  5 - Implementando CQRS com Mediator - I


Neste artigo veremos a implementação do CQRS - Command Query Responsibility Segregation usando o padrão Mediator.(Mediatr)

Eu já apresentei o CQRS neste artigo - ASP .NET Core 3.1 - Entendendo o CQRS e também já abordei o padrão Mediator neste artigo: O padrão de projeto Mediator.

Mas porque usar o Mediator com o CQRS ?

O padrão de Mediator lida com o desacoplamento, colocando uma camada intermediária entre os componentes, chamada de "Mediador", que será sua única fonte de dependências.

Os componentes delegam suas chamadas de função ao Mediador e este decide qual componente precisa ser chamado e para quê. Isso garante que os componentes fiquem "fracamente acoplados" uns aos outros e não chamem uns aos outros diretamente. Isso também cria a oportunidade de um componente ser substituído por outra implementação, se necessário, e o sistema não será afetado.

No CQRS, as responsabilidades de Consulta e Comandos são assumidas por seus respectivos Handlers e torna-se muito difícil fazer a conexão manualmente entre um Comando com seu Handler e manter os mapeamentos.

É aqui que o padrão Mediator entra em cena: ele encapsula essas interações e cuida do mapeamento para nós.

CQRS - Implementação com o padrão Mediator usando o MediatR

Para implementar o CQRS usando este padrão, definimos um "Command" e um "Handler".

O "Command" é criado e enviado pelo método do front-end ao Mediator que contém um mapeamento dos "Commands" e seus "Handlers".

O Mediator reconhece o "Command" e delega o request ao respectivo "Handler", que retorna os dados em conformidade. Ele garante que os métodos dos front-end estejam sempre limpos e focados, enquanto o processamento necessário é feito pelo "Handler".

A implementação do padrão Mediator na ASP .NET Core pode ser feita usando a biblioteca MediatR que foi criada por Jimmy Bogard e que pode ser obtida via Nuget usando os comandos abaixo na janela do Package Manger Console:

Se você estiver usando o VS Code com a linha de comando NET CLI os comandos são:

O primeiro é o pacote do MediatR e o segundo pacote é usado para gerenciar suas dependências.

Nota: Vale lembrar que não somos obrigado a usar o padrão Mediator para implementar o CQRS.

A título de demonstração vamos implementar uma API bem simples que gerencia informações de clientes ou customers.

Vamos definir alguns métodos para encapsular o processamento desta entidade para operações de Leitura e Gravação e implementar o padrão CQRS neste projeto para desacoplar os métodos das camadas de domínio do back-end usando o padrão Mediator.

recursos usados:

Criando o projeto

Vamos iniciar abrindo o VS 2019 Community  e criando uma solução em branco ou Blank Solution com o nome DemoMediator;



A seguir no menu File clique em Add-> New Project e selecione o template Class Library (.NET Standard) e informe o nome DemoMediator.Domain;

Repita o procedimento acima e crie o seguintes projeto na solução

Nota:  Você pode usar outros nomes para as pastas, estes nomes são apenas uma sugestão. A camada CrossCutting neste exemplo poderia ser nomeada como IoC pois vai servir para centralizar a injeção de dependência;

Observação: Poderíamos ter definido o projeto API como sendo o nosso projeto Application e assim não teríamos o projeto Application.

Ao final teremos a solução e 5 projetos exibidos na janela Solution Explorer.

A criação do projeto API deve ser feita assim:

Abra o VS 2019 Community e crie um novo projeto via menu File-> New Project;

Selecione o template ASP .NET Core Web Application, e, Informe o nome da solução Net5_CRQS1 (ou outro nome a seu gosto).

A seguir selecione .NET Core e ASP .NET Core 5.0 e marque o template ASP .NET Core Web API e as configurações conforme figura abaixo:

Depois que o projeto foi criado, precisamos adicionar a referência aos seguintes pacotes:

Nota:  Para instalar use o comandoInstall-Package <nome> --version X.X.X

Vamos aproveitar e já configurar o serviço do MediatR no projeto API incluindo no método ConfigureServices da classe Startup o código destacado em azul :

 public void ConfigureServices(IServiceCollection services)
 {
          services.AddControllers();
          services.AddMediatR(Assembly.GetExecutingAssembly());
  
          services.AddSwaggerGen(c =>
          {
                c.SwaggerDoc("v1", new OpenApiInfo { Title = "DemoMediator.API", Version = "v1" });
          });
 }

Definindo as dependências entre os projetos

Para definir a as dependências entre os projetos temos que atentar para a regra de dependência usada na Clean Architecture que assevera que :  "As dependências do código-fonte devem apontar apenas para dentro, das camadas mais externas para as camadas mais internas, em direção a políticas de nível superior."

Dessa forma nada em um círculo interno pode saber absolutamente nada sobre algo em um círculo externo.
 

Assim para definir as dependências entre os projetos vamos seguir esta regra. Onde teremos que :

Para incluir uma referência ao projeto DemoMediator.Domain no projeto Application, clique com o botão direito do mouse sobre o projeto DemoMediator.Application e selecione Add-> Project Reference;

Selecione o projeto DemoMediator.Domain e clique em OK;

A seguir repita este procedimento e defina as dependências entre os demais projetos.

Definindo o modelo de domínio

Vamos agora organizar as pastas no projeto Domain criando as seguintes pastas:

Vamos iniciar criando a nossa entidade Produto na pasta Entity:

 public partial class Customer
 {
        public int Id { get; set; }
        public string Name { get; set; }
        public string Email { get; set; }
 }

A classe Customer  representa a nossa entidade de domínio e, geralmente, possui estado, comportamento e suas regras de negócio (neste exemplo não foram usadas para tornar mais simples o exemplo).

Criamos assim uma classe POCO anêmica sem comportamentos com get e set públicos. Em uma abordagem mais robusta teríamos que criar um modelo mais rico.

Na pasta Interfaces vamos criar a interface ICustomerRepository e definir o contrato para acessar as informações do domínio usando o padrão repositório.

using DemoMediator.Domain.Entities;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace DemoMediator.Domain.Interfaces
{
    public interface ICustomerRepository
    {
        Task<Customer> GetById(int id);
        Task<Customer> GetByEmail(string email);
        Task<IEnumerable<Customer>> GetAll();
        void Add(Customer customer);
        void Update(Customer customer);
        void Remove(Customer customer);
    }
}

Definimos três métodos GET que retornam dados atribuindo um Task<T> ao tipo de retorno enquanto os outros 3 métodos apenas alteram o estado da entidade e aguardam a execução do SaveChanges e por isso são definidos como síncronos.

Agora, temos que implementar essa interface usando as palavras-chave async e await. (Usar a palavra-chave await não é obrigatório, mas se não o usarmos, nossos métodos assíncronos serão executados de forma síncrona).

Ao final nosso projeto Domain deverá exibir a seguinte estrutura e conteúdo:

Definindo o contexto

Neste projeto iremos usar o EF Core como ferramenta ORM e vamos usá-lo na abordagem Code-First para criar o banco de dados SQL Server CustomersDB a tabela Customers que iremos mapear no arquivo de contexto.

Assim teremos que realizar as seguintes tarefas no projeto Infrastructure:

Vamos iniciar criando as seguintes pastas no projeto Infrastructure:

  1. Context
  2. EntityConfiguration
  3. Repositories

Na pasta Context crie a classe AppDbContext :

using DemoMediator.Domain.Entities;
using DemoMediator.Infrastructure.EntityConfiguration;
using Microsoft.EntityFrameworkCore;
namespace DemoMediator.Infrastructure.Context
{
    public class AppDbContext : DbContext
    {
        public AppDbContext(DbContextOptions<AppDbContext> options) : base(options)
        { }
        public DbSet<Customer> Customers { get; set; }
        protected override void OnModelCreating(ModelBuilder builder)
        {
            builder.ApplyConfiguration(new CustomerConfiguration());
        }
    }
}

Nesta classe definimos no construtor as opções do DbContext e mapeamos a entidade Customer para tabela Customers.

A seguir no método OnModelCreating estamos usando uma instância de ModelBuilder para aplicar as configurações nas propriedades da entidade Customer que serão seguidas pelo EF Core na aplicação do Migrations para criar a tabela.

Assim na pasta EntityConfiguration vamos criar a classe CustomerConfiguration com as definições a seguir:

using DemoMediator.Domain.Entities;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace DemoMediator.Infrastructure.EntityConfiguration
{
    public class CustomerConfiguration : IEntityTypeConfiguration<Customer>
    {
        public void Configure(EntityTypeBuilder<Customer> builder)
        {
            builder.Property(b => b.Name).HasMaxLength(100);
            builder.Property(b => b.Email).HasMaxLength(150);
            builder.HasData(
                new Customer{
                    Id = 1, 
                    Name= "Jim Morrison", 
                    Email = "jim@email.com" 
                },
                new Customer
                {
                    Id = 2,
                    Name = "Elvis Presley",
                    Email = "elvis@email.com"
                },
                new Customer
                {
                    Id = 3,
                    Name = "Janis Joplin",
                    Email = "janis@email.com"
                }
           );
        }
    }
}

Neste código usamos o método HasData da Fluent API onde estamos configurando a tabela Customers para ter dados iniciais que serão gerados ao aplicar o Migrations.

Agora precisamos definir no arquivo appsettings.json do projeto DemoMediator.API a string de conexão para poder informar o nome do banco de dados.

{
  "ConnectionStrings": {
    "DefaultConnection": "Data Source=...\\sqlexpress;Initial Catalog=CustomersDB;Integrated Security=True"
  },
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "AllowedHosts": "*"
}

E a seguir vamos definir a configuração do contexto como um serviço e isso geralmente seria feito no projeto API mas para não termos nenhuma referência ao EF Core no projeto API vamos definir esta configuração no projeto DemoMediator.CrossCutting.

Neste projeto vamos criar a classe DependencyInjection e definir o método de extensão AddInfrastructure() :

using DemoMediator.Domain.Interfaces;
using DemoMediator.Infrastructure.Context;
using DemoMediator.Infrastructure.Repositories;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
namespace DemoMediator.CrossCutting
{
    public static class DependencyInjection
    {
        public static IServiceCollection AddInfrastructure(this IServiceCollection services,
           IConfiguration configuration)
        {
            services.AddDbContext<AppDbContext>(options =>
                options.UseSqlServer(
                    configuration.GetConnectionString("DefaultConnection"),
                    b => b.MigrationsAssembly(typeof(AppDbContext)
                            .Assembly.FullName)));
            services.AddScoped<ICustomerRepository, CustomerRepository>();
            return services;
        }
    }
}

Neste código criamos o serviço para o nosso contexto e também já registrarmos o serviço do nosso repositório embora ainda precisamos fazer a implementação da classe concreta CustomerRepository.

Agora precisamos definir no projeto DemoMediator.API no método ConfigureServices do arquivo Startup a utilização deste método de extensão e assim disponibilizar a definição do serviço do contexto para a aplicação API.

...

         public void ConfigureServices(IServiceCollection services)
         {
            services.AddControllers();
            services.AddInfrastructure(Configuration);
            services.AddMediatR(Assembly.GetExecutingAssembly());           
            services.AddSwaggerGen(c =>
            {
                c.SwaggerDoc("v1", new OpenApiInfo { Title = "DemoMediator.API", Version = "v1" });
            });
        }
...

Agora vamos implementar o repositório no projeto DemoMediator.Infrastructure criando a classe CustomerRepository na pasta Repositories:

using DemoMediator.Domain.Entities;
using DemoMediator.Domain.Interfaces;
using DemoMediator.Infrastructure.Context;
using Microsoft.EntityFrameworkCore;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace DemoMediator.Infrastructure.Repositories
{
    public class CustomerRepository : ICustomerRepository
    {
        protected readonly AppDbContext db;
        public CustomerRepository(AppDbContext context)
        {
            db = context;
        }
        public async Task<Customer> GetById(int id)
        {
            return await db.Customers.FindAsync(id);
        }
        public async Task<IEnumerable<Customer>> GetAll()
        {
            return await db.Customers.ToListAsync();
        }
        public async Task<Customer> GetByEmail(string email)
        {
            return await db.Customers.AsNoTracking().FirstOrDefaultAsync(c => c.Email == email);
        }
        public void Add(Customer customer)
        {
            db.Customers.Add(customer);
            db.SaveChanges();
        }
        public void Update(Customer customer)
        {
            db.Customers.Update(customer);
            db.SaveChanges();
        }
        public void Remove(Customer customer)
        {
            db.Customers.Remove(customer);
            db.SaveChanges();
        }
        public void Dispose()
        {
            db.Dispose();
        }
    }
}

Aplicando o Migrations

Agora podemos aplicar o Migrations.

No Visual Studio acione o menu Tools-> Nuget Package Manager -> Package Manager Console ;

E na janela do Package Manager Console selecione o projeto DemoMediator.Infrastructure e defina os comandos do Migrations:

add-migration Inicial

Agora vamos emitir o comando update-database para criar o banco de dados e a tabela Customers:

Abrindo o SQL Server Management Studio podemos confirmar a criação da tabela Customers contendo os registros:

Dessa forma nosso projeto API não possui referências ao EF Core e todas as referências e a migrações estão no projeto de infraestrutura.

Ao final nosso projeto Infrastructure deverá exibir a seguinte estrutura e conteúdo :

Na próxima parte do artigo vamos continuar implementando o CQRS usando o Mediator.

"E não comuniqueis com as obras infrutuosas das trevas, mas antes condenai-as. Porque o que eles fazem em oculto até dizê-lo é torpe. Mas todas estas coisas se manifestam, sendo condenadas pela luz, porque a luz tudo manifesta."
Efésios 5:11-13

Referências:


José Carlos Macoratti