Back-End

31 mai, 2019

Mediator Pattern com MediatR – ASP.NET Core 2.2

Publicidade

Hoje vamos falar do Mediator Pattern, um dos Design Patterns que vem ganhando espaço no ambiente de desenvolvimento e que possui o MediatR, uma biblioteca C# que simplifica e facilita sua implementação.

O Mediator Pattern cuida das interações entre diferentes objetos, fornecendo uma classe mediadora que coordena todas as interações entre os objetos, visando diminuir o acoplamento e a dependência entre eles e facilitando as manutenções.

Portanto, nenhum objeto conversa diretamente com outro – sempre um objeto utilizará a classe mediadora para conversar indiretamente com outros objetos.

Para exemplificar melhor, consideremos o seguinte cenário:

Objeto A, precisa conversar com Objeto B e também com o Objeto C. Porém, o Objeto A não conhece os outros dois; portanto, o Objeto A, manda uma mensagem para o mediador que, por sua vez, envia para o Objeto B e C.

Com esse padrão, cada objeto possui uma única responsabilidade e consegue se comunicar com outros objetos sem a necessidade de conhecê-los, ou seja, cada objeto, trabalha de forma independente e isolada, não havendo acoplamento entre eles.

Vantagens

  • 1. Desacoplamento entre os objetos, pois nenhum objeto se conhece na comunicação
  • 2. O fluxo de comunicação está centralizado. Com isso, alterações no mediador não afetam seus ouvintes
  • 3. Mudanças podem ser aplicadas facilmente nos objetos, pois são independentes

Desvantagens

  • 1. Dependendo da quantidade de informações a serem processadas, o mediador pode se tornar o gargalo da aplicação
  • 2. Maior complexidade de código

O que é MediatR? Por que utilizá-lo?

MediatR, como dito antes, é uma biblioteca criada por Jimmy Bogard (criador do AutoMapper), que facilita a implementação do Mediator Pattern.

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.

CQRS

Command Query Responsibility Segregation, ou CQRS, é um padrão de arquitetura de desenvolvimento de software que se resume a separar a leitura e a escrita em dois modelos: Query e Command, uma para leitura e outra para escrita de dados, respectivamente.

Em nosso projeto vamos utilizar um pouco desse conceito, porém, não vamos nos aprofundar, pois não é o objetivo deste artigo.

Show me the code!

Agora que conhecemos um pouco das vantagens, vamos para a parte da implementação do MediatR em nosso projeto, lembrando que para essa demonstração, vamos utilizar o Visual Studio 2017.

Para essa demonstração vamos criar um CRUD básico de clientes para facilitar o entendimento, portanto, vamos iniciar criando um novo projeto de API. Caso ainda não saiba, aqui tem um artigo em que explico detalhadamente.

Com o projeto criado, precisamos importar o pacote MediatR via Nuget em nosso projeto. Para isso, podemos utilizar o Package Manager Console que se encontra em Tools > Nuget Package Manager > Package Manager Console e vamos executar as seguintes instruções:

  • Install-Package MediatR
  • Install-Package MediatR.Extensions.Microsoft.DependencyInjection

Para assegurar que possuímos o pacote instalado, verifique se ele faz parte do projeto

Em nosso projeto vamos apenas criar as seguintes pastas e arquivos, conforme a imagem abaixo:

Agora vamos explicar a responsabilidade de cada classe:

  • O CustomerController é a classe controladora (controller) da nossa aplicação, Ou seja, onde vamos receber as requisições da aplicação e fornecer as respostas para cada requisição.
  • As classes CustomerCreateCommand, CustomerDeleteCommand e CustomerUpdateCommand são DTOs (Data Transfer Object) que representam a ação que a aplicação deve realizar ao utilizar esse objeto.
  • CustomerEntity representa a nossa entidade de domínio Cliente, portanto ela possui estado, comportamento e suas regras de negócio.
  • CustomerHandler tem a responsabilidade de coordenar as ações necessária para persistir a entidade no banco de dados, ou seja, é aqui onde fica a implementação do fluxo de dados, validações, entre outros.
  • EmailEvent é a classe responsável por receber a notificação do registro de um novo cliente e executar o fluxo de envio do e-mail.
  • ICustomerRepository é onde ficam os contratos (repositório) de acesso a dados. Notem que utilizei acesso a dados, portanto, não necessariamente precisa ser utilizado um banco de dados, e a classe CustomerRepository contém as implementações desses contratos .
  • CustomerActionEvent representa um DTO (Data Transfer Object) que contém informações sobre o registro persistido na base de dados, portanto, quando uma ação de persistência é executada com sucesso, esse objeto é preenchido e passado para as classes que estão esperando por esse objeto.
  • ActionNotification é uma enum que representa a ação daquela notificação como, por exemplo: Salvar, Atualizar e Excluir.

