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 :

  1. Mensagens de Request/Reponse despachada para um único handler;
  2. Mensagens de Notificação despachada para múltiplos handlers;

Aqui temos dois componentes principais chamados de Request e Handler.

  1. Request → Representa a mensagem a ser processada;
  2. Handler → Faz o processamento de determinada(s) mensagen(s);

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 :

  1. Aqueles que não retornam um valor representados pela interface IRequest;
  2. E aqueles que retornam um valor, representados pela interface IRequest<T>;

Cada interface Request tem sua própria interface de Handler.  Assim temos  :

  1. IRequestHandler<T> - Para processamento que não retornam valor;
  2. IRequestHandler<T, U> - Para processamento que retorna um valor U;

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:


José Carlos Macoratti