Playground: Asp.Net 5 SignalR
Tags: dotnet, csharp, signalr, braziliandevs
Published: 2021-02-24 13:03:27

Olá!

Este post é uma atualização do artigo [Playground: Asp.Net Core SignalR](https://dev.to/wsantosdev/playground-asp-net-core-signalr-5218), 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:

```xml
<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:

```c#
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:

```c#
.AddMessagePackProtocol();
```

passaremos a ter o seguinte:

```c#
.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:

```c#
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:

```c#
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:

```c#
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 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

```c#
.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:

```c#
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

```c#
await _hub.Clients.Group(quote.Symbol).SendQuote(quote);
```

passaremos a ter

```c#
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:

```c#
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

```javascript
quoteConn.invoke("ChangeSubscription", currentSymbol, event.target.value)
```

passaremos a ter 

```c#
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":

![](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zdlq90a705u989eg6g6x.png)

E o seguinte no Console, após fechar o navegador:

![](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/djziu8q2t6ngx4k55ais.png)

## 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](https://github.com/neuecc/MessagePack-CSharp) (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](https://github.com/wsantosdev/playground-signalr-5.0) 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!