ASP .NET Core  - Implementando o Mediator com MediatR


Neste artigo vou mostrar outra abordagem de 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.

O padrão Mediator cuida das interações entre diferentes objetos, fornecendo uma classe mediadora que coordena todas as interações entre os objetos, com o objetivo de diminuir o acoplamento e a dependência entre eles e facilitando as manutenções. Assim, usando o padrão nenhum objeto conversa diretamente com outro, sempre um objeto utilizará a classe mediadora para conversar indiretamente com outros objetos.

Dessa forma cada objeto possui uma única responsabilidade e consegue se comunicar com outros objetos sem a necessidade de conhece-los, pois cada objeto, trabalha de forma independente e isolada, não existindo acoplamento entre eles.

Podemos destacar as seguintes vantagens em usar este padrão:

  1. Desacoplamento entre os objetos, visto que nenhum objeto se conhece na comunicação;
  2. O fluxo de comunicação é centralizado, com isso, alterações no mediador não afetam seus ouvintes;
  3. As alterações podem ser aplicadas facilmente nos objetos, pois são eles independentes;

Uma desvantagem é que a complexidade do código aumenta e havendo muita informação a ser processada o Mediator pode se tornar o gargalo da sua aplicação afetando o desempenho.

E onde entra o MediatR ?

A MediatR é uma biblioteca criada por Jimmy Bogard (criador do AutoMapper) que facilita a implementação do padrão Mediator.

Com ela não precisamos nos preocupar em desenvolver a classe mediadora de comunicação entre os objetos, pois ela fornece interfaces prontas para uso, diminuindo a complexidade do código. Além disso, essa biblioteca traz conceitos do CQRS em nosso código.

O CQRS - Command Query Responsibility Segregation, é um padrão de arquitetura de desenvolvimento de software que permite realizar a separação de leitura e escrita em dois modelos: Query e Command, uma para leitura e outra para escrita de dados, respectivamente.

Neste artigo faremos uma implementação deste padrão em uma aplicação ASP .NET Core utilizando a biblioteca MediatR 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.

Porque usar o CQRS ?

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 :
  1. Queries - São responsáveis pelas leituras ou consultas e retornam objetos DTOs ou ViewModels e não alteram o estado dos dados;
  2. Commands - São responsáveis pelas ações que realizam alguma alteração na base de dados e não retornam nada.(operações para incluir, alterar e deletar). Para gerenciar a comunicação entre os objetos nos Commands usamos o Mediator.

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.

Vamos realizar um CRUD básico usando uma entidade Student que representa o nosso modelo de domínio.

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 Aspn_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 comandoInstall-Package <nome> --version X.X.X

Ao final, verificando os pacotes instalados no projeto deveremos obter o resultado abaixo:

Organizando o projeto

Para organizar o nosso projeto vamos criar algumas pastas onde serão criados os artefatos da implementação do MediatR em nossa aplicação.

Vamos criar a partir da raiz do projeto as seguintes pastas e sub-pastas:

  • Domain
    • Student
      • Command
      • Entity
      • Handler
  • EventsHandlers
  • Infrastructure
  • Notifications
 

Onde :

Configurando o MediatR e criando o domínio

No arquivo Startup.cs vamos incluir o registro da biblioteca MediatR no método ConfigureServices:

public void ConfigureServices(IServiceCollection services)
{
            services.AddMediatR(typeof(Startup));
            services.AddControllers();
}

A seguir vamos criar a classe StudentEntity na pasta Domain/Student do projeto:

    public class StudentEntity
    {
        public StudentEntity(int id, string firstName, string lastName, string email)
        {
            Id = id;
            FirstName = firstName;
            LastName = lastName;
            Email = email;
        }
        public int Id { get; private set; }
        public string FirstName { get; private set; }
        public string LastName { get; private set; }
        public string Email { get; private set; }
    }

