loading...
Cover image for Playground: MediatR

Playground: MediatR

wsantosdev profile image William Santos ・14 min read

Olá!

Este é mais um post da seção Playground e, neste artigo, demonstraremos o uso de uma biblioteca muito conhecida pela comunidade .NET: o MediatR.

Apresentando o MediatR

MediatR é uma biblioteca inspirada no Mediator Pattern (em inglês), e atua como um sistema desacoplado de notificação. Ou seja, os objetos que o utilizam como meio de notificação desconhecem os consumidores da mensagem enviada, tendo conhecimento apenas do mediador.

Vamos entender um pouco sobre como o MediatR funciona com uma aplicação: a MediatR Drink Machine!

Para começar, você vai precisar de:

Construindo a aplicação

Nossa aplicação de exemplo será uma máquina de servir bebidas quentes, como aquelas que encontramos em escritórios ou lojas de conveniência. Ela vai oferecer apenas quatro tipos de bebiba (café, latte, cappuccino e chá), e apresentará um contador de doses restantes para cada bebiba. Como a intenção é demonstrar as formas mais básicas de uso do MediatR, tomamos o cuidado de deixar a aplicação o mais simples possível.

Para começar, vamos criar a infraestrutura da aplicação. Caso você opte pelo uso do Visual Studio, basta criar um projeto Asp.Net Core vazio. Caso esteja usando o VS Code, crie um projeto do tipo Web API e remova do projeto a pasta Controllers e o arquivo WheaterForecast.cs.

PS X:\code\playground-mediatr> dotnet new webapi -o Playground.MediatR.Cafe
Enter fullscreen mode Exit fullscreen mode

Criando nossas bebibas

Para a criação dos objetos que representam as bebidas disponíveis na máquina, vamos criar uma pasta chamada Models na raíz do projeto, um arquivo chamado Drinks.cs e inserir o seguinte conteúdo:

namespace Playground.MediatR.Cafe.Models
{
    public record Drink(string Name);

    public record Coffee : Drink { public Coffee() : base("Coffee") {} }
    public record Latte : Drink { public Latte() : base("Latte") {} }
    public record Cappuccino : Drink { public Cappuccino() : base("Cappuccino") {} }
    public record Tea : Drink { public Tea() : base("Tea") {} }
}
Enter fullscreen mode Exit fullscreen mode

Novidade! O C# 9, lançado oficialmente na semana de publicação deste artigo, oferece um novo tipo de objeto para representarmos elementos de nossa aplicação, os records. Um record nada mais é que uma classe, mas com algumas características interessantes: é imutável por padrão (ou seja, não pode ter suas propriedades alteradas depois de instanciado), oferece métodos de comparação baseados no valor de suas propriedades (assim como structs), e podem ser declarados de modo posicional (como no caso de Drink), o que significa que os parâmetros declarados no construtor serão traduzidos em propriedades. No caso de Drink, teríamos o seguinte:

var drink = new Drink("Refrigerante");
Console.Write(drink.Name); //Refrigerante
Enter fullscreen mode Exit fullscreen mode

Por ser uma classe, record oferece suporte a herança e, por isso, todas as nossas bebidas herdam de Drink, tendo a propriedade Name definida em seu construtor padrão.

Criando os dispensers

Agora que temos nossas bebidas, precisamos armazená-las para poder servi-las. Para tanto, vamos criar os dispensers de nossa máquina.

Ainda na pasta Models, crie uma pasta chamada Dispensers, um arquivo chamado AbstractDispenser.cs e cole o seguinte conteúdo:

using System;

namespace Playground.MediatR.Cafe.Models
{
    public abstract class AbstractDispenser
    {
        public int AvailableDoses { get; private set; } = 10;

        public Drink Dispense()
        {
            if (AvailableDoses <= 0)
                throw new InvalidOperationException("There is no available doses to dispense.");

            AvailableDoses--;

            return DispenseCore();
        }

        protected abstract Drink DispenseCore();
    }
}
Enter fullscreen mode Exit fullscreen mode

