DEV Community

loading...
Cover image for Playground: Asp.Net 5 SignalR

Playground: Asp.Net 5 SignalR

wsantosdev profile image William Santos ・8 min read

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>
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

passaremos a ter o seguinte:

.AddMessagePackProtocol(options =>
                        options.SerializerOptions = MessagePackSerializerOptions.Standard
                                                                                .WithSecurity(MessagePackSecurity.UntrustedData)
                    );
Enter fullscreen mode Exit fullscreen mode

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;
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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);
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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;
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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 temos OnConnectedAsync e OnDisconnectedAsync. Já no segundo, temos InvokeMethodAsync apenas. Isso acontece porque a interface IHubFilter 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()
Enter fullscreen mode Exit fullscreen mode

Passaremos a ter

.AddSignalR(options =>
            options.AddFilter<ConnectionFilter>()
            )
Enter fullscreen mode Exit fullscreen mode

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>()
                        )
Enter fullscreen mode Exit fullscreen mode

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

passaremos a ter

await _hub.Clients.Group(quote.Symbol).SendQuote(new QuoteResponse(quote.Symbol, quote.Price, quote.Time));
Enter fullscreen mode Exit fullscreen mode

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);        
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

passaremos a ter

quoteConn.invoke("ChangeSubscription", { CurrentSymbol: currentSymbol, NewSymbol: event.target.value })
Enter fullscreen mode Exit fullscreen mode

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!

Discussion (2)

pic
Editor guide
Collapse
leandroats profile image
Leandro Torres

Gostei do post, depois vou pegar o repositório para fazer um teste.
Obrigado por compartilhar o seu conhecimento.

Collapse
wsantosdev profile image
William Santos Author

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!