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:
Importante! É possível criar clientes não apenas com Javascript. É possível usar Java ou .NET, como nos mostra a documentação.
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
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:
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:
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:
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
enewSymbol
, pois o front-end contará com um campo do tiposelect
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:
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:
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 tipoIHubContext<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 interfaceIQuoteHub
, 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:
@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:
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:
"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:
<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:
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:
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:
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.
E para ter acesso ao código-fonte da aplicação, acesse meu GitHub.
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:
Asp.Net Core SignalR Javascript client
Usar o protocolo Hub MessagePack no SignalR para ASP.NET Core