ASP.NET Core - Usando a biblioteca MediatR (revisitado)
Hoje vamos rever como usar a biblioteca MediatR em um projeto ASP .NET Core WEB API. |
A biblioteca MediatR é uma biblioteca open source criada por Jimmy Bogard, cujo objetivo é fornecer uma implementação simples do padrão Mediator em aplicações .NET.
Esta biblioteca permite o envio de mensagens no processo - o que, por sua vez, permite seguir o padrão Mediador fornecendo as interfaces que facilitam a implementação do fluxo de comunicação entre os objetos.
O que é o padrão Mediator ?
O padrão
Mediator é um padrão de projetos comportamental (Gof),
definido da seguinte forma:
"O objetivo do padrão Mediator é definir um objeto que encapsula a forma como
um conjunto de objetos interage. O Mediator promove o acoplamento fraco ao
evitar que os objetos se refiram uns aos outros de forma explícita e permite
variar suas interações independentemente.”
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.
A seguir temos uma figura que mostrar de forma simplificada a atuação do padrão Mediator:
Temos 3 objetos A, X e Y..
Objeto A, precisa conversar com Objeto X e também com o Objeto Y mas Objeto A
não conhece nem X nem Y.
Usando o Mediator podemos criar um Mediador e agora
o objeto A, manda uma mensagem para o mediador, e o mediador envia a mensagem
para o objeto X e Y.
O mesmo principio é valido se o objeto x ou o objeto y quiserem se comunicar com
o objeto A ou se comunicarem entre si.
Com esse padrão, cada objeto possui uma única responsabilidade e consegue se comunicar com outros objetos sem a necessidade de conhecê-los, assim, cada objeto, trabalha de forma independente e isolada, não havendo acoplamento entre eles.
Vantagens e desvantagens
Como não toda a tecnologia usar o padrão Mediator trás vantagens e desvantagens e cabe a você analisar o cenário e considerar a viabilidade de sua utilização.
Vantagens
1- Desacoplamento entre os objetos
2- Encapsula a comunicação entre os objetos
3- Os objetos podem ser facilmente alterados pois são independentes
Desvantagens
1- O mediador pode se tornar o gargalo da aplicação
2- A complexidade do código aumenta
MediatR : Instalação e configuração
A biblioteca MediatR pode ser usada em diversos tipos de projetos da plataforma .NET , mas ela é mais usada em projetos ASP .NET Core. Ela nos ajuda a usar o padrão Mediator em nosso projeto ASP.NET Core e assim obter um desacoplamento do código.
A instalação e configuração do MediatR em um projeto ASP .NET Core é feita da seguinte forma:
1- Instalar o pacote MediatR que traz a implementação do padrão Mediator
1- Instalar o pacote MediatR.Extensions.DependencyInjection que permite gerenciar as dependências
A seguir podemos registrar todos os handlers em um assembly usando o método de extensão AddMediatR no método ConfigureServices da classe Startup :
public void
ConfigureServices(IServiceCollection services) { services.AddMediatR(Assembly.GetExecutingAssembly()); ... } |
A seguir poderemos injetar a interface IMediator nos controladores e serviços.
Funcionamento da biblioteca MediatR
Basicamente a biblioteca MediatR possui dois tipos de mensagens que ele despacha :
Aqui temos dois componentes principais chamados de Request e Handler.
Cada Handler normalmente irá tratar um único Request e assim podemos ter classes menores e mais simples.
Esses componentes são implementados usando as interfaces:
Essas interfaces atuam em cenários de comandos e consultas primeiro criando a mensagem com IRequest e a seguir realizando o processamento com IRequestHandler.
Existem dois dois tipos de requests no MediatR :
Cada interface Request tem sua própria interface de Handler. Assim temos :
Esses dois
componentes não fazem nada sozinhos eles precisam de um intermediador, que será
responsável por receber um Request e invocar o Handler associado á
ele.
Para isso temos um componente chamado Mediator que implementa a interface
IMediator, por onde deveremos interagir com as
demais classes. Usando a interface IMediator nossas classes não irão
saber quem ou quais componentes irão realizar determinada ação; apenas
enviamos a mensagem para o Mediator e ele irá se encarregar de chamar a
classe que irá executar o que precisamos.
Vamos agora á parte prática onde vou mostrar como usar a biblioteca MediatR em um projeto ASP .NET Core Web API onde vamos criar dois controladores, um sem usar o padrão Mediator e outro usando o padrão Mediator via biblioteca MediatR.
Neste exemplo eu não vou usar um banco de dados e vou fazer uma implementação de repositório e de um serviço fakes apenas para mostrar o uso da biblioteca MediatR.
Criando o projeto ASP .NET Core
Abra o VS 2019 e clique em New Project e selecione o template ASP .NET Core Web API e clique em Next;
Informe o nome ApiMediatR e clique em Next;
A seguir selecione o Target Framework, Authentication Type e demais configurações conforme mostrada na figura:
Clique em Create.
Vamos incluir no projeto os pacotes MediatR e MediatR.Extensions.DependencyInjection no projeto usando os comandos:
Após isso vamos configurar o serviço do MediatR na classe Startup:
public void ConfigureServices(IServiceCollection services)
{
services.AddMediatR(Assembly.GetExecutingAssembly());
services.AddControllers();
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo { Title = "Api_AutoMapper", Version = "v1" });
});
}
|
A seguir vamos criar no projeto a pasta Models e definir o modelo de domínio criando a classe Customer:
public class Customer
{
public Guid CustomerId { get; set; }
public string Name { get; set; }
}
|
Crie no projeto a pasta Repositories e nesta pasta implemente o padrão Repository criando a interface ICustomerRepository e a classe CustomerRepository:
1- ICustomerRepository
using ApiMediatR.Models;
using System;
using System.Threading.Tasks;
namespace ApiMediatR.Repositories
{
public interface ICustomerRepository
{
Task<Customer> GetCustomer(Guid customerId);
Task<Guid> CreateCustomer(Customer customer);
}
}
|
2- CustomerRepository
using ApiMediatR.Models;
using System;
using System.Threading.Tasks;
namespace ApiMediatR.Repositories
{
public class CustomerRepository : ICustomerRepository
{
public async Task<Customer> GetCustomer(Guid customerId)
{
Customer customer = new Customer
{
CustomerId = customerId,
Name = "Macoratti"
};
return customer;
}
public async Task<Guid> CreateCustomer(Customer customer)
{
return Guid.NewGuid();
}
}
}
|
A seguir crie uma pasta Services e nesta pasta vamos criar a interface IValidationService e ValidationService que será um serviço fake de validação de dados:
1- IValidationService
public interface IValidationService { void Validate<T>(T obj); } |
Estamos criando 3 novas rece
2- ValidationService
public class ValidationService : IValidationService { public void Validate<T>(T obj) { //codigo } } |
Agora podemos iniciar a criação dos controladores. Vamos iniciar criando o controlador CustomerSemMediatorController, que implementa o controlador sem usar o padrão Mediator e não usa os recursos da biblioteca MediatR.
Assim na pasta Controllers crie este controlador com o código abaixo:
using ApiMediatR.Models;
using ApiMediatR.Repositories;
using ApiMediatR.Services;
using Microsoft.AspNetCore.Mvc;
using System;
using System.Threading.Tasks;
namespace ApiMediatR.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class CustomersSemMediatorController : ControllerBase
{
private readonly ICustomerRepository _customerRepository;
private readonly IValidationService _serviceValidation;
public CustomersSemMediatorController(ICustomerRepository customerRepository,
IValidationService validaService)
{
_customerRepository = customerRepository;
_serviceValidation = validaService;
}
[HttpGet("get-customer/{customerId:Guid}")]
public async Task<Customer> GetCustomer(Guid customerId)
{
_serviceValidation.Validate<Guid>(customerId);
return await _customerRepository.GetCustomer(customerId);
}
[HttpPost("create-customer")]
public async Task<Guid> CreateCustomer([FromBody] Customer newCustomer)
{
_serviceValidation.Validate<Customer>(newCustomer);
var customer = new Customer
{
Name = newCustomer.Name
};
return await _customerRepository.CreateCustomer(customer);
}
}
}
|
Temos neste
controlador a implementação padrão feita usando a injeção dos serviços de
repositório e validação no construtor. Em um projeto mais complexo teríamos mais
serviços e a mais dependências sendo injetadas via construtor. Quando isso
acontece, a maioria das Actions acaba usando apenas um subconjunto das
dependências injetadas.
Se tivéssemos que colocar cada Action em sua própria classe, o número de
dependências necessárias provavelmente seria menor do que o total injetado no
controlador. Ter muitas dependências injetadas em qualquer serviço
geralmente é um indicador o principio da responsabilidade única (SRP) esta sendo
violado.
Assim podemos ter muitas dependências em um controlador e em cada método Action além de não usarmos todas as dependências estamos definindo o código para realizar a operação em cada método Action.
Vamos criar outro controlador na mesma pasta chamado CustomersComMediatorController :
using ApiMediatR.Handlers.Request;
using ApiMediatR.Models;
using MediatR;
using Microsoft.AspNetCore.Mvc;
using System;
using System.Threading.Tasks;
namespace ApiMediatR.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class CustomersComMediatorController : ControllerBase
{
private readonly IMediator _mediator;
public CustomersComMediatorController(IMediator mediator) => _mediator = mediator;
[HttpGet("get-customer/{customerId:Guid}")]
public async Task<Customer> GetCustomer(Guid customerId) =>
await _mediator.Send(new GetCustomerRequest { CustomerId = customerId });
[HttpPost("create-customer")]
public async Task<Guid> CreatCustomer(Customer customer) =>
await _mediator.Send(new CreateCustomerRequest { Customer = customer });
}
}
|
Neste código temos
a utilização da interface IMediator sendo injetada no construtor do controlador
de forma a termos acesso à implementação do padrão Mediator através das
interfaces fornecidas.
Observe que a dependência do repositório e da validação foram removidas do
controlador.
Se seguirmos essa refatoração para cada método de Action cada dependência seria
removida do controlador e substituída por apenas uma: a interface IMediator
E agora cada método Action ao invés de realizar as operações está criando um
comando que pode ser passado para um manipulador separado por meio do método
_mediator.Send().
Note que estamos criando as instâncias das classes GetCustomerRequest e CreateCustomerRequest que representam os requests e que vão implementar a interface IRequest e a seguir teremos que definir o respectivo Handler implementando a interface IRequestHandler.
Para realizar estas implementações criamos a pasta Handlers no projeto e nesta pasta criamos a pasta Requests onde criamos as classes GetCustomerRequest e CreateCustomerRequest que implementam a interface IRequest<T>:
1- GetCustomerRequest
using ApiMediatR.Models; using MediatR; using System; namespace ApiMediatR.Handlers.Requests { public class GetCustomerRequest : IRequest<Customer> { public Guid CustomerId { get; set; } } } |
Esta classe vai retornar um Customer e esta fornecendo como input um CustomerId do tipo Guid.
2- CreateCustomerRequest
using ApiMediatR.Models; using MediatR; using System; namespace ApiMediatR.Handlers.Requests { public class CreateCustomerRequest : IRequest<Guid> { public Customer Customer { get; set; } } } |
Esta classe vai retornar um Guid e esta fornecendo como input um Customer do tipo Customer.
A seguir na pasta Handlers temos que implementar os respectivo Handlers para cada uma das classes acima.
Assim na pasta Handlers vamos criar as classes GetCustomerHandler e CreateCustomerHandler que implementam a interface IRequestHandler<T>:
1- GetCustomerHandler
using ApiMediatR.Handlers.Requests;
using ApiMediatR.Models;
using ApiMediatR.Repositories;
using ApiMediatR.Services;
using MediatR;
using System;
using System.Threading;
using System.Threading.Tasks;
namespace ApiMediatR.Handlers
{
public class GetCustomerHandler : IRequestHandler<GetCustomerRequest, Customer>
{
private readonly IValidationService _validationService;
private readonly ICustomerRepository _customerRepository;
public GetCustomerHandler(IValidationService validationService,
ICustomerRepository customerRepository)
{
_customerRepository = customerRepository;
_validationService = validationService;
}
public async Task<Customer> Handle(GetCustomerRequest request,
CancellationToken cancellationToken)
{
_validationService.Validate<Guid>(request.CustomerId);
return await _customerRepository.GetCustomer(request.CustomerId);
}
}
}
|
2- CreateCustomerHandler
using ApiMediatR.Handlers.Requests;
using ApiMediatR.Models;
using ApiMediatR.Repositories;
using ApiMediatR.Services;
using MediatR;
using System;
using System.Threading;
using System.Threading.Tasks;
namespace ApiMediatR.Handlers
{
public class CreateCustomerHandler : IRequestHandler<CreateCustomerRequest, Guid>
{
private readonly IValidationService _validationService;
private readonly ICustomerRepository _customerRepository;
public CreateCustomerHandler(IValidationService validationService,
ICustomerRepository customerRepository)
{
_customerRepository = customerRepository;
_validationService = validationService;
}
public async Task<Guid> Handle(CreateCustomerRequest request, CancellationToken cancellationToken = default)
{
_validationService.Validate<Customer>(request.Customer);
return await _customerRepository.CreateCustomer(request.Customer);
}
}
}
|
Cada classe Handler implementada processa a mensagem do respectivo Request e em cada uma delas estamos fazendo o seguinte:
- Estamos injetando os serviços do repositório e validação
- O processamento esta sendo feito no método Handle definido na interface IRequestHandler só que agora temos classes separadas e independentes para cada método Action e cada uma usa as suas dependências;
- Assim estamos aderentes ao princípio SRP pois temos classes independentes que se comunicam com baixo acoplamento;
O resultado é que
o nosso controlador possui agora apenas dependência que a interface IMediator
e cada método Action agora só precisa traduzir seu modelo de entrada em um
comando enviado o respectivo Request usando o método
Send para o processamento no método
Handler.
Pegue o projeto aqui:
ApiMediatR.zip
"A palavra de
Cristo habite em vós abundantemente, em toda a sabedoria, ensinando-vos e
admoestando-vos uns aos outros, com salmos, hinos e cânticos espirituais,
cantando ao Senhor com graça em vosso coração."
Colossenses 3:16
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 - Macoratti
C# - Fundamentos : Definindo DateTime como Null ...
C# - Os tipos Nullable (Tipos Anuláveis) - Macoratti.net