C# ASP .NET Padrões de Projeto .NET Core

Mediator Pattern com MediatR no ASP.NET Core

Manter os objetos isolados é o objetivo do Mediator Pattern. No ASP.NET Core a sua implementação é facilitada com a biblioteca MediatR.

há 3 anos 10 meses

Formação Full-stack: Desenvolvedor ASP.NET
Conheça a formação em detalhes

Para facilitar o desenvolvimento, a manutenção e manter o código limpo e legível, grandes aplicações procuram seguir princípios SOLID, padrões de projetos e outras recomendações de boas práticas, como a desacoplação dos objetos. Neste grupo de recomendações, vem ganhando espaço a adoção do Mediator Pattern, que no ASP.NET pode ser facilmente implementado com a biblioteca MediatR, como veremos neste artigo.

C# (C Sharp) Avançado
Curso C# (C Sharp) Avançado
Conhecer o curso

O que é Mediator Pattern?

Mediator é um padrão de projetos comportamental especificado pela GoF, que na sua definição formal é descrito como:

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 explicitamente e permite variar suas interações independentemente.

Em outras palavras podemos dizer que o Mediator Pattern gerencia as interações de diferentes objetos. Através de uma classe mediadora, centraliza todas as interações entre os objetos, visando diminuir o acoplamento e a dependência entre eles. Desta forma, neste padrão, os objetos não conversam diretamente entre eles, toda comunicação precisa passar pela classe mediadora.

Podemos ilustrar seu funcionamento com a imagem abaixo:

Representação Gráfica do Mediator Pattern. Há setas apontando e saindo do objeto Mediator para representar a comunicação.

Se um objeto, por exemplo, Objeto A, quiser se comunicar com outro, Objeto C, terá que passar pelo Mediator. Com isso, cada objeto pode focar apenas na sua responsabilidade e não precisa conhecer a estrutura do outro para realizar a comunicação. Se Objeto C for modificado, Objeto A não precisa tomar conhecimento disso, ou seja, cada objeto trabalha de forma independente e isolada.

Entretanto, se houver um grande fluxo de comunicação entre os objetos, o objeto mediador pode se tornar um gargalo para a aplicação. Para resolver isso, a solução comum é implementar o CQRS (Command Query Responsibility Segregation), em tradução livre, Segregação de Responsabilidade de Comando e Consulta.

CQRS vem ao resgate

O Command Query Responsibility Segregation, ou CQRS, é um padrão de projeto que separa as operações de leitura e escrita da base de dados em dois modelos: queries e commands. Os commands são responsáveis pelas ações que realizam alguma alteração na base de dados. Geralmente operações assíncronas que não retornam nenhum dado.

Já as queries são responsáveis pelas consultas, retornando objetos DTOs (Data Transfer Object) para que seja isolada do domínio.

Por gerarem o maior fluxo de informações da aplicação, as consultas podem ser otimizadas através de um serviço específico, como um servidor de cache. Do lado dos commands, o Mediator pode ser utilizado para gerenciar a comunicação dos objetos.

É por causa disso que a biblioteca MediatR implementa conceitos do CQRS, porém não vou me aprofundar nele, já que não é objetivo desse artigo.

O MediatR entra em cena

O MediatR é uma biblioteca open source criada por Jimmy Bogard, o mesmo criador da biblioteca AutoMapper. O seu objetivo é facilitar a implementação do Mediator Pattern em aplicações .NET. Com ela não precisamos nos preocupar com a criação da classe mediadora, pois já são fornecidas interfaces que facilitam a implementação do fluxo de comunicação entre os objetos.

Colocando a mão na massa

Agora que conhecemos os conceitos por trás da biblioteca MediatR, vamos para um exemplo de implementação em um projeto. Será o projeto de uma API simples com um CRUD de pessoas.

A primeira coisa a ser feita é a criação do projeto:

dotnet new webapi -n MediatRSample

Em seguida, iremos adicionar a biblioteca:

dotnet add package MediatR