Assim como em uma máquina de verdade, nosso dispenser é apenas uma forma de armazenamento que desconhece a bebida que carrega e que, quando acionado, entrega uma dose. Por este motivo o criamos como uma classe abstrada. Assim, sua operação ganha a especificidade de servir cada bebida a partir de cada especialização.

Agora vamos conhecer um dispenser especializado. Ainda na pasta Dispensers, crie um arquivo chamado CoffeeDispenser.cs e cole o seguinte conteúdo:

namespace Playground.MediatR.Cafe.Models
{
    public class CoffeeDispenser : AbstractDispenser
    {
        protected override Drink DispenseCore() =>
            new Coffee();
    }
}
Enter fullscreen mode Exit fullscreen mode

Agora temos um dispenser que deverá servir café!

Novidade! Aqui temos mais uma novidade do C# 9, o covariant return types. Repare que o método sobrecarregado DispenseCore espera como retorno o tipo Drink. No entanto, retornamos o tipo Coffee que é extendido de Drink. O tipo de retorno covariante nos permite retornar não apenas o tipo declarado como retorno de um método, como também suas especializações. Desta forma é possível, por exemplo, dispensarmos uma interface IDrink como retorno do método abstrato, o que novamente nos faz economizar código!

Por brevidade, vou demonstrar apenas o dispenser de café. Todos os demais serão iguais, a exceção da bebida que servem. Portanto, ainda na pasta Dispenser, crie os seguintes arquivos: LatteDispenser.cs, CappuccinoDispenser.cs, e TeaDispenser.cs, cole o mesmo conteúdo de CoffeeDispenser alterando o nome da classe e o retorno do método DispenseCore de acordo com cada bebida.

Agora precisamos informar à máquina qual dispenser serve qual bebida, para que, ao apertarmos um botão escolhendo nossa bebida, a máquina saiba o que servir. Para isso, na pasta Models, crie o arquivo MachineSlot.cs e cole o seguinte conteúdo:

namespace Playground.MediatR.Cafe.Models
{
    public enum MachineSlot
    {
        Coffee = 1,
        Latte,
        Cappuccino,
        Tea
    }
}
Enter fullscreen mode Exit fullscreen mode

Agora nossa máquina sabe que o primeiro botão servirá café, e que os demais servirão as demais bebidas em ordem.

Acionando os dispensers

A partir de agora o MediatR entra em ação. Ele será responsável por expor os comandos disponíveis na máquina para acionar os dispensers e entregar as bebidas.

Crie, na pasta raíz do projeto, uma pasta chamada MediatR, em seguida um arquivo chamado Requests.cs, e cole o seguinte conteúdo:

using MediatR;
using Playground.MediatR.Cafe.Models;

namespace Playground.MediatR.Cafe.MediatR
{
    public record CoffeeRequest : IRequest;
    public record LatteRequest : IRequest;
    public record CappuccinoRequest : IRequest;
    public record TeaRequest : IRequest;

    public record AvailableDosesRequest : IRequest<AvailableDoses>;
}
Enter fullscreen mode Exit fullscreen mode

Repare que temos novamente a presença dos records, desta vez implementando a interface de marcação IRequest. Por ser uma interface que não oferece métodos, records acabam sendo mais convenientes que classes comuns para representar nossos comandos (principalmente porque comandos devem ser imutáveis), e tem essa sintaxe mais simples, que economiza várias linhas de código.

Esta interface IRequest é uma interface do MediatR que indica que um tipo é uma requisição, uma das duas formas de notificação disponíveis na biblioteca. Essas requisições podem ser comandos, requisições que não retornam valor (o caso das primeiras quatro requisições), ou consultas, onde o valor esperado como retorno é T em IRequest<T>, como no caso de AvailableDosesRequest.

Sobre AvailableDosesRequest, não nos preocupemos com ele por enquanto, mas ele será o responsável por informar ao cliente quantas doses de cada bebida temos disponíveis ao ligar a máquina. Em breve retornaremos à ele!

