Playground: Asp.Net Core SignalR
Tags: dotnet, csharp, javascript, braziliandevs
Published: 2020-07-11 13:18:03

Olá! 

Este é um post da sessão Playground, uma iniciativa para demonstrar, com pequenos tutoriais, tecnologias e ferramentas que entendo ter potencial para trazer ganhos aos seus projetos. 

## Apresentando o Asp.Net Core SignalR

O SignalR é uma biblioteca para comunicação em tempo real criada pela Microsoft que passou a integrar o Asp.Net Core a partir da versão 2.1. Esta biblioteca permite a troca de mensagens tanto do cliente para o servidor como do servidor para o cliente, e a razão que considero principal para seu uso é, exatamente, a possibilidade de notificar clientes sobre eventos ocorridos no servidor -- que é o caso que veremos neste artigo.

Para utilizar o SignalR você vai precisar de:

- Um editor ou IDE (ex.: [VSCode](https://code.visualstudio.com/download));
- [npm](https://www.npmjs.com/get-npm): para obter as dependências do SignalR para Javascript.

> **Importante!** É possível criar clientes não apenas com Javascript. É possível usar Java ou .NET, como nos mostra a [documentação](https://docs.microsoft.com/pt-br/aspnet/core/signalr/client-features?view=aspnetcore-3.1).

## Começando a Aplicação

A aplicação será um pequeno painel de cotação para ações de empresas brasileiras. Uma pequena amostra de empresas foi pré-selecionada por simplicidade.

Vamos começar criando a infraestrutura da aplicação. Ela será uma Web API, então vamos a partir do template do .NET Core, e remover a pasta `Controllers` e o arquivo `WheaterForecast.cs`

```powershell
PS X:\code\playground-signalr> dotnet new webapi -o Playground.SignalR.Stocks
```

## Criando o Modelo

O modelo de nossa aplicação será bastante simples. Terá uma representação de nossa cotação, e um gerador de preços para simular o recebimento de uma mensagem de atualização.

Para começar, vamos criar nosso modelo de cotação. Crie uma pasta chamada `Models` na raíz do projeto, e um arquivo chamado `Quote.cs` com o seguinte conteúdo:

```c#
using System;

namespace Playground.SignalR.Stocks.Models
{
    public struct Quote
    {
        public string Symbol { get; private set; }
        public decimal Price { get; private set; }
        public DateTime Time { get; private set; }
        
        public static Quote Create(string symbol) => 
            new Quote { Symbol = symbol };

        public void Update(decimal price)
        {
            Price = price;
            Time = DateTime.Now;
        }
    }
}
```

Em seguida, na mesma pasta `Models` crie o arquivo `QuotePriceGenerator.cs`, e adicione o seguinte conteúdo:

```c#
using System;

namespace Playground.SignalR.Stocks.Models
{
    public class QuotePriceGenerator
    {
        private const int MinimumPrice = 10;
        private const int MaximumPrice = 30;
        private const int PriceTreshold = 35;
        private readonly Random _random = new Random();

        public decimal Generate(decimal previousPrice)
        {
            var modifier = (decimal)_random.NextDouble();

            if(previousPrice == 0)
                return _random.Next(MinimumPrice, MaximumPrice) + modifier;

            var updatedPrice = previousPrice + ((modifier > 0.6m ? modifier : modifier * -1) / 100);

            if(updatedPrice > PriceTreshold)
                return MaximumPrice + modifier;

            if(updatedPrice < MinimumPrice)
                return MinimumPrice + modifier;

            return updatedPrice;
        }
    }
}
```

O código acima gera um novo preço a partir do anterior, com uma variação positiva ou negativa a depender do resultado de uma randomização. Além disso, caso o preço fique acima de uma margem máxima, ou abaixo de uma margem mínima, é ajustado para não oscilar descontroladamente.

## Hub: O Protagonista

O Hub é a principal implementação do SignalR, sendo a interface de comunicação entre o cliente o servidor. É nele que usualmente definimos os métodos pelos quais o servidor receberá mensagens, e por quais ele deve enviar.

Para criarmos o hub da nossa aplicação, vamos criar a pasta `Hubs` na raíz do projeto, e adicionar o arquivo `QuoteHub.cs` com o seguinte conteúdo:

```c#
using System.Threading.Tasks;
using Microsoft.AspNetCore.SignalR;

namespace Playground.SignalR.Stocks.Hubs
{
    public class QuoteHub : Hub<IQuoteHub>
    {
        public async Task ChangeSubscription(string oldSymbol, string newSymbol)
        {
            if(!string.IsNullOrEmpty(oldSymbol))
                await Groups.RemoveFromGroupAsync(Context.ConnectionId, oldSymbol);
            
            await Groups.AddToGroupAsync(Context.ConnectionId, newSymbol);
        }
    }
}
```

Dentro do Hub existem outras estruturas que permitem gerenciar a distribuição das mensagens. Uma delas é o Grupo. Grupos são como dicionários, possuem um nome e podem ter adicionado ou removido o ID da conexão com o SignalR, ID este que é semelhante ao SessionId a que estamos acostumados no Asp.Net Core. Quando adicionamos uma conexão a um grupo, qualquer mensagem enviada a este alcançará essa conexão.

No método `ChangeSubscription` vemos que o código de negociação da ação `newSymbol` servirá como nome de grupo. Ou seja, todos os clientes que tiverem interesse em receber a atualização da cotação desta ação serão notificados quando ela for atualizada. 

> **Nota:** Este método foi definido com dois códigos, `oldSymbol` e `newSymbol`, pois o front-end contará com um campo do tipo `select` para promover a troca da cotação acompanhada.

Repare que na declaração da classe, `QuoteHub` herda de `Hub` com a interface `IQuoteHub` como tipo genérico. Herdar de `Hub` é semelhante a herdar de `ControllerBase` em uma Web API. E esta interface adicionada tem uma função bem específica: permitir que os métodos nela especificados para envio de mensagens sejam implementados automaticamente pelo SignalR. É isso mesmo! Nada de implementação manual. É trabalho poupado e tempo ganho!

E aqui temos o código dela. Ainda na pasta `Hubs`, vamos criar o arquivo IQuoteHub.cs e adicionar o seguinte conteúdo:

```c#
using System.Threading.Tasks;
using Playground.SignalR.Stocks.Models;

namespace Playground.SignalR.Stocks.Hubs
{
    public interface IQuoteHub
    {
        Task SendQuote(Quote quote);
    }
}
```

Agora que temos o Hub para receber as solicitações de acompanhamento de cotações, com uma interface que define o método de envio, vamos criar o processo que atualizará as cotações disponíveis.

## Atualizando as Cotações

Para atualizarmos as cotações, vamos utilizar um Background Service do Asp.Net Core. Para isso, na raíz do projeto, vamos criar a pasta `Workers`, e adicionar o arquivo `QuoteWorker.cs` com o seguinte conteúdo:

```c#
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.Hosting;
using Playground.SignalR.Stocks.Hubs;
using Playground.SignalR.Stocks.Models;

namespace Playground.SignalR.Stocks.Workers
{
    public class QuoteWorker : BackgroundService
    {
        private readonly Quote[] _quotes = { Quote.Create("PETR4"), 
                                            Quote.Create("VALE3"), 
                                            Quote.Create("ITUB4"), 
                                            Quote.Create("BBDC4"), 
                                            Quote.Create("BBAS3") };
        private readonly IHubContext<QuoteHub, IQuoteHub> _hub;
        private readonly QuotePriceGenerator _priceGenerator;

        public QuoteWorker(IHubContext<QuoteHub, IQuoteHub> hub, QuotePriceGenerator priceGenerator)
        {
            _hub = hub;
            _priceGenerator = priceGenerator;
        }

        protected override async Task ExecuteAsync(CancellationToken stoppingToken)
        {
            while(!stoppingToken.IsCancellationRequested)
            {
                foreach(Quote quote in _quotes)
                {
                    quote.Update(_priceGenerator.Generate(quote.Price));
                    
                    await _hub.Clients.Group(quote.Symbol).SendQuote(quote);
                }
                
                await Task.Delay(1000, stoppingToken);
            }
        }
    }
}
```

Aqui nós temos todos os nossos componentes em ação. Este BackgroundService vai se encarregar de, a cada segundo, atualizar o preço das cotações previamente cadastradas e enviá-las aos clientes que pertençam ao grupo destas cotações. 

> **Nota:** Aqui temos mais um detalhe interessante. Repare que no construtor de `QuoteWorker` é recebido como argumento uma instância do tipo `IHubContext<QuoteHub, IQuoteHub>`. Essa instância é quem dá acesso ao nosso Hub, e é deste modo que se torna possível enviar mensagens a partir de outras classes além dele. E, como definimos nosso método de envio de mensagens na interface `IQuoteHub`, ela é também usada como tipo genérico. 

Com o back-end quase pronto, é hora de atacar o front!

## O Front-end

Para o front-end, vamos usar uma Razor Page que conterá o painel onde a cotação será exibida. Na raíz do projeto, adicione a pasta `Pages`, e em seguida crie um arquivo chamado `Index.cshtml` com o seguinte conteúdo:

```html
@page

<div>
    <div>
        <select id="selectSymbols">
            <option value="">Selecione um ativo</option>
            <option value="PETR4">PETR4</option>
            <option value="VALE3">VALE3</option>
            <option value="ITUB4">ITUB4</option>
            <option value="BBDC4">BBDC4</option>
            <option value="BBAS3">BBAS3</option>
        </select>
    </div>

    <div style="margin-top:20px;">
        <div>
            Cotação para: <span id="spanSymbol"></span>
        </div>
        <div>
            Ultimo Preço: <span id="spanPrice"></span>
        </div>
        <div>
            Última atualização: <span id="spanTime"></span>
        </div>
    </div>

    <div style="margin-top:20px;">
        <div>
            <span id="spanError"></span>
        </div>
    </div>
</div>

<script src="~/js/libs/signalr.min.js"></script>
<script src="~/js/libs/msgpack5.min.js"></script>
<script src="~/js/libs/signalr-protocol-msgpack.min.js"></script>
<script src="~/js/quotes.js"></script>
```

Aqui nós temos um campo para selecionar a cotação que vamos acompanhar, um painel para exibí-la, e um campo para informação de eventuais erros. Além disso, temos alguns scripts com as dependências do SignalR e nossa lógica para realizar a comunicação com o servidor.

Repare que, entre essas dependências, além do cliente do SignalR temos mais duas bibliotecas: `msgpack5` e `signalr-protocol-msgpack`. Essas bibliotecas server para instruir o cliente SignalR a utilizar o protocolo `MessagePack`, que é um protocolo binário, para serializar os dados para a troca de mensagens. Ou seja, além de podermos trocar mensagens com o servidor, podemos melhorar o desempenho desta troca utilizando um formato mais leve!

É claro que, para tornar isto possível, o servidor também precisa saber que este formato será utilizado. Mas isso será visto à frente, quando chegarmos aos toques finais da aplicação.

Para inserirmos essas dependências no projeto, precisamos executar os seguintes comandos do `npm` na CLI:

```powershell
PS X:\code\playground-signalr> npm init -y
PS X:\code\playground-signalr> npm install @microsoft/signalr-protocol-msgpack
```

Estes comandos vão criar, acima da raíz do projeto, a pasta `node-modules` de onde extrairemos o que precisamos. 

Para continuarmos, vamos utilizar a hospedagem de arquivos estáticos do Asp.Net Core. 

Na pasta raiz do projeto, crie a pasta `wwwroot\js\libs`, e cole o arquivo `signalr.min.js` que está na pasta `node_modules\@microsoft\signalr\dist\browser`. 
Em seguida, cole o arquivo `signalr-protocol-msgpack.min.js` que está na pasta `node_modules\@microsoft\signalr-protocol-msgpack\dist\browser`. 
E, por fim, o arquivo `msgpack5.min.js` que está na pasta `node_modules\msgpack5\dist`.

Para fecharmos nosso front-end, vamos criar o arquivo `quotes.js` em `wwwroot\js` com o seguinte conteúdo:

```javascript
"use strict";

(function() 
{
    var quoteConn = new signalR.HubConnectionBuilder()
                               .withUrl("/quoteHub")
                               .withHubProtocol(new signalR.protocols.msgpack.MessagePackHubProtocol())
                               .build();
    quoteConn.serverTimeoutInMilliseconds = 30000;

    var selectSymbols = document.querySelector('#selectSymbols');
    
    var currentSymbol = '';
    selectSymbols.disabled = true;
    selectSymbols.addEventListener("focus", function(event) 
                 {
                    currentSymbol = event.target.value;
                 });
    selectSymbols.addEventListener("change", function(event) 
                 {
                    quoteConn.invoke("ChangeSubscription", currentSymbol, event.target.value)
                             .catch(function(error) 
                             {
                                console.error(error.toString());
                                spanError.innerHTML = 'Falha ao registrar seu pedido de atualização de cotações';
                             });
                    
                    currentSymbol = selectSymbols.value;
                });

    var spanSymbol = document.querySelector('#spanSymbol');
    var spanTime = document.querySelector('#spanTime');
    var spanPrice = document.querySelector('#spanPrice');
    var spanError = document.querySelector('#spanError');

    quoteConn.on("SendQuote", function (quote) 
    {
        spanSymbol.innerHTML = quote.Symbol;
        spanPrice.innerHTML = parseFloat(quote.Price).toLocaleString('pt-BR', { style: 'currency', currency: 'BRL', mininumFractionDigits: 2, maximumFractionDigits: 2 });
        spanTime.innerHTML = quote.Time.toLocaleTimeString('pt-BR');
    });

    quoteConn.start()
             .then(function ()
              {
                selectSymbols.disabled = false;
              })
              .catch(function (error) 
              {
                spanError.innerHTML = 'Falha ao iniciar conexão com o servidor. Aperte F5.';
              });

    quoteConn.onclose(function(error)
    {
      spanError.innerHTML = 'Conexão com o servidor perdida. Aperte F5.';
    });
    
})();
```

Neste código instanciamos nossa conexão com o Hub informando o caminho */quoteHub* como endpoint, e habilitando nosso `select` tão logo a conexão seja estabelecida. Ao mesmo tempo, adicionamos eventos ao nosso `select` para invocar o método `ChangeSubscription` no servidor para escolher a cotação que acompanharemos. E, também, criamos um manipulador de evento para as mensagens recebidas pelo método `SendQuote` de `IQuoteHub` para podermos exibir nossa cotação na tela.

## Toques Finais

Agora precisamos apenas informar à nossa aplicação quais recursos do Asp.Net Core vamos utilizar. No arquivo do projeto, vamos adicionar a biblioteca do MessagePack, para usarmos este formato no servidor:

```xml
<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <TargetFramework>netcoreapp3.1</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.MessagePack" Version="3.1.0" />
  </ItemGroup>

</Project>
```

Agora, no arquivo `Startup.cs` vamos adicionar os recursos que desejamos utilizar. Para simplificar, basta colar o seguinte conteúdo:

```c#
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Playground.SignalR.Stocks.Hubs;
using Playground.SignalR.Stocks.Models;
using Playground.SignalR.Stocks.Workers;

namespace Playground.SignalR.Stocks
{
    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<QuoteHub>()
                    .AddSingleton<QuotePriceGenerator>()
                    .AddHostedService<QuoteWorker>()
                    .AddSignalR()
                    .AddMessagePackProtocol();

            services.AddRazorPages();
        }

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

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

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapHub<QuoteHub>("/quoteHub");

                endpoints.MapRazorPages();
            });
        }
    }
}

```

No código acima informamos ao Asp.Net Core que utilizaremos nosso Hub, para que ele possa ser injetado via `IHubContext` em nosso Background Service, o atualizador de cotações pelo mesmo motivo, bem como o próprio Background Service (`QuoteWorker`). Também adicionamos o suporte ao SignalR, ao MessagePack, e às Razor Pages.

Também informamos, em `Configure`, que usaremos arquivos estáticos e que devemos mapear nosso Hub para o endereço */quoteHub* que é o endpoint presente em nosso cliente Javascript.

## It's Alive!

Se tudo correu bem, devemos ter o seguinte resultado em nossa tela:

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/r5qqxhfof0xpn8j5poao.png)

> **Importante!** É possível que, ao carregar sua aplicação, seja apresentada uma mensagem de conexão insegura. Caso isso aconteça, não se preocupe. Apenas execute os seguintes comandos na CLI para gerar novos certificados:
>
> ```powershell
> dotnet dev-certs https --clean
> dotnet dev-certs https --trust
> ```

E assim temos nossa primeira aplicação comunicando em tempo real com seus clientes, e utilizando um protocolo que torna essa comunicação mais leve!

Para ver um exemplo funcional, segue uma versão hospedada no [Azure App Service](https://playground-signalr.azurewebsites.net/).

E para ter acesso ao código-fonte da aplicação, acesse meu [GitHub](https://github.com/wsantosdev/playground-signalr).

## Feedkback 

Seu feedback é muito importante para que eu conheça meus acertos, erros, e como posso melhorar de modo geral. Se tiver gostado do artigo, me deixe saber pelos indicadores e mande um comentário se restar alguma dúvida. Respondo assim que puder!

Até a próxima!

> Referências:
>
> [Introdução ao SignalR](https://docs.microsoft.com/pt-br/aspnet/core/tutorials/signalr?view=aspnetcore-3.1&tabs=visual-studio-code)
>
> [Asp.Net Core SignalR Javascript client](https://docs.microsoft.com/en-us/aspnet/core/signalr/javascript-client?view=aspnetcore-3.1)
>
> [Usar o protocolo Hub MessagePack no SignalR para ASP.NET Core](https://docs.microsoft.com/pt-br/aspnet/core/signalr/messagepackhubprotocol?view=aspnetcore-3.1)