DEV Community

Cover image for MessagePack 2 - Comunicando APIs Asp.Net Core
William Santos
William Santos

Posted on • Updated on

MessagePack 2 - Comunicando APIs Asp.Net Core

Olá!

Este será um post com um formato diferente. A ideia é apresentar com mais detalhes a biblioteca com a implementação para C# do formato MessagePack de serialização, brevemente apresentado em nosso artigo sobre SignalR.

O que é o MessagePack?

MessagePack é um formato de serialização binário que visa reduzir o tamanho de objetos para transporte e armazenamento. Estamos muito acostumados, em aplicações Web principalmente, a serializar objetos no formato JSON, que é humanamente legível e relativamente leve, mas que em cenários de troca intensiva de mensagens, ou armazenamento massivo, acaba por gerar um consumo aumentado de banda ou storage. Por conta disso, foi formalizado um padrão de serialização binário que, embora não seja humanamente legível, tem um tamanho muito reduzido, o que também reduz o consumo de banda, e o consumo de storage em caso de armazenamento.

Por ser um padrão, pode ser implementado por qualquer linguagem -- há uma implementação em Javascript utilizada para aplicações SignalR, por exemplo. Isso permite que aplicações escritas em diversas linguagens possam se comunicar a partir dele.

Quando usar o MessagePack?

Como dito acima, em cenários de trocas de mensagens e armazenamento de objetos, o formato oferece um tamanho reduzido. Como poderá ser visto no projeto de exemplo disponibilizado no final deste artigo, um dos usos possíveis é na comunicação entre APIs, que geralmente acontece com payloads serializados como JSON. Ou seja, em qualquer cenário onde seja importante serializar as mensagens em um formato reduzido, faz sentido empregar o MessagePack.

Ao longo deste artigo mostraremos os pontos mais importantes da biblioteca para C#, para que, desta forma, o projeto de testes faça sentido e você tenha uma visão das opções oferecidas por ela.

Vamos lá!

Contratos - O que.

Os contratos são as definições de quais tipos podem ser serializados, ou seja, o que vamos serializar. Há 3 formas de definir contratos no MessagePack: via índice, via propriedade, ou via resolver (que trataremos à frente). Nos dois primeiros casos, é usado o atributo [MessagePackObject], que indica que um dado tipo pode ser serializado para o formato MessagePack.

Um contrato definido por índice é um array onde cada posição representa uma propriedade do contrato. Por exemplo:

[MessagePackObject]
public record MessageSampleByIntKey
{
    [Key(0)]
    public string Sender { get; init; }
    [Key(1)]
    public string Recipient { get; init; }
    [Key(2)]
    public string Body { get; init; }
}
Enter fullscreen mode Exit fullscreen mode

Neste caso, o que informamos por estes atributos é o seguinte: temos um tipo serializável no formato de um array, onde cada propriedade ocupará um índice definido pelo atributo Key correspondente. Sendo assim, a propriedade Recipient, por exemplo, ocupará o índice 1 deste array, que será serializado da seguinte forma:

["Brian", "Bob", "Alice is in town. Let's visit her."]
Enter fullscreen mode Exit fullscreen mode

Este formato é o mais performático, pois não há a necessidade de informar quais são as propriedades do objeto serializado. Ao desserializar o objeto, o serializador saberá a qual propriedade atribuir qual valor pois terá a indicação dos índices nos atributos do contrato.

Já um contrato definido por prioridade possui uma declaração diferente, podendo se dar de duas formas, implícita e explícita. A implícita considera cada propriedade do tipo uma propriedade correspondente no modelo serializado, como o exemplo abaixo:

[MessagePackObject(true)]
public record MessageSampleByImplicitStringKey
{
    public string Sender { get; init; }
    public string Recipient { get; init; }
    public string Body { get; init; }
}
Enter fullscreen mode Exit fullscreen mode

Como podemos ver, não precisamos informar a cada propriedade como ela será serializada. Apenas informamos a propriedade keyAsPropertyName do atributo MessagePackObject como true, e todas as propriedades serão serailzadas de forma semelhante a um JSON:

{ Sender = "Brian", Recipient = "Bob", Body = "Alice is in town. Let's visit her." }
Enter fullscreen mode Exit fullscreen mode

Já na forma explícita, podemos escolher os nomes das propriedades que serão serializadas:

[MessagePackObject]
public record MessageSampleByImplicitStringKey
{
    [Key("A")]
    public string Sender { get; init; }
    [Key("B")]
    public string Recipient { get; init; }
    [Key("C")]
    public string Body { get; init; }
}
Enter fullscreen mode Exit fullscreen mode

E a saída seria a seguinte:

{ A = "Brian", B = "Bob", C = "Alice is in town. Let's visit her." }
Enter fullscreen mode Exit fullscreen mode

A forma explícita pode ser utilizada para encurtar o nome das propriedades, por exemplo, reduzindo o conteúdo do objeto serializado.

Nota: nos três casos de exemplo, o resultado da serialização será um binário. O formato string utilizado é apenas uma representação do conteúdo que será serializado.