Agora, vamos criar uma pasta em MediatR chamada RequestHandlers, onde teremos as classes responsáveis por responder aos comandos enviados pelos botões da máquina. Dentro desta nova pasta, crie o arquivo DrinkRequestHandlerBase.cs, com o seguinte conteúdo:

using MediatR;
using Playground.MediatR.Cafe.Models;
using System.Threading.Tasks;

namespace Playground.MediatR.Cafe.MediatR
{
    public class DrinkRequestHandlerBase<TDispenser>
        where TDispenser : AbstractDispenser
    {
        private readonly IMediator _mediator;
        private readonly TDispenser _dispenser;

        public DrinkRequestHandlerBase(IMediator mediator, TDispenser dispenser) =>
            (_mediator, _dispenser) = (mediator, dispenser);

        public Task<Unit> Handle()
        {
            var drink = _dispenser.Dispense();

            _mediator.Publish(new DrinkDispensed(drink, _dispenser.AvailableDoses));

            return Task.FromResult(Unit.Value);
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Aqui vemos na prática o tipo de retorno covariante. Por conta desta funcionalidade, podemos receber uma especialização de AbstractDispenser e ativar seu método Dispense para trazer a bebida que precisamos.

Além disso, podemos ver a injeção da interface IMediatR, que é a responsável por invocar as funcionalidades de notificação da biblioteca. Neste caso, a utilizamos para a publicação de notificações, a segunda forma de notificação da biblioteca.

A publicação de notificações aqui é usada como uma forma de publicar um evento, o que indica que uma bebida foi servida (indicando qual bebida e quantas doses da mesma restam no dispenser). Com isso, temos como manter o cliente informado sobre o estado da máquina. Veremos a implementação do tratamento deste evento mais adiante, quando tratarmos da interação com o cliente. Por ora, vamos concluir o processo de servir a bebida escolhida pelo cliente.

Importante! Uma notificação de publicação tem uma relação 1:N com seus handlers. Ou seja, diversos objetos de uma aplicação podem reagir a este tipo de notificação, desacoplados de seu publicador, e esta funcionalidade corresponde à implementação do padrão Mediator.

Nota: Repare que no método Handle temos como tipo de retorno Unit. Este é um tipo do MediatR utilizado para sinalizar a ausência de um tipo de retorno para o método. É esse tipo que diferencia comandos de consulta, e é usado pela biblioteca como uma representação de void.

Ainda na pasta RequestHandlers, crie o arquivo CoffeeRequestHandler.cs e cole o seguinte conteúdo:

using MediatR;
using Playground.MediatR.Cafe.Models;
using System.Threading;
using System.Threading.Tasks;

namespace Playground.MediatR.Cafe.MediatR
{
    public class CoffeeRequestHandler : DrinkRequestHandlerBase<CoffeeDispenser>,
                                        IRequestHandler<CoffeeRequest>
    {
        public CoffeeRequestHandler(IMediator mediator, CoffeeDispenser dispenser) : base(mediator, dispenser) { }

        Task<Unit> IRequestHandler<CoffeeRequest, Unit>.Handle(CoffeeRequest request, CancellationToken cancellationToken) =>
            Handle();
    }
}
Enter fullscreen mode Exit fullscreen mode

Aqui informamos de fato ao MediatR que vamos tratar uma mensagem de requisição, por meio da implementação da interface IRequestHandler<CoffeeRequest>. Aqui estamos dizendo ao MediatR que, ao receber uma mensagem do tipo CoffeeRequest, este handler deverá ser acionado e a mensagem a ele encaminhada. Repare que nosso handler estende nosso handler abstrato, informando que o dispenser que será utilizado será o de café.

Importante! Ao contrário da manipulação de notificações, no caso do atendimento a requisições existe uma relação 1:1 entre o tipo da requisição e seu respectivo handler. Ou seja, não é possível que dois ou mais handlers trate um mesmo tipo de mensagem. A razão para esta restrição é consistência. Não faria sentido ter uma consulta de um mesmo tipo com dois resultados, tampouco um mesmo comando executado mais de uma vez, e potencialmente de formas diferentes!

Mais uma vez por brevidade, vamos demonstrar apenas o handler responsável por servir café. Para implementar os demais, basta criar os arquivos LatteRequestHandler.cs, CappuccinoRequestHandler.cs e TeaRequestHandler, substituindo os dispensers e tipos de requisição de acordo com a bebida servida.

Inicializando

Agora que vimos como servir nossas bebidas, vamos atender à requisição inicial da máquina quando ligada: a que busca a quantidade de doses disponíveis para cada bebida.

Ainda na pasta RequestHandlers, crie o arquivo AvailableDosesRequestHandler.cs, e cole o seguinte conteúdo:

using MediatR;
using Playground.MediatR.Cafe.Models;
using System.Threading;
using System.Threading.Tasks;

namespace Playground.MediatR.Cafe.MediatR
{
    public class AvailableDosesRequestHandler : IRequestHandler<AvailableDosesRequest, AvailableDoses>
    {
        private readonly CoffeeDispenser _coffeeDispenser;
        private readonly LatteDispenser _latteDispenser;
        private readonly CappuccinoDispenser _cappuccinoDispenser;
        private readonly TeaDispenser _teaDispenser;

        public AvailableDosesRequestHandler(CoffeeDispenser coffeeDispenser,
                                            LatteDispenser latteDispenser,
                                            CappuccinoDispenser cappuccinoDispenser,
                                            TeaDispenser teaDispenser)
        {
            _coffeeDispenser = coffeeDispenser;
            _latteDispenser = latteDispenser;
            _cappuccinoDispenser = cappuccinoDispenser;
            _teaDispenser = teaDispenser;
        }

        public Task<AvailableDoses> Handle(AvailableDosesRequest request, CancellationToken cancellationToken)
        {
            return Task.FromResult(new AvailableDoses(_coffeeDispenser.AvailableDoses,
                                                      _latteDispenser.AvailableDoses,
                                                      _cappuccinoDispenser.AvailableDoses,
                                                      _teaDispenser.AvailableDoses));
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Repare que acima temos uma implementação de IRequestHandler<AvailableDosesRequest, AvailableDoses>. O tipo AvailableDoses aqui indica que este será o tipo de retorno de nossa consulta quando uma requisição do tipo AvailableDosesRequest for enviada.

Agora todas as nossas requisições podem ser atendidas. Vamos ver como faremos para receber as requisições dos clientes para que as sejam.

Comunicando com o cliente

Para a comunicação como cliente, foi escolhido o uso do SignalR, biblioteca para comunicação em tempo real do Asp.Net Core. Caso não tenha familiaridade com a biblioteca, recomendo um artigo, também da série Playground, onde a exploramos.

Na pasta raíz do projeto, vamos criar uma pasta chamada Hubs, e criar o arquivo IDrinkMachineHub.cs com o seguinte conteúdo:

using Playground.MediatR.Cafe.Models;
using System.Threading.Tasks;

namespace Playground.MediatR.Cafe.Hubs
{
    public interface IDrinkMachineHub
    {
        Task Ready(AvailableDoses availableDoses);
        Task Serve(string drinkName, int remainingDoses);
    }
}
Enter fullscreen mode Exit fullscreen mode

Uma das facilidades oferecidas pelo SignalR é a implementação automática de métodos de envio de mensagens. Ou seja, não precisaremos implementar esta interface! O próprio SignalR vai implementá-la para nós automaticamente.

Agora, vamos criar o Hub que receberá as requisições do cliente. Ainda na pasta Hubs, crie o arquivo DrinkMachine.cs com o seguinte conteúdo:

using MediatR;
using Microsoft.AspNetCore.SignalR;
using Playground.MediatR.Cafe.MediatR;
using Playground.MediatR.Cafe.Models;
using System;
using System.Threading.Tasks;

namespace Playground.MediatR.Cafe.Hubs
{
    public class DrinkMachineHub : Hub<IDrinkMachineHub>
    {
        private readonly IMediator _mediator;

        public DrinkMachineHub(IMediator mediator) =>
            _mediator = mediator;

        public async Task GetAvailableDoses() =>
            await Clients.All.Ready(await _mediator.Send(new AvailableDosesRequest()));

        public async Task RequestDrink(int machineSlot)
        {
            switch((MachineSlot)machineSlot)
            {
                case MachineSlot.Coffee:
                    await _mediator.Send(new CoffeeRequest());
                    break;
                case MachineSlot.Latte:
                    await _mediator.Send(new LatteRequest());
                    break;
                case MachineSlot.Cappuccino:
                    await _mediator.Send(new CappuccinoRequest());
                    break;
                case MachineSlot.Tea:
                    await _mediator.Send(new TeaRequest());
                    break;
                default:
                    throw new ArgumentException("Invalid option.");
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Aqui temos novamente a injeção da interface IMediatR, desta vez para encaminhar nossas requisições. Temos, também, dois métodos, sendo o primeiro GetAvailableDoses o método invocado na inicialização da máquina para informar a quantidade de doses restantes de cada bebida. E o segundo, RequestDrink, pelo qual o cliente solicita sua bebida.

Nota: Repare que nos primeiro método invocamos um dos métodos declarados na interface IDrinkMachineHub, já contando com a implementação automática do SignalR para notificar os clientes conectados!

Agora que temos como nos comunicar com o cliente, vamos para a última parte da implementação de nosso backend, que servirá de fato a bebida e atualizará a quantidade de doses restantes após serví-la.

Na pasta MediatR, crie uma nova pasta chamada EventHandlers, e o arquivo DrinkDispensedEventHandler.cs com o seguinte conteúdo:

using MediatR;
using Microsoft.AspNetCore.SignalR;
using Playground.MediatR.Cafe.Hubs;
using Playground.MediatR.Cafe.Models;
using System.Threading;
using System.Threading.Tasks;

namespace Playground.MediatR.Cafe.MediatR
{
    public record DrinkDispensed(Drink Drink, int RemainingDoses) : INotification;

    public class DrinkDispensedEventHandler : INotificationHandler<DrinkDispensed>
    {
        private readonly IHubContext<DrinkMachineHub, IDrinkMachineHub> _hubContext;

        public DrinkDispensedEventHandler(IHubContext<DrinkMachineHub, IDrinkMachineHub> hubContext) =>
            _hubContext = hubContext;

        public async Task Handle(DrinkDispensed notification, CancellationToken cancellationToken) =>
            await _hubContext.Clients.All.Serve(notification.Drink.Name, notification.RemainingDoses);
    }
}
Enter fullscreen mode Exit fullscreen mode

Aqui, por comodidade, foi criado o tipo que representa a notificação DrinkDispensed e seu handler no mesmo arquivo, já que este será o único.

Aqui temos injetado o contexto de nosso Hub do SignalR, e o invocamos para notificar o mesmo evento ao cliente, enviando uma mensagem ao nosso frontend, que veremos como construir a partir de agora.

Construindo o frontend

Agora que temos nosso backend quase pronto, é hora de darmos atenção ao frontend, por onde o cliente solicitará suas bebidas. O primeiro passo para isso é criar uma pasta Pages na pasta raíz do projeto, e criar o arquivo Index.cshtml (uma Razor Page) com o seguinte conteúdo:

@page

<link rel="stylesheet" type="text/css" href="~/css/drink-machine.css" />

<div id="container">
    <div id="title">Welcome to the MediatR Drink Machine</div>

    <div id="separator"></div>
    <div><button id="buttonCoffee">Coffe</button></div>
    <div><button id="buttonLatte">Latte</button></div>
    <div><button id="buttonCappuccino">Cappuccino</button></div>
    <div><button id="buttonTea">Tea</button></div>

    <div id="separator"></div>
    <div><span id="spanServerResponse"></span></div>

    <div id="separator"></div>
    <div><span id="spanError"></span></div>
</div>

<script src="~/js/signalr.min.js"></script>
<script src="~/js/drink-machine.js"></script>
Enter fullscreen mode Exit fullscreen mode

Temos aqui uma estrutura bem simples. Um arquivo de estilos, o título, os botões pelos quais o cliente pedirá sua bebiba, e alguns spans para mensagens do servidor. Temos também a inclusão de um script JS com a biblioteca cliente do SignalR, seguido do que contém nossa implementação de controle do estado do frontend e comunicação com o backend.

Para servirmos os arquivos estáticos (css e js) precisamos inserí-los na pasta adequada. Para isso, vamos voltar à raíz de nosso projeto, criar a pasta wwwroot e, dentro dela, as pastas css e js.

Vamos agora criar o arquivo drink-machine.css, com o seguinte conteúdo:

body {
    font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}

#container {
    width: 600px;
    margin: auto;
    text-align: center;
}

#title {
    font-size: larger;
    margin-bottom: 10px;
}

#separator {
    margin-bottom: 5px;
}

#spanError {
    color: darkred;
    font-weight: bold;
}

button {
    font-size: large;
    cursor: pointer;
    border: 1px solid #CCC;
    border-radius: 10px;
    width: 250px;
    padding: 7px 0 7px 0;
    margin: 5px 0 5px 0;
}
Enter fullscreen mode Exit fullscreen mode

Em seguida, na pasta js, vamos criar o arquivo drink-machine.js, com o seguinte conteúdo:

"use strict";

(function () {
    var machineConn = new signalR.HubConnectionBuilder().withUrl("/machine").build();
    machineConn.serverTimeoutMilliseconds = 10000;

    var buttonCoffee = document.querySelector('#buttonCoffee');
    buttonCoffee.disabled = true;
    buttonCoffee.addEventListener("click", function (event) {
        machineConn.invoke("RequestDrink", 1);
    });

    var buttonLatte = document.querySelector('#buttonLatte');
    buttonLatte.disabled = true;
    buttonLatte.addEventListener("click", function (event) {
        machineConn.invoke("RequestDrink", 2);
    });

    var buttonCappuccino = document.querySelector('#buttonCappuccino');
    buttonCappuccino.disabled = true;
    buttonCappuccino.addEventListener("click", function (event) {
        machineConn.invoke("RequestDrink", 3);
    });

    var buttonTea = document.querySelector('#buttonTea');
    buttonTea.disabled = true;
    buttonTea.addEventListener("click", function(event) {
        machineConn.invoke("RequestDrink", 4);
    });

    var spanServerResponse = document.querySelector('#spanServerResponse');
    var spanError = document.querySelector('#spanError');

    machineConn.on("Ready", function (dosesPerSlot) {
        buttonCoffee.innerHTML = 'Coffee (' + dosesPerSlot.coffee + ' left)';
        if (dosesPerSlot.coffee > 0)
            buttonCoffee.disabled = false;

        buttonLatte.innerHTML = 'Latte (' + dosesPerSlot.latte + ' left)';
        if (dosesPerSlot.latte > 0)
            buttonLatte.disabled = false;

        buttonCappuccino.innerHTML = 'Cappuccino (' + dosesPerSlot.cappuccino + ' left)';
        if (dosesPerSlot.cappuccino > 0)
            buttonCappuccino.disabled = false;

        buttonTea.innerHTML = 'Tea (' + dosesPerSlot.tea + ' left)';
        if (dosesPerSlot.tea > 0)
            buttonTea.disabled = false;
    });

    machineConn.on("Serve", function (drinkName, remainingDoses) {
        spanServerResponse.innerHTML = 'Your ' + drinkName + ' is ready!';

        document.querySelector('#button' + drinkName).innerHTML = drinkName + ' (' + remainingDoses + ' left)';

        if (remainingDoses == 0)
            document.querySelector('#button' + drinkName).disabled = true;
    });

    machineConn.start()
        .then(function () {
            machineConn.invoke("GetAvailableDoses");
        })
        .catch(function (error) {
            spanError.innerHTML = 'Unable to connect to the server. Please press F5.';
        });

    machineConn.onclose(function (error) {
        spanError.innerHTML = 'Connection lost. Please press F5';
    })
})();
Enter fullscreen mode Exit fullscreen mode

Aqui temos nosso cliente. Nele especificamos que, ao carregar nossa página, construiremos e iniciaremos uma conexão com nosso backend a partir do SignalR. Nossos botões são configurados como desabilitados por padrão, sendo habilitados apenas após o atendimento à requisição de GetAvailableDoses, e desde que o método retorne uma quantidade de doses superior a zero à sua respectiva bebida.

Além disso, configuramos o evento click de nossos botões para acionar a máquina, solicitando a bebida correspondente. Temos métodos para tratar as mensagens recebidas do backend avisando que a bebida foi servida, atualizando o rótulo do botão com a quantidade de doses restantes e desabilitando-o caso não haja mais doses disponíveis. E, por fim, temos métodos de tratamento de falhas na conexão, que notificarão o cliente da necessidade de recarregar nossa página.

Nota: Por comodidade, a biblioteca do SignalR será inclusa no repositório da aplicação. Para maiores detalhes sobre como gerá-lo, acesse nosso artigo mencionado acima sobre SignalR.

Toques finais!

Nossa máquina já está quase pronta. Nos resta apenas configurar nosso backend para juntar todas as suas peças.

Abra o arquivo Startup.cs, na pasta raíz do projeto, e substitua seu conteúdo pelo seguinte:

using MediatR;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Playground.MediatR.Cafe.Hubs;
using Playground.MediatR.Cafe.Models;

namespace Playground.MediatR.Cafe
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMediatR(typeof(Startup));
            services.AddRazorPages();
            services.AddSignalR();

            services.AddSingleton<DrinkMachineHub>();

            services.AddSingleton<CoffeeDispenser>()
                    .AddSingleton<LatteDispenser>()
                    .AddSingleton<CappuccinoDispenser>()
                    .AddSingleton<TeaDispenser>();
        }

        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.UseHttpsRedirection();
            app.UseStaticFiles();
            app.UseRouting();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapHub<DrinkMachineHub>("/machine");
                endpoints.MapRazorPages();
            });
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Repare que não fazemos referência a quaisquer dos handlers acessados pelo MediatR. Isso porque quando invocamos o método services.MediatR(typeof(Startup)), a biblioteca de injeção de dependências do MediatR se encarrega obter junto ao contêiner de componentes do Asp.Net os tipos marcados com suas interfaces, carregando-os automaticamente quando necessário, reduzindo assim a quantidade de código necessário para configurá-lo para uso.

E lá vamos nós!

Se estiver tudo certo com seu código, ao executar a aplicação você terá o seguinte resultado:

Considerações finais

O MediatR é um excelente meio de notificar sua aplicação sobre requisições e eventos. Em aplicações complexas, onde haja o interesse de desacoplar seus controllers dos executores de comandos (ou consultas), ou mesmo caso queira apenas notificar eventos ocorridos em seu domínio, é uma ferramenta que facilita muito o trabalho!

Curiosidade: a biblioteca oferece também uma outra funcionalidade, chamada behaviors (em inglês), que implementa um pipeline de execução para seus handlers, mas entendemos que a mesma estava fora do escopo deste artigo. Podemos demonstrá-la em um artigo futuro se for de seu interesse (nos deixe saber pelos comentários).

Aqui você encontra o repositório do Github deste projeto. Fique à vontade para cloná-lo e modificá-lo como quiser!

Caso queira conhecer mais a fundo as novidades do C# 9, veja este artigo da Microsoft (em inglês). Ele ainda está em desenvolvimento, mas contém logo no início um índice com todas as mudanças. Vale a pena pesquisar para se familiarizar, são recursos excelentes!

Gostou? Se sim, me deixe saber pelos indicadores. Caso tenha chegado a este artigo por um post do Twiiter ou do LinkedIn, peço que compartilhe o post com seus amigos e contatos.

Por favor, deixe um feedback nos comentários. É muito importante para saber se este conteúdo está sendo útil pra você!

Muito obrigado, e até a próxima!

Discussion

pic
Editor guide