🇧🇷 Commands + Domain Events + Real time notification na Prática

Arley Pádua
6 min readSep 23, 2018

Seguindo a minha série de posts sobre sistemas distribuídos em que começo explicando os princípios na modelagem de serviços, também explicando o porquê eles devem ser resilientes e a importância da implementação de domain events, após alguns feedbacks do pessoal resolvi compartilhar um pouco de prática associada aos conceitos mencionados nos posts.

Obs.: No final do post, há um link com a solução completa.

Para esse post prático, vou demonstrar um sistema muito simples composto de:

  • Uma API que aceita pedidos contendo nome do cliente e lista de itens de um pedido.
  • Um frontend simples usado somente para o envio de pedidos e receber notificações do backend.
  • Um projeto de backend, responsável por processar o pedido e publicar domain events.
  • Para a abstração de commands e domain events vou usar a biblioteca MediatR, muito fácil e intuitiva para o exemplo.
  • Para o envio de notificações para o frontend, vou usar SignalR, uma vez que ele oferece uma maneira fácil de trabalhar com websockets.

Veja na imagem abaixo os conceitos abordados nesse exemplo:

Começando a partir da API, crie um projeto com ASP.NET Core 2.1 e adicione o NuGet package: MediatR.Extensions.Microsoft.DependencyInjection.

Ele conterá tudo que é necessário para configurar o MediatR como nosso message broker.

É importante observar que o MediatR é uma biblioteca que oferece abstrações de envio de comandos e publicação de eventos, porém tudo roda em memória. Em um caso real, você deve adotar um sistema de transporte de mensagens, como Azure Service Bus ou RabbitMQ. Eles garantem que as mensagens não se percam, mesmo que seu servidor caia.

Crie uma controller chamada OrderController e adicione um endpoint conforme abaixo:

[Route("api/[controller]")]
public class OrdersController : Controller
{
private readonly IMediator _mediator;
public OrdersController(IMediator mediator)
{
_mediator = mediator;
}
[HttpPost]
public Task PostAsync([FromBody]PlaceOrderCommand order) =>
_mediator.Send(order);
}

Veja que não há muito segredo aqui e nossa controller está bem simples.

Um comando nunca deve ser enviado sem estar válido, porém, para fins de simplicidade do exemplo, não vou implementar as validações. Caso queira olhar mais a fundo, veja o wiki do MediatR para behaviors. Com eles é possível criar um pipeline de execução antes que o comando seja enviado e implementar sua validação lá.

Note que o método PostAsync recebe como parâmetro uma DTO chamada de PlaceOrderCommand, representando a ação do endpoint. Veja a sua definição:

public class PlaceOrderCommand : IRequest
{
public string CustomerName { get; set; }
public OrderLine[] OrderLines { get; set; }
public class OrderLine
{
public string ProductName { get; set; }
public int Quantity { get; set; }
}
}

Um simples DTO contendo o nome do cliente e os produtos requisitados.

Note que para que seja possível enviar o comando através da API do MediatR, é necessário implementar a interface IRequest.

Até agora temos uma API que aceita requisições HTTP e redireciona as requisições como comandos para o nosso message broker. Agora precisamos implementar o worker responsável por processar essas mensagens.

Para isso, crie uma classe chamada PlaceOrderRequestHandler implementando a interface IRequestHandler<PlaceOrderCommand>.

public class PlaceOrderRequestHandler :
IRequestHandler<PlaceOrderCommand>
{
private readonly IMediator _mediator;
public PlaceOrderRequestHandler(IMediator mediator)
{
_mediator = mediator;
}
public async Task<Unit> Handle(PlaceOrderCommand request,
CancellationToken cancellationToken)
{
// Manipulate your domain object
// Persist it
// Publish events
// Wait for the matter of the example
await Task.Delay(2000, cancellationToken);
await _mediator.Publish(new OrderPlacedEvent { OrderId =
Guid.NewGuid() }, cancellationToken);
return Unit.Value;
}
}

Veja que a interface IRequestHandler, pede um tipo genérico significando que tipo de mensagem esse worker vai processar, no nosso caso processar mensagens do tipo PlaceOrderCommand.

O interessante é que o MediatR será o responsável por descobrir essa classe, instanciar e cuidar da injeção de dependência. Fazendo com que a nossa API não precise conhecer nenhum detalhe de quem vai processar o comando.

O método Handle será executado assim que o MediatR identificar um comando. Essa parte, geralmente é responsável por recuperar o agregado/entidade do domínio, modificá-lo e publicar eventos.

No método Handle um processo longo de criação do pedido e o evento de pedido criado (OrderPlacedEvent) é publicado contendo o ID do pedido.