Para uma aplicação ASP.NET Core, também é necessário instalar o pacote de injeção de dependência:

dotnet add package MediatR.Extensions.Microsoft.DependencyInjection

Com o projeto criado e as dependências adicionadas, vamos adicionar algumas pastas nele, para que o nosso código fique mais organizado. O projeto ficará com a seguinte estrutura:

Projeto ASP.NET Core, com uma pasta Application, que contém seis pastas: Commands, EventHandlers, Handlers, Models e Notifications.

Onde dentro da pasta Application, temos as pastas:

  • Commands: Onde serão definidos os objetos DTOs que representam uma ação a ser executada;
  • EventHandlers: Onde serão definidos objetos responsáveis por receber uma notificação gerada pelos Handlers;
  • Handlers: Onde serão definidos objetos responsáveis por receber as ações definidas pelos Commands;
  • Models: Onde serão definidas as entidades utilizadas pela aplicação;
  • Notifications: Onde serão definidos os objetos DTOs que representam notificações.

Vamos implementar as classes dessas pastas.

C# (C Sharp) Avançado
Curso C# (C Sharp) Avançado
Conhecer o curso

Especificando a classe domínio e o repositório da aplicação

Antes de implementar os objetos que farão uso da biblioteca MediatR, é necessário especificar a nossa classe de domínio, o model Pessoa:

public class Pessoa
{
    public int Id { get; set; }
    public string Nome { get; set; }
    public int Idade { get; set; }
    public char Sexo { get; set; }
}

Esta classe contém todas as propriedades que necessitamos para representar um objeto pessoa na nossa aplicação. Os commands que iremos definir serão baseados nela.

Por se tratar de um exemplo simples, esta aplicação não fará uso de um banco de dados, mas possuirá uma interface IRepository:

public interface IRepository<T>
{
    Task<IEnumerable<T>> GetAll();

    Task<T> Get(int id);

    Task Add(T item);

    Task Edit(T item);

    Task Delete(int id);
}

E uma classe repositório que salva os dados em uma coleção estática:

public class PessoaRepository : IRepository<Pessoa>
{
    private static Dictionary<int, Pessoa> pessoas = new Dictionary<int, Pessoa>();

    public async Task<IEnumerable<Pessoa>> GetAll(){
        return await Task.Run(() => pessoas.Values.ToList());
    }

    public async Task<Pessoa> Get(int id){
        return await Task.Run(() => pessoas.GetValueOrDefault(id));
    }

    public async Task Add(Pessoa pessoa){
        await Task.Run(() => pessoas.Add(pessoa.Id, pessoa));
    }

    public async Task Edit(Pessoa pessoa){
        await Task.Run(() =>
        {
            pessoas.Remove(pessoa.Id);
            pessoas.Add(pessoa.Id, pessoa);
        });
    }

    public async Task Delete(int id){
        await Task.Run(() => pessoas.Remove(id));
    }
}

Implementando o padrão Command

Para gerenciar as interações dos objetos, a biblioteca MediatR implementa o padrão Command. Este padrão específica um objeto que encapsula toda informação necessária para executar uma ação posterior.

É neste ponto que a biblioteca faz uso do CQRS, como explicado anteriormente. Como vimos, no CQRS as operações são separadas em queries e commands. A parte dos commands é uma aplicação do padrão Command, que na implementação do MediatR é composta de dois objetos: Command e Command Handler.

Os objetos Command definem solicitações que irão alterar o estado dos dados e que o sistema precisa realizar. Por ser imperativo e se tratar de uma ação que será executada apenas uma vez (por solicitação) é recomendado que esses objetos sejam nomeados com o verbo no imperativo: “cadastra”, “altera”, etc. Também se recomenda a adição de um tipo, e.g., CadastraPessoaCommand, AlteraPessoaCommand, etc.

Já os objetos Command Handler serão responsáveis por executar as ações definidas pelos objetos Command. É aqui que ficará centralizado grande parte da lógica da aplicação.

Vamos iniciar a nossa implementação deste padrão definindo as classes Command:

public class CadastraPessoaCommand : IRequest<string>
{
    public string Nome { get; set; }
    public int Idade { get; set; }
    public char Sexo { get; set; }
}
public class AlteraPessoaCommand : IRequest<string>
{
    public int Id { get; set; }
    public string Nome { get; set; }
    public int Idade { get; set; }
    public char Sexo { get; set; }
}
public class ExcluiPessoaCommand : IRequest<string>
{
    public int Id { get; set; }
}

Note que essas são classes POCO simples, que contém apenas os atributos necessários para executar a ação especificada. Todas implementam a interface IRequest da biblioteca MediatR.

Nesta interface genérica se especifica o tipo de dado que será retornado quando o command for processado. Também é atráves do uso desta interface que será possível vincular os commands com as classes Command Handlers. É desta forma que a biblioteca saberá qual objeto deve ser invocado quando uma solicitação for gerada.

As boas práticas recomendam que para cada objeto Command haja um objeto Command Handler, entretanto seria possível implementar um objeto Command Handler para lidar com todos os commands definidos na aplicação.

Para este exemplo vou seguir as boas práticas, então a aplicação conterá as três classes abaixo:

public class CadastraPessoaCommandHandler : IRequestHandler<CadastraPessoaCommand, string>
{
    private readonly IMediator _mediator;
    private readonly IRepository<Pessoa> _repository;
    public CadastraPessoaCommandHandler(IMediator mediator, IRepository<Pessoa> repository)
    {
        this._mediator = mediator;
        this._repository = repository;
    }

    public async Task<string> Handle(CadastraPessoaCommand request, CancellationToken cancellationToken)
    {
        var pessoa = new Pessoa { Nome = request.Nome, Idade = request.Idade, Sexo = request.Sexo };

        try {
            pessoa = await _repository.Add(pessoa);

            await _mediator.Publish(new PessoaCriadaNotification { Id = pessoa.Id, Nome = pessoa.Nome, Idade = pessoa.Idade, Sexo = pessoa.Sexo});

            return await Task.FromResult("Pessoa criada com sucesso");
        } catch(Exception ex) {
            await _mediator.Publish(new PessoaCriadaNotification { Id = pessoa.Id, Nome = pessoa.Nome, Idade = pessoa.Idade, Sexo = pessoa.Sexo });
            await _mediator.Publish(new ErroNotification { Excecao = ex.Message, PilhaErro = ex.StackTrace });
            return await Task.FromResult("Ocorreu um erro no momento da criação");
        }

    }
}
public class AlteraPessoaCommandHandler : IRequestHandler<AlteraPessoaCommand, string>
{
    private readonly IMediator _mediator;
    private readonly IRepository<Pessoa> _repository;
    public AlteraPessoaCommandHandler(IMediator mediator, IRepository<Pessoa> repository)
    {
        this._mediator = mediator;
        this._repository = repository;
    }

    public async Task<string> Handle(AlteraPessoaCommand request, CancellationToken cancellationToken)
    {
        var pessoa = new Pessoa { Id = request.Id, Nome = request.Nome, Idade = request.Idade, Sexo = request.Sexo };

        try {
            await _repository.Edit(pessoa);

            await _mediator.Publish(new PessoaAlteradaNotification { Id = pessoa.Id, Nome = pessoa.Nome, Idade = pessoa.Idade, Sexo = pessoa.Sexo, IsEfetivado = true});

            return await Task.FromResult("Pessoa alterada com sucesso");
        } catch(Exception ex) {
            await _mediator.Publish(new PessoaAlteradaNotification { Id = pessoa.Id, Nome = pessoa.Nome, Idade = pessoa.Idade, Sexo = pessoa.Sexo, IsEfetivado = false});
            await _mediator.Publish(new ErroNotification { Excecao = ex.Message, PilhaErro = ex.StackTrace });
            return await Task.FromResult("Ocorreu um erro no momento da alteração");
        }

    }
}
namespace MediatRSample.Application.Handlers
{
    public class ExcluiPessoaCommandHandler : IRequestHandler<ExcluiPessoaCommand, string>
    {
        private readonly IMediator _mediator;
        private readonly IRepository<Pessoa> _repository;
        public ExcluiPessoaCommandHandler(IMediator mediator, IRepository<Pessoa> repository)
        {
            this._mediator = mediator;
            this._repository = repository;
        }