Vale ressaltar, que para cada command, caso necessite disparar eventos, posteriormente a alguma persistência ou alteração de dados, é necessário criar classes especialistas para cada notificação desejada, afinal, cada entidade é diferente de outra.

Porém, isso não é regra e não há problema nenhum em utilizar notificações genéricas para as entidades.

Vamos agora a implementação das nossas classes. Em nossa Startup.cs, adicionaremos o seguinte código:

using MediatorPatternExample.Infra;
using MediatR;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;

namespace MediatorPatternExample
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddSingleton<ICustomerRepository, CustomerRepository>();
            services.AddMediatR(typeof(Startup));
            services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.UseMvc();
        }
    }
}

Note que adicionamos o MediatR em nosso container de serviços para ser utilizado pela nossa aplicação com o uso do AddMediatR().

Outro ponto importante é que usamos a injeção de dependência (DI) em nossos repositórios com o uso do comando AddSingleton<ICustomerRepository, CustomerRepository>(), que nada mais faz do que criar uma única instância da classe CustomerRepository para compartilhar com toda a aplicação, quando invocamos os contratos da interface ICustomerRepository.

Antes de tudo, vamos implementar nossa classe de domínio, que é muito importante em nossa aplicação

namespace MediatorPatternExample.Domain.Customer.Entity
{
    public class CustomerEntity
    {
        public CustomerEntity(int id, string firstName, string lastName, string email, string phone)
        {
            Id = id;
            FirstName = firstName;
            LastName = lastName;
            Email = email;
            Phone = phone;
        }

        public int Id { get; private set; }
        public string FirstName { get; private set; }
        public string LastName { get; private set; }
        public string Email { get; private set; }
        public string Phone { get; private set; }
    }
}

Nossa entidade de domínio possui todas as informações referentes a cliente. Note também que nossas propriedades estão com private set, indicando que nenhuma outra classe de atribuir valor às propriedades, onde somente é possível atribuir através do construtor.

Com nossa classe de domínio finalizada, vamos para a implementação dos nossos commands.

using MediatR;

namespace MediatorPatternExample.Domain.Customer.Command
{
    public class CustomerCreateCommand : IRequest<string>
    {
        public int Id { get; set; }
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public string Email { get; set; }
        public string Phone { get; set; }
    }
}
using MediatR;

namespace MediatorPatternExample.Domain.Customer.Command
{
    public class CustomerDeleteCommand : IRequest<string>
    {
        public int Id { get; set; }
    }
}
namespace MediatorPatternExample.Domain.Customer.Command
{
    public class CustomerUpdateCommand : CustomerCreateCommand
    { }
}

Em todos os nossos commands, é herdada o IRequest, que é uma interface disponibilizada pelo mediatR, utilizada para indicar que esse é um comando utilizado por nossas classes Handlers, que vamos ver um pouco mais a frente.

Esses commands acima representam algum tipo de ação que deve ser executado (Create, Update e Delete).

Agora é a vez de implementar nossos repositórios de acesso a dados em nossa aplicação. Para isso, vamos realizar a implementação da nossa interface.

using MediatorPatternExample.Domain.Customer.Entity;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace MediatorPatternExample.Infra
{
    public interface ICustomerRepository
    {
        Task Save(CustomerEntity customer);
        Task Update(int id, CustomerEntity customer);
        Task Delete(int id);
        Task<CustomerEntity> GetById(int id);
        Task<IEnumerable<CustomerEntity>> GetAll();
    }
}

Em nossa interface definimos os métodos/assinatura (contratos) que estarão disponíveis para persistência e leitura de dados. Esse, inclusive, é um ponto chave. Quando definimos um contrato, não nos preocupamos de onde nossos dados serão disponibilizados ou onde serão armazenados.

Caso queira saber um pouco mais sobre o Repository Pattern, esse artigo vai te ajudar!

Com a implementação finalizada dos nossos métodos, é a vez da implementação concreta de cada contrato.

using MediatorPatternExample.Domain.Customer.Entity;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace MediatorPatternExample.Infra
{
    public class CustomerRepository : ICustomerRepository
    {
        public List<CustomerEntity> Customers { get; }

        public CustomerRepository()
        {
            Customers = new List<CustomerEntity>();
        }

        public async Task Save(CustomerEntity customer)
        {
            await Task.Run(() => Customers.Add(customer));
        }

        public async Task<IEnumerable<CustomerEntity>> GetAll()
        {
            return await Task.FromResult(Customers);
        }

        public async Task Update(int id, CustomerEntity customer)
        {
            int index = Customers.FindIndex(m => m.Id == id);
            if (index >= 0)
                await Task.Run(() => Customers[index] = customer);
        }

        public async Task Delete(int id)
        {
            int index = Customers.FindIndex(m => m.Id == id);
            await Task.Run(() => Customers.RemoveAt(index));        
        }

        public async Task<CustomerEntity> GetById(int id)
        {
            var result = Customers.Where(p => p.Id == id).FirstOrDefault();
            return await Task.FromResult(result);
        }
    }
}