Nossa parte de realizar pedido está pronta, mas antes de rodar, precisamos configurar o MediatR na nossa aplicação. Para isso vá até o método ConfigureServices da classe Startup.cs e adicione na última linha o seguinte:

services.AddMediatR();

Agora faça o debug do projeto e veja o resultado chamando a API com o Postman:

Voltando Ă  primeira imagem do post, precisamos implementar o caminho de volta e notificar o cliente que um pedido foi criado.

Para isso precisamos criar:

  • Um endpoint de websocket, para que clientes possam se conectar e receber mensagens do servidor
  • Um worker que ficará escutando o evento OrderPlacedEvent e enviando mensagens para os clientes.

Para criar o websocket, vamos utilizar o ASP.NET Core SignalR. A configuração é bem simples: ainda na classe Startup.cs, adicione o seguinte no final do método ConfigureServices:

services.AddSignalR();

E no final do método Configure adicione o seguinte:

app.UseSignalR(routes =>
{
routes.MapHub<OrderingEventsClientHub>("/ordering-events");
});

Esse último, configura o middleware responsável por mapear as rotas disponíveis para o nosso websocket. Para o nosso exemplo, só estamos configurando a rota “/ordering-events” para o Hub “OrderingEventsClientHub”.

O Hub é uma representação no SignalR do canal de comunicação entre os clients e o backend, para isso, crie a classe OrderingEventsClientHub:

public class OrderingEventsClientHub : Hub { }

Perceba que ela está vazia, pois ela está representando somente um websocket sem nenhum método definido. Sua única função é abrir uma porta de comunicação entre o backend e o client.

Tendo criado o Hub, precisamos de criar o responsável por escutar os eventos de pedido criado e enviá-los ao client.

public class OrderingEventsClientDispatcher :
INotificationHandler<OrderPlacedEvent>
{
private readonly IHubContext<OrderingEventsClientHub>
_hubContext;
public OrderingEventsClientDispatcher(
IHubContext<OrderingEventsClientHub> hubContext)
{
_hubContext = hubContext;
}
public Task Handle(OrderPlacedEvent @event,
CancellationToken cancellationToken)
{
return _hubContext.Clients.All.SendAsync("orderPlaced",
@event, cancellationToken);
}
}

Essa classe implementa a interface INotificationHandler, fornecida pelo MediatR. Quando o MediatR reconhecer que um evento OrderPlacedEvent foi publicado, essa classe será usada e o método Handle será executada.

No construtor da classe, o serviço IHubContext<T> é injetado. Ele é um serviço fornecido pelo SignalR e fornece métodos de comunicação do backend para os clients.

Note que no método Handle, estou usando o contexto para redirecionar o evento para todos os clientes.

Os clientes poderão escutar esse evento através do identificador “orderPlaced” passado como primeiro parâmetro.

Obs.: Para fins de simplicidade do exemplo, estou enviando o evento para todos os clientes. Em um cenário real, o evento deveria ser enviado apenas para clients que estão interessados nesse evento, como por exemplo, algum usuário em específico. O SignalR fornece uma API bem coerente para usuários através do ClaimsPrincipal usado no ASP.NET.

Para finalizar o exemplo, precisamos criar um cliente que possa consumir esses eventos, para isso, substitua a página “Index.cshtml” pelo seguinte:

@page
@model IndexModel
@{
ViewData["Title"] = "Home page";
}
<div class="row">
<div class="col-md-12">
<button id="placeOrder" class="btn btn-info">Place Order</button>
</div>
</div>
<div class="row">
<div class="col-md-12">
<ul id="event-list"></ul>
</div>
</div>
<script src="~/lib/signalr/dist/browser/signalr.js"></script>
<script src="~/js/ordering.js"></script>

Essa página bem simples contém um botão, que será utilizado para criar pedidos e uma lista de eventos que usamos para mostrar o progresso das requisições/eventos vindos do backend.

Também há dois javascripts:

  • “~/lib/signalr/dist/browser/signalr.js” — Cliente para SignalR em Javascript. Pode ser obtido atravĂ©s do node package: https://www.npmjs.com/package/@aspnet/signalr
  • “~/js/ordering.js” — Javascript contendo a lĂłgica necessária para conectar ao backend.

Clique aqui para ver o conteĂşdo do script ordering.js.

Com todos esses componentes, Ă© possĂ­vel fazer o debug do projeto e ver o resultado final, Confira:

Acesse o repositório do projeto e veja a solução completa.

Gostou ? Não gostou ? Se quiser saber de mais detalhes ou discutir, deixe nos comentários.

Me acompanhe no Twitter @_arleypadua, obrigado, até a próxima.

Esse artigo também está disponível em Inglês.

--

--

Arley Pádua

Software Engineer and passionate about distributed systems