        public async Task<string> Handle(ExcluiPessoaCommand request, CancellationToken cancellationToken)
        {
            try {
                await _repository.Delete(request.Id);

                await _mediator.Publish(new PessoaExcluidaNotification { Id = request.Id, IsEfetivado = true});

                return await Task.FromResult("Pessoa excluída com sucesso");
            } catch(Exception ex) {
                await _mediator.Publish(new PessoaExcluidaNotification { Id = request.Id, IsEfetivado = false });
                await _mediator.Publish(new ErroNotification { Excecao = ex.Message, PilhaErro = ex.StackTrace });
                return await Task.FromResult("Ocorreu um erro no momento da exclusão");
            }

        }
    }
}

Note que todos os command handlers implementam a interface IRequestHandler, nesta interface é especificado 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 que definimos as instruções que devem ser realizadas para aplicar a solicitação definida pelo command.

Após a solicitação ser atendida, é possível utilizar o método Publish() para emitir uma notificação para todo sistema. Onde o MediatR irá procurar por uma a classe que tenha implementado a interface INotificationHandler<tipo da notificacao> e invocar o método Handler() para processar aquela notificação, como veremos a seguir.

Implementando notificações

Na sua essência as solicitações command não retornam nenhuma informação, assim para ser informado que a solicitação foi finalizada com sucesso, ou não, pode ser implementada notificações.

Como vimos no tópico anterior, no método Handler de uma classe Command Handler, pode ser invocado o método Publish(), passando por parâmetro um objeto notificação. Todos os Event Handlers que estiverem “ouvindo” notificações do tipo do objeto “publicado” serão notificados e poderão processá-lo.

Assim, para implementar as notificações inicialmente é necessário definir os objetos notificação:

public class PessoaCriadaNotification : INotification
{
    public int Id { get; set; }
    public string Nome { get; set; }
    public int Idade { get; set; }
    public char Sexo { get; set; }
}
public class PessoaAlteradaNotification : INotification
{
    public int Id { get; set; }
    public string Nome { get; set; }
    public int Idade { get; set; }
    public char Sexo { get; set; }
    public bool IsEfetivado { get; set; }
}
public class PessoaExcluidaNotification : INotification
{
    public int Id { get; set; }
    public bool IsEfetivado { get; set; }
}
public class ErroNotification : INotification
{
    public string Excecao { get; set; }
    public string PilhaErro { get; set; }
}

Como as classes Commands, as notificações são classes POCO que contêm apenas os dados necessários para processar a informação. Para que a biblioteca reconheça o objeto dessas classes como uma notificação é importante que elas implementem a interface INotification.

Já a nossa classe Notification Handler irá “escutar” todas as notificações, pois todas serão apenas registradas no console:

public class LogEventHandler :
                            INotificationHandler<PessoaCriadaNotification>,
                            INotificationHandler<PessoaAlteradaNotification>,
                            INotificationHandler<PessoaExcluidaNotification>,
                            INotificationHandler<ErroNotification>
{
    public Task Handle(PessoaCriadaNotification notification, CancellationToken cancellationToken)
    {
        return Task.Run(() =>
        {
            Console.WriteLine($"CRIACAO: '{notification.Id} - {notification.Nome} - {notification.Idade} - {notification.Sexo}'");
        });
    }

    public Task Handle(PessoaAlteradaNotification notification, CancellationToken cancellationToken)
    {
        return Task.Run(() =>
        {
            Console.WriteLine($"ALTERACAO: '{notification.Id} - {notification.Nome} - {notification.Idade} - {notification.Sexo} - {notification.IsEfetivado}'");
        });
    }

    public Task Handle(PessoaExcluidaNotification notification, CancellationToken cancellationToken)
    {
        return Task.Run(() =>
        {
            Console.WriteLine($"EXCLUSAO: '{notification.Id} - {notification.IsEfetivado}'");
        });
    }

    public Task Handle(ErroNotification notification, CancellationToken cancellationToken)
    {
        return Task.Run(() =>
        {
            Console.WriteLine($"ERRO: '{notification.Excecao} \n {notification.PilhaErro}'");
        });
    }
}