A classe StudentEntity representa a nossa entidade de domínio e esta definida com 4 propriedades onde definimos os setters como private e criamos um construtor. Assim nenhuma outra classe pode atribuir valor às propriedades e tem que fazer isso usando o construtor.

Nota: Deixamos essa entidade sem comportamentos para simplificar o código.

Criando os Commands

Agora na pasta Domain/Student/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. Command - Define ações que irão alterar o estado dos dados e os objetos;
  2. Command Handler - São responsáveis por executar as ações definidas pelos objetos Command;

1- StudentCreateCommand

using MediatR

public class StudentCreateCommand : IRequest<string>
{
        public int Id { get; set; }
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public string Email { get; 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- StudentDeleteCommand

using MediatR

public class StudentDeleteCommand : IRequest<string>
{
        public int Id { get; set; }
}

3- StudentUpdateCommand

using MediatR

public class StudentUpdateCommand : StudentCreateCommand     
{ }

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

Vamos agora implementar o repositório de acesso a dados, vamos iniciar criando uma interface IStudentRepository e a seguir implementar esta interface através da classe StudentRepository.

Vamos criar na pasta Infrastructure a interface IStudentRepository

1- IStudentRepository

using Aspn_MediatR1.Domain.Student.Entity;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace Aspn_MediatR1.Infrastructure
{
    public interface IStudentRepository
    {
        Task Save(StudentEntity student);
        Task Update(int id, StudentEntity student);
        Task Delete(int id);
        Task<StudentEntity> GetById(int id);
        Task<IEnumerable<StudentEntity>> GetAll();
    }
}

Nesta interface definimos o contrato que deverão ser implementados para leitura e persistência de dados. Observe que estamos definindo um repositório assíncrono usando Task como tipo de retorno.

A seguir vamos criar na mesma pasta a classe StudentRepository que implementa esta interface:

2 - StudentRepository

using Aspn_MediatR1.Domain.Student.Entity;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Aspn_MediatR1.Infrastructure
{
    public class StudentRepository : IStudentRepository
    {
        public List<StudentEntity> Students { get; }
        public StudentRepository()
        {
            Students = new List<StudentEntity>();
        }
        public async Task Delete(int id)
        {
            int index = Students.FindIndex(m => m.Id == id);
            await Task.Run(() => Students.RemoveAt(index));
        }
        public async Task<IEnumerable<StudentEntity>> GetAll()
        {
            return await Task.FromResult(Students);
        }
        public async Task<StudentEntity> GetById(int id)
        {
            var result = Students.Where(p => p.Id == id).FirstOrDefault();
            return await Task.FromResult(result);
        }
        public async Task Save(StudentEntity student)
        {
            await Task.Run(() => Students.Add(student));
        }
        public async Task Update(int id, StudentEntity student)
        {
            int index = Students.FindIndex(m => m.Id == id);
            if (index >= 0)
                await Task.Run(() => Students[index] = student);
        }
    }
}

Observe que estamos realizando o acesso e a persistência de dados em uma lista. Estamos assim abstraindo o banco de dados.

Agora precisamos registrar o serviço do repositório no método ConfigureServices:

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers();
    services.AddMediatR(typeof(Startup));
    services.AddSingleton<IStudentRepository, StudentRepository>();
}
 

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.

Para simplificar vamos implementar uma enumeração para representar a ação que foi executada pela requisição criando a enum ActionNotification na pasta Notifications do projeto:

 public enum ActionNotification
 {
        Created = 1,
        Updated = 2,
        Deleted = 3
 }

A seguir vamos criar a classe StudentActionNotification que implementa a interface INotification.

1- StudentActionNotification

using MediatR;
namespace Aspn_MediatR1.Notifications
{
    public class StudentActionNotification : INotification
    {
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public string Email { get; set; }
        public ActionNotification Action { get; set; }
    }
}

A interface INotification vem da biblioteca MediatR que indica que nossa classe representa uma notificação a ser processada por outras classes que esperam pela ação.

Para eventuais erros vamos criar a classe ErroNotification onde teremos o erro e a pilha de erro:

2- ErroNotification

using MediatR;
namespace Aspn_MediatR1.Notifications
{
    public class ErroNotification : INotification
    {
        public string Erro { get; set; }
        public string PilhaErro { get; set; }
    }
}

Precisamos criar uma classe do tipo Notification Handler que deverá “escutar” todas as notificações, pois todas serão apenas registradas no console.

Assim, vamos criar a classe SucessoHandler  na pasta EventsHandlers que vai receber as notificações que ocorreram com as informações dos clientes na aplicação nas operações.

2- SucessoHandler

using MediatR;
using Aspn_MediatR1.Notifications;

using System;
using System.Threading;
using System.Threading.Tasks;

namespace Aspn_MediatR1.EventsHandlers
{
    public class SucessoHandler : INotificationHandler<StudentActionNotification>
    {
        public Task Handle(StudentActionNotification notification, CancellationToken cancellationToken)
        {
            return Task.Run(() =>
            {
                Console.WriteLine("The student {0} {1} was {2} successfully", notification.FirstName, notification.LastName, notification.Action.ToString().ToLower());
            });
        }
    }
}

Nesta classe implementamos a interface INotificationHandler do pacote MediatR, e o método Handle é responsável por receber a notificação e executar a lógica para exibir a mensagem no Console. Poderíamos ter implementado qualquer outra forma de aviso ao cliente como um enviar um email, um sms , etc.

Assim esta classe será responsável por receber e processar qualquer notificação que for emitida referente ao estudante no sistema.

Criando os Command Handlers

Para cada objeto Command podemos ter um objeto Command Handler e assim podemos criar classes distintas para cada comando ou criar uma única classe onde vamos definir todos os comandos.

Vamos criar uma classe StudentCommandHandler, na pasta Domain/Student/Handler, e implementar a interface IRequestHandler e criar os Command Handlers na nesta classe.

1- StudentCommandHandler

using Aspn_MediatR1.Domain.Student.Command;
using Aspn_MediatR1.Domain.Student.Entity;
using Aspn_MediatR1.Infrastructure;
using Aspn_MediatR1.Notifications;
using MediatR;
using System.Threading;
using System.Threading.Tasks;
namespace Aspn_MediatR1.Domain.Student.Handler
{
    public class StudentCommandHandler : IRequestHandler<StudentCreateCommand, string>,
        IRequestHandler<StudentUpdateCommand, string>,
        IRequestHandler<StudentDeleteCommand, string>
    {
        private readonly IMediator _mediator;
        private readonly IStudentRepository _studentRepository;
        public StudentCommandHandler(IMediator mediator, IStudentRepository studentRepository)
        {
            _mediator = mediator;
            _studentRepository = studentRepository;
        }
        public async Task<string> Handle(StudentCreateCommand request, CancellationToken cancellationToken)
        {
            var student = new StudentEntity(request.Id, request.FirstName, request.LastName, request.Email);
            await _studentRepository.Save(student);
            await _mediator.Publish(new StudentActionNotification
            {
                FirstName = request.FirstName,
                LastName = request.LastName,
                Email = request.Email,
                Action = ActionNotification.Created
            }, cancellationToken);
            return await Task.FromResult("Student save successfully");
        }
        public async Task<string> Handle(StudentUpdateCommand request, CancellationToken cancellationToken)
        {
            var student = new StudentEntity(request.Id, request.FirstName, request.LastName, request.Email);
            await _studentRepository.Update(request.Id, student);
            await _mediator.Publish(new StudentActionNotification
            {
                FirstName = request.FirstName,
                LastName = request.LastName,
                Email = request.Email,
                Action = ActionNotification.Updated
            }, cancellationToken);
            return await Task.FromResult("Student updated successfully");
        }
        public async Task<string> Handle(StudentDeleteCommand request, CancellationToken cancellationToken)
        {
            var client = await _studentRepository.GetById(request.Id);
            await _studentRepository.Delete(request.Id);
            await _mediator.Publish(new StudentActionNotification
            {
                FirstName = client.FirstName,
                LastName = client.LastName,
                Email = client.Email,
                Action = ActionNotification.Deleted
            }, cancellationToken);
            return await Task.FromResult("Student deleted successfully");
        }
    }
}

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 StudentsController e clique em Add.

Ao final será criado o controlador onde vamos incluir o código abaixo:

using Aspn_MediatR1.Domain.Student.Command;
using Aspn_MediatR1.Infrastructure;
using MediatR;
using Microsoft.AspNetCore.Mvc;
using System.Threading.Tasks;
namespace Aspn_MediatR1.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class StudentsController : ControllerBase
    {
        private readonly IMediator _mediator;
        private readonly IStudentRepository _studentRepository;
        public StudentsController(IMediator mediator, IStudentRepository studentRepository)
        {
            _mediator = mediator;
            _studentRepository = studentRepository;
        }
        [HttpPost]
        public async Task<IActionResult> Post(StudentCreateCommand command)
        {
            var response = await _mediator.Send(command);
            return Ok(response);
        }
        [HttpPut]
        public async Task<IActionResult> Put(StudentUpdateCommand command)
        {
            var response = await _mediator.Send(command);
            return Ok(response);
        }
        [HttpDelete("{id}")]
        public async Task<IActionResult> Delete(int id)
        {
            var dto = new StudentDeleteCommand { Id = id };
            var result = await _mediator.Send(dto);
            return Ok(result);
        }
        [HttpGet]
        public async Task<IActionResult> GetAll()
        {
            var result = await _studentRepository.GetAll();
            return Ok(result);
        }
        [HttpGet("{id}")]
        public async Task<IActionResult> Get(int id)
        {
            var result = await _studentRepository.GetById(id);
            return Ok(result);
        }
    }
}

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 para chamar os comand handlers definidos.