Note que nossa persistência de dados acontece em uma lista, pois quando falamos em acesso à dados, podemos utilizar um arquivo txt, excel, database ou qualquer outra fonte que forneça dados para a aplicação e tudo isso abstraído em nossos repositórios. Fica Lindo!

Vamos agora para a parte de notificações do nosso sistema e, para isso, implementaremos um enum que representa a ação que o cliente sofreu naquela requisição.

namespace MediatorPatternExample.Notifications
{
    public enum ActionNotification
    {
        Criado = 1,
        Atualizado = 2,
        Excluido = 3
    }
}

Agora em nossa classe CustomerActionNotification, vamos implementar as seguintes propriedades:

using MediatR;

namespace MediatorPatternExample.Notifications
{
    public class CustomerActionNotification : INotification
    {
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public string Email { get; set; }
        public ActionNotification Action { get; set; }
    }
}

Nessa classe, temos a herança do INotification, outra interface disponibilizada pelo MediatR que indica que essa classe representa é uma notificação para ser processada por outras classes que estejam esperando por esse tipo de ação.

Perceba que nesse objeto temos também uma referência ao enum que acabamos de implementar, que é responsável por transmitir a ação daquela notificação.

Por fim, no EmailHandler vamos receber as notificações das ações que aconteceram com os registros dos clientes do nosso sistema.

using System;
using System.Threading;
using System.Threading.Tasks;
using MediatorPatternExample.Notifications;
using MediatR;

namespace MediatorPatternExample.EventsHandler
{
    public class EmailHandler : INotificationHandler<CustomerActionNotification>
    {
        public Task Handle(CustomerActionNotification notification, CancellationToken cancellationToken)
        {
            return Task.Run(() =>
            {
                Console.WriteLine("O cliente {0} {1} foi {2} com sucesso", notification.FirstName, notification.LastName, notification.Action.ToString().ToLower());
            });
        }
    }
}

Nessa classe, herdamos da interface INotificationHandler, disponibilizada também pelo MediatR, que nos obriga a implementar o método Handle, responsável por receber a notificação e executar a lógica de envio do e-mail.

Nesse caso, exibiremos apenas uma mensagem de console para facilitar o entendimento.

Portanto, sempre que emitimos alguma notificação referente a cliente em nosso sistema, nossa classe EmailHandler é responsável por receber essa notificação e processá-la.

Agora vamos realizar a implementação do nosso CustomerHandler:

using System.Threading;
using System.Threading.Tasks;
using MediatorPatternExample.Domain.Customer.Command;
using MediatorPatternExample.Domain.Customer.Entity;
using MediatorPatternExample.Infra;
using MediatorPatternExample.Notifications;
using MediatR;

namespace MediatorPatternExample.Domain.Customer.Handler
{
    public class CustomerHandler :
        IRequestHandler<CustomerCreateCommand, string>,
        IRequestHandler<CustomerUpdateCommand, string>,
        IRequestHandler<CustomerDeleteCommand, string>
    {
        private readonly IMediator _mediator;
        private readonly ICustomerRepository _customerRepository;

        public CustomerHandler(IMediator mediator, ICustomerRepository customerRepository)
        {
            _mediator = mediator;
            _customerRepository = customerRepository;
        }

        public async Task<string> Handle(CustomerCreateCommand request, CancellationToken cancellationToken)
        {
            var customer = new CustomerEntity(request.Id, request.FirstName, request.LastName, request.Email, request.Phone);
            await _customerRepository.Save(customer);

            await _mediator.Publish(new CustomerActionNotification
            {
                FirstName = request.FirstName,
                LastName = request.LastName,
                Email = request.Email,
                Action = ActionNotification.Criado
            }, cancellationToken);

            return await Task.FromResult("Cliente registrado com sucesso");
        }

        public async Task<string> Handle(CustomerUpdateCommand request, CancellationToken cancellationToken)
        {
            var customer = new CustomerEntity(request.Id, request.FirstName, request.LastName, request.Email, request.Phone);
            await _customerRepository.Update(request.Id, customer);

            await _mediator.Publish(new CustomerActionNotification
            {
                FirstName = request.FirstName,
                LastName = request.LastName,
                Email = request.Email,
                Action = ActionNotification.Atualizado
            }, cancellationToken);

            return await Task.FromResult("Cliente atualizado com sucesso");
        }

        public async Task<string> Handle(CustomerDeleteCommand request, CancellationToken cancellationToken)
        {
            var client = await _customerRepository.GetById(request.Id);
            await _customerRepository.Delete(request.Id);

            await _mediator.Publish(new CustomerActionNotification
            {
                FirstName = client.FirstName,
                LastName = client.LastName,
                Email = client.Email,
                Action = ActionNotification.Excluido
            }, cancellationToken);

            return await Task.FromResult("Cliente excluido com sucesso");
        }
    }
}