A aplicação poderia definir quantas classes Notification Handlers forem necessárias. Por exemplo, além da classe acima poderia haver uma classe que enviaria um e-mail para o usuário informando que uma pessoa foi cadastrada. Outra que avisaria a equipe de suporte que um erro foi gerado.

Caso uma notificação seja “ouvida” por mais de um Notification Handlers, todos serão invocados quando a notificação for gerada.

Controller e configuração MediatR

Agora que os objetos do MediatR estão criados, podemos definir o nosso controller:

[ApiController]
[Route("[controller]")]
public class PessoaController : ControllerBase
{

    private readonly IMediator _mediator;
    private readonly IRepository<Pessoa> _repository;

    public PessoaController(IMediator mediator, IRepository<Pessoa> repository)
    {
        this._mediator = mediator;
        this._repository = repository;
    }

    [HttpGet]
    public async Task<IActionResult> Get()
    {
        return Ok(await _repository.GetAll());
    }

    [HttpGet("{id}")]
    public async Task<IActionResult> Get(int id)
    {
        return Ok(await _repository.Get(id));
    }

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

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

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

Note que neste controller estamos “injetando” a interface IMediator, isso foi feito para que seja possível enviar as solicitações dos nossos objetos command com o método Send que esta interface disponibiliza:

var response = await _mediator.Send(command);

Neste contexto, o IMediator será a classe mediadora que através do método Send chama os command handlers que definimos, com base no objeto passado.

Por fim, é necessário adicionar o MediatR como serviço da nossa aplicação no método ConfigureServices da classe Startup:

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers();
    services.AddMediatR(typeof(Startup));
    services.AddSingleton<IRepository<Pessoa>, PessoaRepository>();
}

Também repare que estamos registrando a nossa classe repositório para que ela seja “injetada” no nosso controller e nas classes Command Handlers.

E a mágica acontece

Com a nossa aplicação definida, podemos iniciá-la e testar os endpoints. Note que ao executar uma solicitação:

Tela no Postman, mostrando o exemplo de uma requisição POST para o endpoint “https://localhost:5001/pessoa”

Ela será registrada no console:

Print do terminal do Visual Studio Code mostrando o texto: “CRIACAO: ‘1 - Carlos - 21 - M’”

Todas as solicitações que realizam alguma alteração nos dados serão registradas:

Print do terminal do Visual Studio Code mostrando logs do endpoint

Você pode ver o código completo da aplicação no repositório dela no meu Github.

C++ Avançado
Curso C++ Avançado
Conhecer o curso

No episódio de hoje aprendemos…

Mesmo com o exemplo simples demonstrado neste artigo, é possível notar que o Mediator Pattern nos ajuda a manter os objetos do sistema totalmente isolados e independentes, cada um possuindo apenas a sua responsabilidade.

Com a biblioteca MediatR a implementação deste padrão em uma aplicação ASP.NET Core é facilitada. Mas é importante frisar que este o Mediator não deve ser utilizado em qualquer projeto, dependendo do caso, a “sobrecarga “ necessária para implementá-lo não compensará os benefícios que fornece.

Então como He-Man diria, utilize com sabedoria.

Autor(a) do artigo

Wladimilson M. Nascimento
Wladimilson M. Nascimento

Instrutor, nerd, cinéfilo e desenvolvedor nas horas vagas. Graduado em Ciências da Computação pela Universidade Metodista de São Paulo.

Todos os artigos

Artigos relacionados Ver todos