Serializador - Quem.

O serializador é um tipo estático da biblioteca responsável por serializar/desserializar um objeto no formato MessagePack. Basicamente, ele funciona da seguite forma para serializar um objeto:

var message = new MessageSample { Sender = "Brian", Recipient = "Bob", Body = "Alice is in town. Let's visit her." };
var binary = MessagePackSerializer.Serialize(message);
Enter fullscreen mode Exit fullscreen mode

Já a desserialização seria desta forma:

var message = MessagePackSerializer.Deserialize<MessageSample>(binary);
Enter fullscreen mode Exit fullscreen mode

Repare que ele funciona da mesma forma que o serializador JSON quando invocado explicitamente.

Resolvers - Como.

Resolvers são componentes que possuem um conjunto de formatadores que serão empregados conforme o tipo a ser serializado. Cada resolver tem registrados formatadores adequados para um determinado conjunto de tipos. Os resolvers mais comuns são:

  • StandardResolver
  • ContractlessStandardResolver
  • TypelessContractlessStandardResolver

O StandardResolver possui os formatadores para os tipos primitivos e coleções mais usadas do framework, e é o resolver padrão utilizado pelo serializador. Portanto, dados os exemplos acima, caso informássemos um resolver explícitamente teríamos o seguinte:

using MessagePack.Resolvers;
...
var binary = MessagePackSerializar.Serialize(message, StandardResolver.Options);
Enter fullscreen mode Exit fullscreen mode

Já o ContractlessStandardResolver tem duas capacidades adicionais: 1) dispensar um contrato da decoração por atributos, ou seja, poderíamos serializar da seguinte forma:

public record MessageSample
{
    public string Sender { get; init; }
    public string Recipient { get; init; }
    public string Body { get; set; }
}
...
using MessagePack.Resolvers;
...
var message = new MessageSample { Sender = "Brian", Recipient = "Bob", Body = "Alice is in town. Let's visit her." }
var binary = MessagePackSerializer.Serialize(message, ContractlessStandardResolver.Options);
Enter fullscreen mode Exit fullscreen mode

E 2) serializar tipos dinâmicos (dynamic), como por exemplo:

using MessagePack.Resolvers;
...
var message = new MessageSample { Sender = "Brian", Recipient = "Bob", Body = "Alice is in town. Let's visit her." }
var binary = MessagePackSerializer.Serialize(message, ContractlessStandardResolver.Options);
...
var deserialized = MessagePackSerializer.Deserialize<dynamic>(message, ContractlessStandardResolver.Options);
Enter fullscreen mode Exit fullscreen mode

E, por fim, o TypelessContractlessStandardResolver tem a capacidade de serializar tipos sem a necessidade de informar o tipo durante a serialização/desserialização. Neste caso, o nome do tipo é serializado junto com a mensagem. Considerando o exemplo acima, funcionaria da seguinte forma:

using MessagePack.Resolvers;
...
var message = new MessageSample { Sender = "Brian", Recipient = "Bob", Body = "Alice is in town. Let's visit her." }
var binary = MessagePackSerializer.Typeless.Serialize(message);
...
var deserialized = MessagePackSerializer.Typeless.Deserialize(message) as MessageSample;
Enter fullscreen mode Exit fullscreen mode

Já o resultado, seria semelhante ao do StandardResolver com uma declaração de contrato por índice:

["SampleProject.MessageSample, SampleProject", "Brian", "Bob", "Alice is in town. Let's visit her."]
Enter fullscreen mode Exit fullscreen mode

Segurança - Onde.

Existem dois tipos de fontes de dados sob uma perspectiva de segurança: confiáveis e não confiáveis. Ao se trabalhar com MessagePack é possível configurar a desserialização para considerar qual tipo de fonte de dados está sendo usada. Caso a fonte seja considerada não confiável, serão adicionadas algumas verificações no desserializador para evitar formas simples e comuns de tentativa de ataque.
Cabe frisar que estas verificações de segurança não garantem a segurança da mensagem, apenas dificultam alguns dos tipos mais comuns de ataques, e que usar serialização do tipo Typeless aumenta a superfície de ataque, já que um tipo imprevisto pode compor a mensagem.

Para configurar o desserializador para traduzir mensagens de fontes não confiáveis é necessário informar, junto as opções do resolver, a opção de segurança, da seguinte forma:

var options = MessagePackSerializerOptions.Standard.WithSecurity(MessagePackSecurity.UntrustedData);
...
var deserialized = MessagePackSerializer.Deserialize<MessageSample>(binary, options);
Enter fullscreen mode Exit fullscreen mode

Também é possível preconfigurar o serializador para usar essa opção por padrão:

MessagePackSerializer.DefaultOptions = MessagePackSerializerOptions.Standard.WithSecurity(MessagePackSecurity.UntrustedData);
Enter fullscreen mode Exit fullscreen mode

Asp.Net Core: Quando.