Note que possuímos uma herança do IRequestHandler que nos obriga a implementar o método Handler, responsável por receber os commands e orquestrar todo o fluxo de validação e regras de negócio da nossa entidade CustomerEntity.

Nessa classe possuímos três métodos Handlers, mas cada método representa uma ação diferente, já que recebemos commands diferentes nos parâmetros.

Outro ponto importante é sobre o método Publish(), responsável por emitir a notificação em todo sistema, onde ele vai procurar a classe que possui a herança da interface INotificationHandler<tipo do objeto> e invocar o método Handler() para processar aquela notificação.

Vale ressaltar que apenas invocará a classe responsável por um determinado tipo de informação. Ou seja, se a notificação é relacionada a um cliente, apenas notificações de cliente ela processará.

Por último, vamos implementar nossa CustomerController:

using MediatorPatternExample.Domain.Customer.Command;
using MediatorPatternExample.Infra;
using MediatR;
using Microsoft.AspNetCore.Mvc;
using System.Threading.Tasks;

namespace MediatorPatternExample.Controllers
{
    [ApiController]
    [Route("api/[controller]")]
    public class CustomerController : ControllerBase
    {
        private readonly IMediator _mediator;
        private readonly ICustomerRepository _customerRepository;

        public CustomerController(IMediator mediator, ICustomerRepository customerRepository)
        {
            _mediator = mediator;
            _customerRepository = customerRepository;
        }

        [HttpPost]
        public async Task<IActionResult> Post(CustomerCreateCommand command)
        {
            var response = await _mediator.Send(command);
            return Ok(response);
        }

        [HttpPut]
        public async Task<IActionResult> Put(CustomerUpdateCommand command)
        {
            var response = await _mediator.Send(command);
            return Ok(response);
        }

        [HttpDelete("{id}")]
        public async Task<IActionResult> Delete(int id)
        {
            var dto = new CustomerDeleteCommand { Id = id };
            var result = await _mediator.Send(dto);
            return Ok(result);
        }

        [HttpGet]
        public async Task<IActionResult> GetAll()
        {
            var result = await _customerRepository.GetAll();
            return Ok(result);
        }

        [HttpGet("{id}")]
        public async Task<IActionResult> Get(int id)
        {
            var result = await _customerRepository.GetById(id);
            return Ok(result);
        }
    }
}

Note que em nossa classe temos o IMediator, uma interface disponibilizada pelo MediatR que disponibiliza o método Send, responsável por enviar nosso comando para a classe que o executará.

O IMediator é a classe mediadora que através do método Send, chama os métodos da classe CustomerHandler com base no objeto passado.

Para exemplificar, quando recebemos no método Post o command CustomerCreateCommand, passamos esse objeto para o método Send, que por sua vez vai procurar alguma classe com herança do IRequestHandler e invocará o método Handler com base no objeto passado. Com isso, nosso Send encontra a classe CustomerHandler invocando o método correto.

Agora começou a encaixar as partes, né? Para facilitar, a imagem abaixo ilustra o fluxo de dados que ocorre em nosso sistema.

Tudo começa pelo nosso controlador (CustomerController), onde nossa aplicação receberá a requisição, passando para nosso mediador, representada pela interface IMediator a responsabilidade de invocar a classe que processará aquele objeto recebido, que por sua vez chamará o CustomerHandler, que é a classe responsável por processar a informação.

Em um dos métodos Handler do nosso CustomerHandler, acontecerá a persistência de dados, onde após essa ação, é criado um objeto de notificação e passado ao nosso mediador, que emite essa notificação em nosso sistema para as classes que vão processar essas notificações. No caso, o EmailService, que está esperando receber a notificação para processá-la e finalizando o fluxo de processamento daquele dado em nosso sistema.

Conclusão

O Mediator Pattern nos ajuda a manter nosso sistema flexível a mudanças e faz com que nossos objetos sejam totalmente independentes, cada um possuindo sua responsabilidade.

Utilizando a biblioteca MediatR, toda a comunicação entre os objetos está encapsulada e pronta para uso, fazendo com que a implementação desse padrão seja mais rápida e fácil.

Ficou com dúvida? Deixe nos comentários!

Até mais!

Link do projeto: https://github.com/reniciuspagotto/MediatorSample

Referências

  • Design Patterns: Elements of Reusable Object-Oriented Software
  • Github MediatR
  • Mediator Pattern — Macoratti
  • GoF Design Patterns