Olá!
Este post é uma atualização do artigo Playground: Asp.Net Core SignalR, e nele falaremos sobre o que mudou e o que foi acrescentado ao SignalR entre a versão daquele artigo (Asp.Net Core 3.x) e a versão do .Net 5. Caso não tenha lido o artigo anterior, recomendo que o faça e baixe seu código-fonte, pois partiremos dele para o que faremos neste artigo.
Vamos lá!
Alterando a versão do projeto
Antes de mais nada, precisamos atualizar nosso projeto para o Asp.Net 5. E, para isso, vamos ao arquivo Playground.SignalR.Stocks.csproj
e deixá-lo como segue:
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.MessagePack" Version="5.0.3" />
</ItemGroup>
</Project>
Aqui temos 3 mudanças: 1) a atualização da versão do runtime para o .Net 5 via tag TargetFramework
, com o moniker net5.0
; 2) a habilitação da feature Nullable Reference Types
do C#, que será utilizada mais à frente; 3) a atualização da biblioteca de serialização do MessagePack, que explicaremos a seguir.
Atualização do MessagePack para a versão 2.x
Para a versão 5.0 do Asp.Net Core, a dependência da biblioteca do MessagePack foi atualizada para a versão 2.x. Grosso modo essa mudança atualiza o modelo de serialização das mensagens, e traz uma pequena mudança na forma como os modelos devem ser desenhados para tornar a serialização possível.
Para demonstrar essa mudança, e como ela é compatível com os Record Types
do C# 9, vamos incluir um arquivo chamado QuoteDtos.cs
na pasta Models
de nosso projeto, e inserir o seguinte conteúdo:
using MessagePack;
using System;
namespace Playground.SignalR.Stocks.Models
{
[MessagePackObject(true)]
public record QuoteRequest(string CurrentSymbol, string NewSymbol);
[MessagePackObject(true)]
public record QuoteResponse(string Symbol, decimal Price, DateTime Time);
}
Repare que, ao contrário da versão original do projeto, temos aqui um atributo MessagePackObject
, que funciona de forma análoga ao atributo Serializable
já presente no C#, e indica que um dado tipo pode ser serializado pelo MessagePack. O valor true
representa a propriedade keyAsPropertyName
, que informa ao MessagePack qual modelo de serialização será utilizado.
Nota: Existem dois modelos de serialização no MessagePack: indexado e por propriedade. De forma simples, o indexado cria um array onde cada propriedade de nossos tipos estará presente em um índice, e o por propriedade cria um objeto complexo onde cada propriedade é referida as is. O modelo indexado cria um objeto menor e, por isso, é mais performático. Mas, para evitarmos mudanças no consumo das mensagens em nosso cliente JS, optamos por usar o modelo por propriedades.
Agora temos dois DTOs, um que representa a requisição por uma cotação, e um que representa a cotação retornada ao cliente. Mais à frente veremos como ambos são empregados.
Atualização do tipo de configuração do MessagePack
Uma segunda mudança relacionada ao MessagePack é que a forma como o mesmo é configurado deixou de utilizar um tipo fornecido pelo Asp.Net Core (que era o IList<MessagePack.IFormatterResolver>
), para um tipo da própria biblioteca do MessagePack, o MessagePackSerializationOptions
.
Não utilizamos este tipo de configuração no projeto original porque assumimos as configurações padrão do MessagePack naquela ocasião. Entretanto, precisamos incluir uma configuração de segurança no projeto atual e, para isso, vamos utilizar este novo tipo.
Para realizarmos esta configuração, vamos ao arquivo Startup.cs
fazer uma alteração. Onde tínhamos:
.AddMessagePackProtocol();
passaremos a ter o seguinte:
.AddMessagePackProtocol(options =>
options.SerializerOptions = MessagePackSerializerOptions.Standard
.WithSecurity(MessagePackSecurity.UntrustedData)
);
Esta é uma configuração muito importante: ela indica ao MessagePack que as mensagens que serão recebidas para desserialização em nossa aplicação vem de uma fonte não confiável (a Internet), e isso leva à ativação de mecanismos de segurança da biblioteca para reduzir a superfície de ataque (o que, evidentemente, tem um custo em desempenho, necessário neste caso). Para a comunicação com clientes confiáveis, geralmente dentro de sua rede, a opção MessagePackSecurity.TrustedData
pode ser utilizada para melhorar o desempenho da troca de mensagens.
Agora que temos um DTO para enviar a cotação ao cliente, vamos fazer uma pequena alteração em nosso modelo de domínio, a cotação que é atualizada em nosso serviço para ser enviada ao cliente. Para isso, vamos ao arquivo Quote.cs
, ainda na pasta Models
, e vamos alterar o conteúdo para o seguinte:
using System;
namespace Playground.SignalR.Stocks.Models
{
public class Quote
{
public string Symbol { get; }
public decimal Price { get; private set; }
public DateTime Time { get; private set; }
private Quote(string symbol) =>
Symbol = symbol;
public static Quote Create(string symbol) =>
new Quote(symbol);
public void Update(decimal price)
{
Price = price;
Time = DateTime.Now;
}
}
}
Aqui a mudança foi mínima: introduzimos um construtor privado que recebe o valor de Symbol
de nosso factory method, e isso porque habilitamos o Nullable Reference Types
em nosso projeto, o que passou a exigir que Symbol, não marcado como nullable
, sempre tivesse valor após a construção de nosso tipo. Não foi por causa deste tipo que habilitamos a feature, mas sim por causa de outro que explicaremos a seguir.
Hub Filters
Esta é uma novidade desta versão do SignalR. De forma semelhante aos Filters do Asp.Net Core, os Hub Filters são executados em duas ocasiões: logo após os eventos de conexão e desconexão de um cliente; e quando um método do Hub é invocado.
Para conhecermos um pouco melhor esta feature, vamos criar dois filtros. Para isso, vamos criar a pasta Filters
na raíz do projeto e, em seguida, criar o arquivo ConnectionFilter.cs
com o seguinte conteúdo:
using Microsoft.AspNetCore.SignalR;
using System;
using System.Threading.Tasks;
namespace Playground.SignalR.Stocks.Filters
{
public class ConnectionFilter : IHubFilter
{
public Task OnConnectedAsync(HubLifetimeContext context,
Func<HubLifetimeContext, Task> next)
{
Console.WriteLine($"New client connected. Connection ID: {context.Context.ConnectionId}");
return next(context);
}
public Task OnDisconnectedAsync(HubLifetimeContext context,
Exception? exc,
Func<HubLifetimeContext, Exception?, Task> next)
{
Console.WriteLine($"Client with connection ID {context.Context.ConnectionId} has disconnected.");
if (exc != null)
Console.WriteLine($"Disconnection exception: {exc}");
return next(context, exc);
}
}
}
A intenção deste filtro é registrar em console os eventos de conexão e desconexão de um dado cliente. Para isso, utilizamos os métodos OnConnectedAsync
e OnDisconnectedAsync
da interface IHubFilter
. Repare que na assinatura do método OnDisconnectedAsync
temos uma Exception nullable
. É por conta do uso de Nullable Reference Types
na interface IHubFilter
que optamos por habilitar o recurso (habilitamos diretamente no projeto por simplicidade).
Agora, vamos criar o segundo filtro, que será acionado sempre que um método do nosso hub QuoteHub
for acionado, e gerará um registro em Console informando qual o ativo foi selecionado por um dado ConnectionId
para obter cotações. Para isso, vamos criar o arquivo QuoteHubFilter.cs
, também na pasta Filters
e incluir o seguinte conteúdo:
using Microsoft.AspNetCore.SignalR;
using Playground.SignalR.Stocks.Models;
using System;
using System.Threading.Tasks;
namespace Playground.SignalR.Stocks.Filters
{
public class QuoteHubFilter : IHubFilter
{
public async ValueTask<object?> InvokeMethodAsync(HubInvocationContext invocationContext,
Func<HubInvocationContext,
ValueTask<object?>> next)
{
var request = invocationContext.HubMethodArguments[0] as QuoteRequest;
try
{
if(request != null)
Console.WriteLine($"{invocationContext.Context.ConnectionId} has selected {request.NewSymbol}.");
return await next(invocationContext);
}
catch (Exception exc)
{
if(request != null)
Console.WriteLine($"Error switching symbol to '{request.NewSymbol}': {exc}");
throw;
}
}
}
}
Aqui cabe um detalhe interessante: o objeto invocationContext
possui as informações relativas ao envio da mensagem, de forma muito semelhante ao HttpContext
no Asp.Net. Uma das propriedades deste objeto é HubMethodName
, que nos permitiria filtrar em quais métodos gostaríamos que o filtro fosse executado. Em nosso caso, como QuoteHub
possui um único método invocável, esta propriedade não foi utilizada, apenas o primeiro argumento enviado ao método, que é a mensagem de requisição de cotação (QuoteRequest
).
Nota: ambos os filtros implementam
IHubFilter
, mas implementam métodos diferentes. No primeiro filtro temosOnConnectedAsync
eOnDisconnectedAsync
. Já no segundo, temosInvokeMethodAsync
apenas. Isso acontece porque a interfaceIHubFilter
se utiliza da feature Default Interface Implementation, do C# 8. Ou seja, na ausência de uma implementação para um de seus métodos, a própria interface provê um comportamento padrão. Podemos falar sobre esta feature em artigo futuro.
Configurando os Filters
Agora temos de informar ao Asp.Net como nossos filtros devem funcionar. Existem dois tipos de filtro: globais e locais. Os globais são aqueles executados em todos os hubs, e os locais são executados no hub especificado. Como queremos saber quando um cliente se conecta a partir do primeiro filtro, vamos configurá-lo como global. Para isso, vamos ao arquivo Startup.cs
e alterar o registro do SignalR. Onde tínhamos
.AddSignalR()
Passaremos a ter
.AddSignalR(options =>
options.AddFilter<ConnectionFilter>()
)
Já nosso filtro de cotações será local, uma vez que sua execução está restrita ao hub de cotações. Vamos, ainda no arquivo Startup.cs
, incluir o seguinte conteúdo logo abaixo do registro do SignalR:
.AddHubOptions<QuoteHub>(options =>
options.AddFilter<QuoteHubFilter>()
)
Com isso temos nossos filtros configurados para interceptar tanto os eventos de conexão/desconexão, quanto ao pedido de cotações.
Ajustes Finais
Agora estamos próximos da conclusão da aplicação. Precisamos, apenas, informar ao sistema que nossos DTOs de cotação (QuoteRequest
e QuoteRespose
) serão utilizados. Para isso, vamos fazer duas mudanças.
A primeira é no arquivo IQuoteHub.cs
na pasta Hubs
, vamos alterar o conteúdo para o seguinte:
using System.Threading.Tasks;
using Playground.SignalR.Stocks.Models;
namespace Playground.SignalR.Stocks.Hubs
{
public interface IQuoteHub
{
Task SendQuote(QuoteResponse quote);
}
}
Em seguida, vamos alterar nosso background service que invoca este método de nosso hub. Para isso, vamos ao arquivo QuoteWorker
na pasta Workers
, e fazer a seguinte substituição: onde tínhamos
await _hub.Clients.Group(quote.Symbol).SendQuote(quote);
passaremos a ter
await _hub.Clients.Group(quote.Symbol).SendQuote(new QuoteResponse(quote.Symbol, quote.Price, quote.Time));
Agora, vamos mudar nosso Hub para aceitar as mensagens de requisição de cotação. No arquivo QuoteHub.cs
, na pasta Hubs
, vamos atualizar seu conteúdo pelo seguinte:
using System.Threading.Tasks;
using Microsoft.AspNetCore.SignalR;
using Playground.SignalR.Stocks.Models;
namespace Playground.SignalR.Stocks.Hubs
{
public class QuoteHub : Hub<IQuoteHub>
{
public async Task ChangeSubscription(QuoteRequest request)
{
if(!string.IsNullOrEmpty(request.CurrentSymbol))
await Groups.RemoveFromGroupAsync(Context.ConnectionId, request.CurrentSymbol);
await Groups.AddToGroupAsync(Context.ConnectionId, request.NewSymbol);
}
}
}
E, por fim, vamos atualizar nosso cliente Javascript para enviar este novo DTO para o servidor. No arquivo quotes.js
na pasta wwwroot\js
, onde tínhamos
quoteConn.invoke("ChangeSubscription", currentSymbol, event.target.value)
passaremos a ter
quoteConn.invoke("ChangeSubscription", { CurrentSymbol: currentSymbol, NewSymbol: event.target.value })
E voi lá!
Com isso, temos nossa aplicação pronta. Se tudo deu certo, devemos ter o seguinte resultado em tela, escolhendo "ITUB4":
E o seguinte no Console, após fechar o navegador:
Conclusão
As mudanças realizadas no SignalR para o Asp.Net 5 trouxeram mais desempenho para a troca de mensagens, e maior flexibilidade com a inclusão dos filtros. São mudanças aparentemente simples, mas que criam diversas oportunidades para suas aplicações. Recomendo a leitura da documentação do MessagePack 2 (em inglês) para maiores detalhes, pois além de haver diversas opções de configuração além das exibidas aqui, o protocolo pode ser utilizado para a troca de mensagens em aplicações Asp.Net além do SignalR.
Como de costume, segue o repositório do Github com o código deste artigo.
Gostou? Me deixe saber pelos indicadores. Tem alguma dúvida? Comente ou entre em contato pelas redes sociais.
Até a próxima!
Top comments (2)
Gostei do post, depois vou pegar o repositório para fazer um teste.
Obrigado por compartilhar o seu conhecimento.
Sou eu quem te agradece pela atenção, Leandro. Fique à vontade pra me dar um feedback sobre seus testes – feedbacks são sempre úteis!
Valeu!