Serialização no .NET
Quando Usar Esta Habilidade
Use esta habilidade quando:
- Escolher um formato de serialização para APIs, mensageria ou persistência
- Migrar de Newtonsoft.Json para System.Text.Json
- Implementar serialização compatível com AOT
- Projetar formatos de transmissão para sistemas distribuídos
- Otimizar o desempenho da serialização
Baseado em Esquema vs Baseado em Reflexão
| Aspecto | Baseado em Esquema | Baseado em Reflexão |
|---|---|---|
| Exemplos | Protobuf, MessagePack, System.Text.Json (source gen) | Newtonsoft.Json, BinaryFormatter |
| Informação de tipo no payload | Não (esquema externo) | Sim (nomes de tipo embutidos) |
| Versionamento | Números/nomes de campo explícitos | Implícito (estrutura do tipo) |
| Desempenho | Rápido (sem reflexão) | Mais lento (reflexão em tempo real) |
| Compatível com AOT | Sim | Não |
| Compatibilidade de transmissão | Excelente | Ruim |
Recomendação: Use serialização baseada em esquema para tudo que ultrapassa limites de processo.
Recomendações de Formato
| Caso de Uso | Formato Recomendado | Motivo |
|---|---|---|
| APIs REST | System.Text.Json (source gen) | Padrão, compatível com AOT |
| gRPC | Protocol Buffers | Formato nativo, excelente versionamento |
| Mensageria de atores | MessagePack ou Protobuf | Compacto, rápido, seguro para versões |
| Event sourcing | Protobuf ou MessagePack | Deve lidar com eventos antigos para sempre |
| Cache | MessagePack | Compacto, rápido |
| Configuração | JSON (System.Text.Json) | Legível por humanos |
| Registro (logging) | JSON (System.Text.Json) | Estruturado, analisável |
Formatos a Evitar
| Formato | Problema |
|---|---|
| BinaryFormatter | Vulnerabilidades de segurança, obsoleto, nunca usar |
| Newtonsoft.Json padrão | Nomes de tipo no payload quebram ao renomear |
| DataContractSerializer | Complexo, versionamento pobre |
| XML | Verboso, lento, complexo |
System.Text.Json com Source Generators
Para serialização JSON, use System.Text.Json com source generators para compatibilidade AOT e desempenho.
Configuração
// Defina um JsonSerializerContext com todos os seus tipos
[JsonSerializable(typeof(Order))]
[JsonSerializable(typeof(OrderItem))]
[JsonSerializable(typeof(Customer))]
[JsonSerializable(typeof(List<Order>))]
[JsonSourceGenerationOptions(
PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull)]
public partial class AppJsonContext : JsonSerializerContext { }
Uso
// Serializar com contexto
var json = JsonSerializer.Serialize(order, AppJsonContext.Default.Order);
// Desserializar com contexto
var order = JsonSerializer.Deserialize(json, AppJsonContext.Default.Order);
// Configurar no ASP.NET Core
builder.Services.ConfigureHttpJsonOptions(options =>
{
options.SerializerOptions.TypeInfoResolverChain.Insert(0, AppJsonContext.Default);
});
Benefícios
- Sem reflexão em tempo de execução - Toda informação de tipo gerada em tempo de compilação
- Compatível com AOT - Funciona com publicação Native AOT
- Mais rápido - Sem análise de tipo em tempo real
- Trim-safe - O vinculador sabe exatamente o que é necessário
Protocol Buffers (Protobuf)
Melhor para: Sistemas de atores, gRPC, event sourcing, qualquer formato de transmissão de longa duração.
Configuração
dotnet add package Google.Protobuf
dotnet add package Grpc.Tools
Definir Esquema
// orders.proto
syntax = "proto3";
message Order {
string id = 1;
string customer_id = 2;
repeated OrderItem items = 3;
int64 created_at_ticks = 4;
// Adicionar novos campos é sempre seguro
string notes = 5; // Adicionado na v2 - leitores antigos ignoram
}
message OrderItem {
string product_id = 1;
int32 quantity = 2;
int64 price_cents = 3;
}
Regras de Versionamento
// SEGURO: Adicionar novos campos com novos números
message Order {
string id = 1;
string customer_id = 2;
string shipping_address = 5; // NOVO - seguro
}
// SEGURO: Remover campos (leitores antigos ignoram desconhecidos, novos usam padrão)
// Apenas pare de usar o campo, mantenha o número reservado
message Order {
string id = 1;
// customer_id removido, mas campo 2 está reservado
reserved 2;
}
// INSEGURO: Mudar tipos de campo
message Order {
int32 id = 1; // Era: string - QUEBRA!
}
// INSEGURO: Reutilizar números de campo
message Order {
reserved 2;
string new_field = 2; // Reutilizando 2 - QUEBRA!
}
MessagePack
Melhor para: Cenários de alto desempenho, payloads compactos, mensageria de atores.
Configuração
dotnet add package MessagePack
dotnet add package MessagePack.Annotations
Uso com Contratos
[MessagePackObject]
public sealed class Order
{
[Key(0)]
public required string Id { get; init; }
[Key(1)]
public required string CustomerId { get; init; }
[Key(2)]
public required IReadOnlyList<OrderItem> Items { get; init; }
[Key(3)]
public required DateTimeOffset CreatedAt { get; init; }
// Novo campo - leitores antigos pulam chaves desconhecidas
[Key(4)]
public string? Notes { get; init; }
}
// Serializar
var bytes = MessagePackSerializer.Serialize(order);
// Desserializar
var order = MessagePackSerializer.Deserialize<Order>(bytes);
Configuração Compatível com AOT
// Use source generator para AOT
[MessagePackObject]
public partial class Order { } // partial habilita source gen
// Configurar resolver
var options = MessagePackSerializerOptions.Standard
.WithResolver(CompositeResolver.Create(
GeneratedResolver.Instance, // Gerado
StandardResolver.Instance));
Migrando de Newtonsoft.Json
Problemas Comuns
| Newtonsoft | System.Text.Json | Correção |
|---|---|---|
$type no JSON | Não suportado por padrão | Use discriminadores ou conversores personalizados |
JsonProperty | JsonPropertyName | Atributo diferente |
DefaultValueHandling | DefaultIgnoreCondition | API diferente |
NullValueHandling | DefaultIgnoreCondition | API diferente |
| Setters privados | Requer [JsonInclude] | Aceitação explícita |
| Polimorfismo | [JsonDerivedType] (.NET 7+) | Discriminadores explícitos |
Padrão de Migração
// Newtonsoft (baseado em reflexão)
public class Order
{
[JsonProperty("order_id")]
public string Id { get; set; }
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
public string? Notes { get; set; }
}
// System.Text.Json (compatível com source-gen)
public sealed record Order(
[property: JsonPropertyName("order_id")]
string Id,
string? Notes // Tratamento de nulo via JsonSerializerOptions
);
[JsonSerializable(typeof(Order))]
[JsonSourceGenerationOptions(
PropertyNamingPolicy = JsonKnownNamingPolicy.SnakeCaseLower,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull)]
public partial class OrderJsonContext : JsonSerializerContext { }
Polimorfismo com Discriminadores
// .NET 7+ polimorfismo
[JsonDerivedType(typeof(CreditCardPayment), "credit_card")]
[JsonDerivedType(typeof(BankTransferPayment), "bank_transfer")]
public abstract record Payment(decimal Amount);
public sealed record CreditCardPayment(decimal Amount, string Last4) : Payment(Amount);
public sealed record BankTransferPayment(decimal Amount, string AccountNumber) : Payment(Amount);
// Serializa como:
// { "$type": "credit_card", "amount": 100, "last4": "1234" }
Padrões de Compatibilidade de Transmissão
Leitor Tolerante
Código antigo deve ignorar com segurança campos desconhecidos:
// Protobuf/MessagePack: Automático - campos desconhecidos ignorados
// System.Text.Json: Configure para permitir
var options = new JsonSerializerOptions
{
UnmappedMemberHandling = JsonUnmappedMemberHandling.Skip
};
Introduzir Leitura Antes da Escrita
Implante desserializadores antes de serializadores para novos formatos:
// Fase 1: Adicionar desserializador (implantado em todos os lugares)
public Order Deserialize(byte[] data, string manifest) => manifest switch
{
"Order.V1" => DeserializeV1(data),
"Order.V2" => DeserializeV2(data), // NOVO - pode ler V2
_ => throw new NotSupportedException()
};
// Fase 2: Habilitar serializador (próximo lançamento, após V1 implantado em todos os lugares)
public (byte[] data, string manifest) Serialize(Order order) =>
_useV2Format
? (SerializeV2(order), "Order.V2")
: (SerializeV1(order), "Order.V1");
Nunca Embutir Nomes de Tipo
// RUIM: Nome de tipo no payload - renomear classe quebra formato de transmissão
{
"$type": "MyApp.Order, MyApp.Core",
"id": "123"
}
// BOM: Discriminador explícito - refatoração segura
{
"type": "order",
"id": "123"
}
Comparação de Desempenho
Taxa de transferência aproximada (maior é melhor):
| Formato | Serializar | Desserializar | Tamanho |
|---|---|---|---|
| MessagePack | ★★★★★ | ★★★★★ | ★★★★★ |
| Protobuf | ★★★★★ | ★★★★★ | ★★★★★ |
| System.Text.Json (source gen) | ★★★★☆ | ★★★★☆ | ★★★☆☆ |
| System.Text.Json (reflexão) | ★★★☆☆ | ★★★☆☆ | ★★★☆☆ |
| Newtonsoft.Json | ★★☆☆☆ | ★★☆☆☆ | ★★★☆☆ |
Para caminhos críticos, prefira MessagePack ou Protobuf.
Serialização Akka.NET
Para sistemas de atores Akka.NET, use serialização baseada em esquema:
akka {
actor {
serializers {
messagepack = "Akka.Serialization.MessagePackSerializer, Akka.Serialization.MessagePack"
}
serialization-bindings {
"MyApp.Messages.IMessage, MyApp" = messagepack
}
}
}
Veja Documentação de Serialização Akka.NET.
Melhores Práticas
FAÇA
// Use source generators para System.Text.Json
[JsonSerializable(typeof(Order))]
public partial class AppJsonContext : JsonSerializerContext { }
// Use números/chaves de campo explícitos
[MessagePackObject]
public class Order
{
[Key(0)] public string Id { get; init; }
}
// Use records para tipos de mensagem imutáveis
public sealed record OrderCreated(OrderId Id, CustomerId CustomerId);
NÃO FAÇA
// Não use BinaryFormatter (nunca)
var formatter = new BinaryFormatter(); // Risco de segurança!
// Não embuta nomes de tipo em formato de transmissão
settings.TypeNameHandling = TypeNameHandling.All; // Quebra ao renomear!
// Não use serialização por reflexão para caminhos críticos
JsonConvert.SerializeObject(order); // Lento, não compatível com AOT
Recursos
- System.Text.Json Source Generation: https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json/source-generation
- Protocol Buffers: https://protobuf.dev/
- MessagePack-CSharp: https://github.com/MessagePack-CSharp/MessagePack-CSharp
- Akka.NET Serialization: https://getakka.net/articles/networking/serialization.html
- Wire Compatibility: https://getakka.net/community/contributing/wire-compatibility.html