Dessa forma quando recebemos no método Post o comando StudenteCreateCommand passamos o objeto para o método Send, que por sua vez vai procurar alguma classe que herda de IRequestHandler e vai invocar o método Handler com base no objeto passado, e com isso, o método Send encontra a classe StudentCommandHandler invocando o respectivo método.

A figura abaixo ilustra o fluxo de dados :

Para testar podemos simplesmente definir alguns estudantes em memória no Repositório StudentRepository:

  public class StudentRepository : IStudentRepository
 {
        private List<StudentEntity> Students = new List<StudentEntity>();
        public StudentRepository()
        {
            Students = GetStudents();
        }
        public List<StudentEntity> GetStudents()
        {
            Students.Add(new StudentEntity(1, "Pedro","Soares","pedro@email.com"));
            Students.Add(new StudentEntity(2, "Maria","Sanches","maria@email.com"));
            Students.Add(new StudentEntity(3, "Manuel","Ribeiro","manul@email.com"));
            return Students;
        }
   ...
}

E podemos alterar o arquivo launchSettings.json definindo a url inicial de acesso para : api/students

...
"profiles": {
    "IIS Express": {
      "commandName": "IISExpress",
      "launchBrowser": true,
      "launchUrl": "api/students",
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      }
    },
    "AspNet_MediatR1": {
      "commandName": "Project",
      "launchBrowser": true,
      "launchUrl": "api/students",
      "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

3-Fazendo um PUT

4- Fazendo um DELETE

O projeto completo aqui:   Aspn_MediatR1.zip  (sem as referências)

"(Disse Jesus) Ainda um pouco, e o mundo não me verá mais, mas vós me vereis; porque eu vivo, e vós vivereis."
João 14:19

Referências:


José Carlos Macoratti