ASP .NET Core - Usando o padrão Mediator com MediatR (CQRS)
Neste artigo
vou mostrar como podemos implementar o padrão Mediator usando o
MediatR em uma aplicação ASP .NET Core MVC na versão 3.1. |
O padrão Mediator é um padrão de projetos comportamental (Gof) que eu já abordei neste artigo: O padrão de projeto Mediator.
A aplicação deste padrão nos ajuda a garantir um baixo acoplamento entre os objetos de nossa aplicação permitindo que um objeto se comunique com outros objetos sem saber nada de sua estrutura e também centraliza o fluxo de comunicação entre os objetos facilitando sua manutenção.
Neste artigo faremos uma implementação deste padrão em uma aplicação ASP .NET Core utilizando 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:
dotnet add package MediatR
dotnet add package MediatR.Extensions.Microsoft.DependencyInjection --version 8.0.0
O primeiro é o pacote do MediatR e o segundo pacote é usado para gerenciar suas dependências.
Ocorre que se em sua aplicação houver um grande fluxo de requisições entre os objetos, o objeto mediator pode ser o gargalo da aplicação e para contornar isso é comum realizar a implementação do CQRS - Command Query Responsability Segregation que traduzindo é Segregação de Responsabilidade de Comando e Consulta.
|
|
O
CQRS é mais um padrão de projeto separa as
operações de leitura e de escrita da base de dados em dois modelos :
|
Nota: Vale lembrar que não precisamos do padrão Mediator para implementar o padrão CQRS.
Como funciona ?
Aqui temos dois componentes principais chamados de Request e Handler, que são implementados usando as interfaces IRequest e IRequestHandler<TRequest> :
Onde, um Request contém propriedades que são usadas para fazer o input dos dados para os Handlers.
Para que esses dois componentes funcionem precisamos de um mediador que faz o meio de campo recebendo um request e invocando o Handler associado a ele.
É aqui que o componente Mediator, que implementa a interface IMediator, entra em cena, e por onde deveremos interagir com as demais classes. Como usamos uma interface (IMediator) apenas enviamos o request para o Mediator que vai chamar a classe que vai executar o comando; tudo de forma transparente.
Aplicando isso à ASP .NET Core podemos ter controladores mais enxutos que vão receber os requests HTTP e executar o Mediator.
A seguir vamos criar uma aplicação ASP .NET Core onde vamos aplicar o padrão Mediator usando a biblioteca MediatR e usar os conceitos de CQRS via mediator.
recursos usados:
Criando o projeto inicial no VS 2019
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 AspNet_MediatR1 (ou outro nome a seu gosto).
A seguir selecione .NET Core e ASP .NET Core 3.1 e marque o template 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 comando: Install-Package <nome> --version X.X.X
Definindo o modelo de domínio, os comandos e os handlers
Vamos agora organizar as pastas do Domínio.
Crie uma pasta Domain no projeto e nesta pasta crie as pastas:
Vamos iniciar criando a nossa entidade Produto na pasta Entity:
public class Produto
{
public int Id { get; set; }
public string Nome { get; set; }
public decimal Preco { get; set; }
}
|
A classe Produto representa a nossa entidade de domínio Produto, e, geralmente, possui estado, comportamento e suas regras de negócio (neste exemplo não foram usadas para tornar mais simples o exemplo).
Criando os Commands
Agora na pasta Command vamos implementar os comandos relativos às ações que iremos executar. Para isso vamos implementar o padrão Command que define um objeto que encapsula toda a informação para executar uma ação.
É aqui que temos a utilização do CQRS pela implementação do padrão Command composto de dois objetos:
1- ProdutoCreateCommand
using MediatR public class ProdutoCreateCommand : IRequest<string> { public string Nome { get; private set; } public decimal Preco { get; private set; } } |
Note que esta classe implementa a interface IRequest que é a interface do MediatR usada para indicar que esse é um comando usado pelas classes Handlers que iremos criar a seguir.
2- ProdutoDeleteCommand
using MediatR public class ProdutoDeleteCommand : IRequest<string> { public int Id { get; set; } } |
3- ProdutoUpdateCommand
using MediatR public class ProdutoUpdateCommand : IRequest<string> { public int Id { get; private set; } public string Nome { get; private set; } public decimal Preco { get; private set; } } |
Todas as classes implementam IRequest<T> onde especificamos o tipo de dados que será retornado quando o comando for processado, e, também, através da qual vinculamos os comandos com as classes Command Handlers. É assim que a MediatR sabe qual objeto deve ser invocado quando um request for gerado.
Assim para cada Command vamos criar um Command Handler, embora podemos implementar um único objeto Command Handler para tratar todos os Commands criados na aplicação.
Criando o Repositório
Não vamos usar um banco de dados, ao invés disso, vamos criar uma interface IRepository<T> e implementar esta interface para salvar os dados em uma coleção estática usando a classe ProdutoRepository.
Vamos criar uma pasta Repository no projeto e nesta pasta criar a interface e a classe que a implementa:
1- IRepository
using System.Collections.Generic; using System.Threading.Tasks;
public interface IRepository<T> |
2 - ProdutoRepository
using AspNet_MediatR1.Domain.Entity; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; public class ProdutoRepository : IRepository<Produto> public async Task<IEnumerable<Produto>> GetAll() public async Task<Produto> Get(int id) public async Task Add(Produto produto) public async Task Edit(Produto produto) public async Task Delete(int id) |
Criando Notificações
As notificações são necessárias para informar que uma requisição foi concluída com sucesso visto que as requisições Command não retornam nenhuma informação.
Para isso podemos invocar o método Publish() no método Handler da classe Command Handler passando por parâmetro uma notificação, assim, todos os Event Handlers que estiverem “ouvindo” as notificações do tipo do objeto “publicado” serão notificados e poderão processá-lo.
O método Publish() é o responsável por emitir a notificação em todo sistema, e, ele vai procurar a classe que possui a herança da interface INotificationHandler<T> e invocar o método Handler() para processar aquela notificação.
Para implementar as notificações temos que definir os objetos notification.
Vamos criar uma pasta Notifications no projeto e a seguir criar três classes que herdam da interface INotification.
1- ProdutoCreateNotification
using MediatR;
namespace AspNet_MediatR1.Notifications |
2- ProdutoUpdateNotification
using MediatR;
namespace AspNet_MediatR1.Notifications |
3- ProdutoDeleteNotification
using MediatR;
namespace AspNet_MediatR1.Notifications |
4- ErroNotification
using MediatR;
namespace AspNet_MediatR1.Notifications |
Precisamos criar uma classe do tipo Notification Handler que deverá “escutar” todas as notificações, pois todas serão apenas registradas no console.
Para isso vamos criar a pasta EventsHandlers no projeto e criar a classe LogEventHandler :
using AspNet_MediatR1.Notifications; using MediatR; using System; using System.Threading; using System.Threading.Tasks; namespace AspNet_MediatR1.EventsHandlers public Task Handle(ProdutoUpdateNotification notification, CancellationToken cancellationToken) public Task Handle(ProdutoDeleteNotification notification, CancellationToken cancellationToken) public Task Handle(ErroNotification notification, CancellationToken cancellationToken) |
Podemos definir tantas classes Notification Handlers quanto forem necessárias. Caso uma notificação seja “ouvida” por mais de um Notification Handlers, todos serão invocados quando a notificação for gerada.
Criando os Command Handlers
Para cada objeto Command devemos ter um objeto Command Handler.
Vamos então criar os Command Handlers na pasta Handlers:
1- ProdutoCreateCommandHandler
using AspNet_MediatR1.Domain.Command; using AspNet_MediatR1.Domain.Entity; using AspNet_MediatR1.Notifications; using AspNet_MediatR1.Repository; using MediatR; using System; using System.Threading; using System.Threading.Tasks;
namespace AspNet_MediatR1.Domain.Handler
public ProdutoCreateCommandHandler(IMediator mediator,
IRepository<Produto> repository)
try |
2- ProdutoUpdateCommandHandler
using AspNet_MediatR1.Domain.Command; using AspNet_MediatR1.Domain.Entity; using AspNet_MediatR1.Notifications; using AspNet_MediatR1.Repository; using MediatR; using System; using System.Threading; using System.Threading.Tasks; namespace AspNet_MediatR1.Domain.Handler public ProdutoUpdateCommandHandler(IMediator mediator, IRepository<Produto> repository) try await _mediator.Publish(new ProdutoUpdateNotification return await Task.FromResult("Produto alterado com sucesso"); await _mediator.Publish(new ErroNotification { Erro = ex.Message,
return await Task.FromResult("Ocorreu um erro no momento da alteração"); |
3- ProdutoDeleteCommandHandler
using System.Threading; using System.Threading.Tasks; namespace AspNet_MediatR1.Domain.Handler public ProdutoDeleteCommandHandler(IMediator mediator, IRepository<Produto> repository) await _mediator.Publish(new ProdutoDeleteNotification return await Task.FromResult("Produto excluido com sucesso"); await _mediator.Publish(new ErroNotification return await Task.FromResult("Ocorreu um erro no momento da exclusão"); |
Os command handlers implementam a interface IRequestHandler, onde é especificada uma classe Command e o tipo de retorno. Quando esta classe Command gerar uma solicitação, o MediatR irá invocar o command handler, chamando o método Handler.
É no método Handler onde são definidas instruções que devem ser realizadas para aplicar a solicitação definida pelo command.
Após a solicitação ser atendida, podemos usar o método Publish() para emitir uma notificação para todo sistema. Aqui o MediatR vai procurar pela classe com a implementação da interface INotificationHandler<notificacao> e invocar o método Handler() para processar aquela notificação que implementamos.
Criando o controlador
Agora vamos criar o controlador da nossa aplicação.
Clique com o botão direito do mouse sobre a pasta Controllers e a seguir clique em Add->Controller;
A seguir escolha a opção API Controller - Empty e clique em Add;
Informe o nome ProdutosController e clique em Add.
Ao final será criado o controlador onde vamos incluir o código abaixo:
using
AspNet_MediatR1.Domain.Command; using AspNet_MediatR1.Domain.Entity; using AspNet_MediatR1.Repository; using MediatR; using Microsoft.AspNetCore.Mvc; using System.Threading.Tasks; namespace AspNet_MediatR1.Controllers public ProdutosController(IMediator mediator, IRepository<Produto> repository) [HttpGet] [HttpGet("{id}")] [HttpPost] [HttpPut] [HttpDelete("{id}")] |
Injetamos no construtor uma instância do repositório e da interface IMediator pois precisamos enviar as requisições dos nosso objetos Command usando o método Send.
Aqui a interface IMediator faz o papel da classe mediadora que usa o método Send par chamar os comand handlers definidos.
Assim para tudo isso funcionar precisamos adicionar o MediatR como um serviço e registrar o serviço do repositório no método ConfigureServices:
public
void ConfigureServices(IServiceCollection services) { services.AddControllers(); services.AddMediatR(typeof(Startup)); services.AddSingleton<IRepository<Pessoa>, PessoaRepository>(); } |
Para testar podemos simplesmente definir alguns produtos em memória no Repositório ProdutoRepository:
public class ProdutoRepository : IRepository<Produto> { private Dictionary<int, Produto> produtos = new Dictionary<int, Produto>();
public Dictionary<int, Produto> GetProdutos()
public ProdutoRepository() |
E podemos alterar o arquivo launchSettings.json definindo a url inicial de acesso para : api/produtos
... "profiles": { "IIS Express": { "commandName": "IISExpress", "launchBrowser": true, "launchUrl": "api/produtos", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } }, "AspNet_MediatR1": { "commandName": "Project", "launchBrowser": true, "launchUrl": "api/produtos", "applicationUrl": "https://localhost:5001;http://localhost:5000", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } } ... |
Agora é só alegria.
Executando o projeto teremos e usando o Postman podemos testar nossa implementação.
1- Fazendo um GET
2- Fazendo um POST
O projeto completo aqui: AspNet_MediatR1.zip (sem as referências)
"E, chegando-se
Jesus, falou-lhes, dizendo: É-me dado todo o poder no céu e na terra."
Mateus 28:18
Referências:
ASP .NET Core - Implementando a segurança com ... -
ASP.NET Core MVC - Criando um Dashboard ...
C# - Gerando QRCode - Macoratti
ASP .NET - Gerando QRCode com a API do Google