Por fim, depois de conhecermos os principais mecanismos para o uso do MessagePack, temos a possibilidade de integrá-lo ao Asp.Net Core com um formatador. Para isso, precisamos adicionar algumas opções ao método de extensão AddMvc em nossa coleção de serviços, opções estas contidas na biblioteca MessagePack.AspNetCoreMvcFormatter.

Vejamos como adicioná-las:

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc().AddMvcOptions(option =>
    {
        option.OutputFormatters.Clear();
        option.OutputFormatters.Add(new MessagePackOutputFormatter(ContractlessStandardResolver.Options));

        option.InputFormatters.Clear();
        option.InputFormatters.Add(new MessagePackInputFormatter(ContractlessStandardResolver.Options));
    });
}
Enter fullscreen mode Exit fullscreen mode

Acima temos três dados importantes: 1) os formatadores padrão do Asp.Net Core foram removidos, tanto para input quanto para output; 2) foram incluídos os formatadores de input e output do MessagePack; 3) por conta desta combinação, esta aplicação saberá se comunicar apenas em MessagePack. Ou seja, só aceitará payloads em MessagePack (retornando erro 500 para outros formatos), e retornará resultados apenas em MessagePack.

Vale a pena observar que, por padrão, a aplicação serializará as mensagens usando o ContractlessStandardResolver, ou seja, não seria necessário decorar os contratos com os atributos do MessagePack, e haveria a possibilidade de serializar objetos dinâmicos. Apesar do exemplo, recomendamos que seja utilizado o StandardResolver e que os contratos sejam bem definidos. Desta forma torna-se possível identificar quais tipos poderão ser serializados de forma mais rápida, e os detalhes da serialização também serão mais facilmente reconhecíveis.

Nota: No projeto disponível ao final do artigo, demonstramos como é possível comunicar tanto em MessagePack quanto em JSON, ilustrando como é possível trabalhar com mais de um formatador. Além disso, foram criados decorators para os formatadores a fim de registrar seu acionamento em console. Desta forma é possível compreender melhor a anatomia de um formatador.

Compressão: O extremo!

Existe uma forma de compressão das mensagens chamada Compressão LZ4. A ideia é utilizá-la quando houver o desejo de se obter performance extrema na serialização de tipos.

Para ativar a compressão, assim como no caso da segurança, é necessário informar ao serialilzador que a mesma deve ser utilizada por meio de opções. Considerando o exemplo acima, seria da seguinte forma:

var options = MessagePackSerializerOptions.Standard
                                .WithSecurity(MessagePackSecurity.UntrustedData)
                                .WithCompression(MessagePackCompression.Lz4BlockArray);
...
var binary = MessagePackSerializer.Serialize(message, options);
Enter fullscreen mode Exit fullscreen mode

Projeto de Exemplo

Aqui temos o projeto de exemplo pronto para testar. Existem 6 projetos na solução, 3 deles são as APIs que vão se comunicar, 2 deles são as bibliotecas com os contratos MessagePack compartilhados entre essas APIs, e um é um projeto de extensão para adicionarmos logs às chamadas dos formatadores do MessagePack, para input e output.

As 3 APIs serão executadas ao se executar a solução e a principal delas (Lab.MsgPack2.Trading) será aberta no navegador apontando para seu Swagger. Neste será possível consultar o valor na conta corrente do cliente, as ações que este cliente possui na sua carteira, e poderá ser simulada uma negociação que afetará sua carteira e seu saldo de acordo com o tipo de operação (compra ou venda).

Quando o método de consulta ao saldo em conta corrente é invocado, a API de conta corrente é invocada pela API de Trading, utilizando um contrato do MessagePack. O mesmo acontece quando se consulta a carteira de ações. E, quando uma negociação é simulada, as duas são chamadas para agir de acordo com a operação. E tanto a consulta ao saldo em conta corrente quanto a consulta à carteira de ações escreverão no console que foram acionadas e qual rota foi invocada, pra que se tenha certeza de que a mensagem MessagePack foi corretamente transmitida.

Divirta-se!

Conclusão

A implementação do MessagePack para o C# é bastante simples e, ao mesmo tempo, poderosa. Podemos, a partir dela, tornar a comunicação com as nossas APIs mais leve, tanto entre elas e seus clientes (há uma implementação em Javascript já demonstrada em nosso artigo sobre SignalR) quando entre si. A diferença de desempenho em relação ao JSON é muito significativa, a ponto de compensar a complexidade adicional de seu uso.

Para conhecer melhor a biblioteca, e ter acesso a seu repositório com a documentação completa, recomendo sua página no nuget (em inglês).

Gostou? Me deixe saber pelos indicadores. Caso tenha alguma dúvida, deixe nos comentários ou em minhas redes sociais.

Até a próxima!

Latest comments (2)

Collapse
 
jessilyneh profile image
Jessilyneh

Gostei muito! E principalmente, da forma que voce separou os tópicos. Parabens!

Collapse
 
wsantosdev profile image
William Santos

Muito obrigado, Jéssica! Fiquei feliz por ter gostado. 😄