Criar mensagens do Protobuf para aplicativos .NET
Observação
Esta não é a versão mais recente deste artigo. Para a versão atual, consulte a versão .NET 9 deste artigo.
Aviso
Esta versão do ASP.NET Core não tem mais suporte. Para obter mais informações, confira .NET e a Política de Suporte do .NET Core. Para informações sobre a versão vigente, confira a Versão do .NET 8 deste artigo.
Importante
Essas informações relacionam-se ao produto de pré-lançamento, que poderá ser substancialmente modificado antes do lançamento comercial. A Microsoft não oferece nenhuma garantia, explícita ou implícita, quanto às informações fornecidas aqui.
Para a versão atual, consulte a versão .NET 9 deste artigo.
Por James Newton-King e Mark Rendle
O gRPC usa o Protobuf como sua linguagem IDL. A IDL do Protobuf é um formato neutro de linguagem para especificar as mensagens enviadas e recebidas pelos serviços gRPC. As mensagens do Protobuf são definidas nos arquivos .proto
. Este documento explica como os conceitos do Protobuf são mapeados para o .NET.
Mensagens de Protobuf
As mensagens são o objeto principal da transferência de dados no Protobuf. Elas são conceitualmente semelhantes às classes do .NET.
syntax = "proto3";
option csharp_namespace = "Contoso.Messages";
message Person {
int32 id = 1;
string first_name = 2;
string last_name = 3;
}
A definição de mensagem anterior especifica três campos como pares nome-valor. Assim como as propriedades em tipos .NET, cada campo tem um nome e um tipo. O tipo de campo pode ser um tipo de valor escalar do Protobuf, por exemplo, int32
, ou outra mensagem.
O guia de estilo do Protobuf recomenda usar underscore_separated_names
para nomes de campo. Novas mensagens do Protobuf criadas para aplicativos .NET devem seguir as diretrizes de estilo do Protobuf. As ferramentas do .NET geram automaticamente tipos .NET que usam padrões de nomenclatura do .NET. Por exemplo, um campo first_name
do Protobuf gera uma propriedade FirstName
do .NET.
Além de um nome, cada campo na definição de mensagem tem um número exclusivo. Os números de campo são usados para identificar campos quando a mensagem é serializada para o Protobuf. Serializar um número pequeno é mais rápido do que serializar o nome do campo inteiro. Como os números de campo identificam um campo, é importante tomar cuidado ao alterá-los. Para obter mais informações sobre como alterar mensagens do Protobuf, consulte Controle de versão de serviços gRPC.
Quando um aplicativo é criado, as ferramentas do Protobuf geram tipos .NET dos arquivos .proto
. A mensagem Person
gera uma classe .NET:
public class Person
{
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
}
Para obter mais informações sobre mensagens do Protobuf, consulte o guia de linguagem do Protobuf.
Tipos de valor escalar
O Protobuf dá suporte a uma série de tipos de valor escalar nativos. A tabela a seguir lista todos eles com o tipo C# equivalente:
Tipo de Protobuf | Tipo de C# |
---|---|
double |
double |
float |
float |
int32 |
int |
int64 |
long |
uint32 |
uint |
uint64 |
ulong |
sint32 |
int |
sint64 |
long |
fixed32 |
uint |
fixed64 |
ulong |
sfixed32 |
int |
sfixed64 |
long |
bool |
bool |
string |
string |
bytes |
ByteString |
Os valores escalares sempre têm um valor padrão e não podem ser definidos como null
. Essa restrição inclui string
e ByteString
, os quais são classes C#. string
usa como padrão um valor de cadeia de caracteres vazio e ByteString
usa como padrão um valor de bytes vazio. Tentar defini-los para null
gera um erro.
Tipos de wrapper anuláveis podem ser usados para dar suporte a valores nulos.
Datas e horas
Os tipos escalares nativos não fornecem valores de data e hora, equivalentes a DateTimeOffset, DateTime e TimeSpan do .NET. Esses tipos podem ser especificados usando algumas das extensões de Tipos Conhecidos do Protobuf. Essas extensões fornecem suporte de geração de código e runtime a tipos de campo complexos nas plataformas compatíveis.
A tabela a seguir mostra os tipos de data e hora:
Tipo .NET | Tipo conhecido do Protobuf |
---|---|
DateTimeOffset |
google.protobuf.Timestamp |
DateTime |
google.protobuf.Timestamp |
TimeSpan |
google.protobuf.Duration |
syntax = "proto3";
import "google/protobuf/duration.proto";
import "google/protobuf/timestamp.proto";
message Meeting {
string subject = 1;
google.protobuf.Timestamp start = 2;
google.protobuf.Duration duration = 3;
}
As propriedades geradas na classe de C# não são os tipos de data e hora do .NET. As propriedades usam as classes Timestamp
e Duration
no namespace Google.Protobuf.WellKnownTypes
. Essas classes fornecem métodos de conversão entre DateTimeOffset
, DateTime
e TimeSpan
.
// Create Timestamp and Duration from .NET DateTimeOffset and TimeSpan.
var meeting = new Meeting
{
Time = Timestamp.FromDateTimeOffset(meetingTime), // also FromDateTime()
Duration = Duration.FromTimeSpan(meetingLength)
};
// Convert Timestamp and Duration to .NET DateTimeOffset and TimeSpan.
var time = meeting.Time.ToDateTimeOffset();
var duration = meeting.Duration?.ToTimeSpan();
Observação
O tipo Timestamp
funciona com horários UTC. Os valores de DateTimeOffset
sempre têm um deslocamento de zero e a propriedade DateTime.Kind
é sempre DateTimeKind.Utc
.
Tipos anuláveis
A geração de código de Protobuf para C# usa os tipos nativos, como int
para int32
. Portanto, os valores são sempre incluídos e não podem ser null
.
Para valores que exigem null
explícito, como ao usar int?
no código de C#, os Tipos Conhecidos do Protobuf incluem wrappers compilados para tipos de C# anuláveis. Para usá-los, importe wrappers.proto
para o arquivo .proto
, como o código a seguir:
syntax = "proto3";
import "google/protobuf/wrappers.proto";
message Person {
// ...
google.protobuf.Int32Value age = 5;
}
Os tipos wrappers.proto
não são expostos em propriedades geradas. O Protobuf os mapeia automaticamente para tipos anuláveis apropriados do .NET em mensagens de C#. Por exemplo, um campo google.protobuf.Int32Value
gera uma propriedade int?
. As propriedades de tipo de referência, como string
e ByteString
, não são alteradas, exceto null
podem ser atribuídas a elas sem erro.
A tabela a seguir mostra a lista completa de tipos de wrapper com seu tipo de C# equivalente:
Tipo de C# | Wrapper de tipo conhecido |
---|---|
bool? |
google.protobuf.BoolValue |
double? |
google.protobuf.DoubleValue |
float? |
google.protobuf.FloatValue |
int? |
google.protobuf.Int32Value |
long? |
google.protobuf.Int64Value |
uint? |
google.protobuf.UInt32Value |
ulong? |
google.protobuf.UInt64Value |
string |
google.protobuf.StringValue |
ByteString |
google.protobuf.BytesValue |
Bytes
Há suporte para conteúdos binários no Protobuf com o tipo de valor escalar bytes
. Uma propriedade gerada em C# usa ByteString
como o tipo de propriedade.
Use ByteString.CopyFrom(byte[] data)
para criar uma nova instância de uma matriz de bytes:
var data = await File.ReadAllBytesAsync(path);
var payload = new PayloadResponse();
payload.Data = ByteString.CopyFrom(data);
Os dados ByteString
são acessados diretamente usando ByteString.Span
ou ByteString.Memory
. Ou chame ByteString.ToByteArray()
para converter uma instância de volta em uma matriz de bytes:
var payload = await client.GetPayload(new PayloadRequest());
await File.WriteAllBytesAsync(path, payload.Data.ToByteArray());
Decimais
O Protobuf não dá suporte nativo ao tipo decimal
do .NET, apenas a double
e float
. Há uma discussão em andamento no projeto do Protobuf sobre a possibilidade de adicionar um tipo padrão decimal aos tipos conhecidos, com suporte da plataforma para linguagens e estruturas compatíveis com ele. Nada foi implementado ainda.
É possível criar uma definição de mensagem para representar o tipo decimal
, que funciona para uma serialização segura entre clientes e servidores .NET. Mas os desenvolvedores de outras plataformas teriam que entender o formato que está sendo usado e implementar o próprio processamento dele.
Criando um tipo decimal personalizado para Protobuf
package CustomTypes;
// Example: 12345.6789 -> { units = 12345, nanos = 678900000 }
message DecimalValue {
// Whole units part of the amount
int64 units = 1;
// Nano units of the amount (10^-9)
// Must be same sign as units
sfixed32 nanos = 2;
}
O campo nanos
representa valores de 0.999_999_999
a -0.999_999_999
. Por exemplo, o valor decimal
1.5m
seria representado como { units = 1, nanos = 500_000_000 }
. É por isso que o campo nanos
neste exemplo usa o tipo sfixed32
, que codifica com mais eficiência do que int32
para valores maiores. Se o campo units
for negativo, o campo nanos
também deverá ser negativo.
Observação
Algoritmos adicionais estão disponíveis para codificação de valores decimal
como cadeias de caracteres de bytes. O algoritmo usado por DecimalValue
:
- É fácil de entender.
- Não é afetado por big-endian ou little-endian em diferentes plataformas.
- Dá suporte a números decimais que variam de positivo
9,223,372,036,854,775,807.999999999
a negativo9,223,372,036,854,775,808.999999999
com uma precisão máxima de nove casas decimais, que não é o intervalo completo de umdecimal
.
A conversão entre esse tipo e o tipo BCL decimal
pode ser implementada em C# da seguinte maneira:
namespace CustomTypes
{
public partial class DecimalValue
{
private const decimal NanoFactor = 1_000_000_000;
public DecimalValue(long units, int nanos)
{
Units = units;
Nanos = nanos;
}
public static implicit operator decimal(CustomTypes.DecimalValue grpcDecimal)
{
return grpcDecimal.Units + grpcDecimal.Nanos / NanoFactor;
}
public static implicit operator CustomTypes.DecimalValue(decimal value)
{
var units = decimal.ToInt64(value);
var nanos = decimal.ToInt32((value - units) * NanoFactor);
return new CustomTypes.DecimalValue(units, nanos);
}
}
}
O código anterior:
- Adiciona uma classe parcial para
DecimalValue
. A classe parcial é combinada comDecimalValue
gerada a partir do arquivo.proto
. A classe gerada declara as propriedadesUnits
eNanos
. - Tem operadores implícitos para converter entre
DecimalValue
e o tipo de BCLdecimal
.
Coleções
Listas
As listas no Protobuf são especificadas usando a palavra-chave com prefixo repeated
em um campo. O seguinte exemplo mostra como criar uma lista:
message Person {
// ...
repeated string roles = 8;
}
No código gerado, os campos repeated
são representados pelo tipo genérico Google.Protobuf.Collections.RepeatedField<T>
.
public class Person
{
// ...
public RepeatedField<string> Roles { get; }
}
RepeatedField<T>
implementa IList<T>. Portanto, você pode usar consultas LINQ ou convertê-lo em uma matriz ou em uma lista. As propriedades RepeatedField<T>
não têm um setter público. Os itens devem ser adicionados à coleção existente.
var person = new Person();
// Add one item.
person.Roles.Add("user");
// Add all items from another collection.
var roles = new [] { "admin", "manager" };
person.Roles.Add(roles);
Dicionários
O tipo IDictionary<TKey,TValue> do .NET é representado no Protobuf usando map<key_type, value_type>
.
message Person {
// ...
map<string, string> attributes = 9;
}
No código gerado do .NET, os campos map
são representados pelo tipo genérico Google.Protobuf.Collections.MapField<TKey, TValue>
. MapField<TKey, TValue>
implementa IDictionary<TKey,TValue>. Assim como as propriedades repeated
, as propriedades map
não têm um setter público. Os itens devem ser adicionados à coleção existente.
var person = new Person();
// Add one item.
person.Attributes["created_by"] = "James";
// Add all items from another collection.
var attributes = new Dictionary<string, string>
{
["last_modified"] = DateTime.UtcNow.ToString()
};
person.Attributes.Add(attributes);
Mensagens não estruturadas e condicionais
O Protobuf é um formato de mensagens de primeiro contrato. As mensagens de um aplicativo, incluindo seus campos e tipos, devem ser especificadas nos arquivos .proto
quando o aplicativo é criado. O design de primeiro contrato do Protobuf é ótimo para impor o conteúdo da mensagem, mas pode limitar cenários em que um contrato estrito não é necessário:
- Mensagens com conteúdos desconhecidos. Por exemplo, uma mensagem com um campo que pode conter qualquer mensagem.
- Mensagens condicionais. Por exemplo, uma mensagem retornada de um serviço gRPC pode ser um resultado de êxito ou um resultado de erro.
- Valores dinâmicos. Por exemplo, uma mensagem com um campo que contém uma coleção não estruturada de valores, semelhante a JSON.
O Protobuf oferece recursos e tipos de linguagem para dar suporte a esses cenários.
Qualquer
O tipo Any
permite que você use mensagens como tipos inseridos sem ter sua definição .proto
. Para usar o tipo Any
, importe any.proto
.
import "google/protobuf/any.proto";
message Status {
string message = 1;
google.protobuf.Any detail = 2;
}
// Create a status with a Person message set to detail.
var status = new ErrorStatus();
status.Detail = Any.Pack(new Person { FirstName = "James" });
// Read Person message from detail.
if (status.Detail.Is(Person.Descriptor))
{
var person = status.Detail.Unpack<Person>();
// ...
}
Oneof
Os campos oneof
são um recurso de linguagem. O compilador manipula a palavra-chave oneof
quando gera a classe de mensagem. Usar oneof
para especificar uma mensagem de resposta que pode retornar um Person
ou Error
pode ter esta aparência:
message Person {
// ...
}
message Error {
// ...
}
message ResponseMessage {
oneof result {
Error error = 1;
Person person = 2;
}
}
Os campos dentro do conjunto oneof
deverão ter números de campo exclusivos na declaração geral da mensagem.
Ao usar um oneof
, o código de C# gerado incluirá uma enumeração que especificará qual dos campos foi definido. É possível testar a enumeração para descobrir qual campo está definido. Campos que não estiverem definidos retornarão null
ou o valor padrão, em vez de gerar uma exceção.
var response = await client.GetPersonAsync(new RequestMessage());
switch (response.ResultCase)
{
case ResponseMessage.ResultOneofCase.Person:
HandlePerson(response.Person);
break;
case ResponseMessage.ResultOneofCase.Error:
HandleError(response.Error);
break;
default:
throw new ArgumentException("Unexpected result.");
}
Valor
O tipo Value
representa um valor tipado dinamicamente. Pode ser null
, um número, uma cadeia de caracteres, um booliano, um dicionário de valores (Struct
) ou uma lista de valores (ValueList
). Value
é um tipo conhecido do Protobuf que usa o recurso oneof
discutido anteriormente. Para usar o tipo Value
, importe struct.proto
.
import "google/protobuf/struct.proto";
message Status {
// ...
google.protobuf.Value data = 3;
}
// Create dynamic values.
var status = new Status();
status.Data = Value.ForStruct(new Struct
{
Fields =
{
["enabled"] = Value.ForBool(true),
["metadata"] = Value.ForList(
Value.ForString("value1"),
Value.ForString("value2"))
}
});
// Read dynamic values.
switch (status.Data.KindCase)
{
case Value.KindOneofCase.StructValue:
foreach (var field in status.Data.StructValue.Fields)
{
// Read struct fields...
}
break;
// ...
}
Usar Value
diretamente pode ser verbose. Uma maneira alternativa de usar Value
é com o suporte interno do Protobuf para mapear mensagens para JSON. Os tipos JsonFormatter
e JsonWriter
do Protobuf podem ser usados com qualquer mensagem do Protobuf. Value
é particularmente adequado para ser convertido de e para JSON.
Esse é o JSON equivalente do código anterior:
// Create dynamic values from JSON.
var status = new Status();
status.Data = Value.Parser.ParseJson(@"{
""enabled"": true,
""metadata"": [ ""value1"", ""value2"" ]
}");
// Convert dynamic values to JSON.
// JSON can be read with a library like System.Text.Json or Newtonsoft.Json
var json = JsonFormatter.Default.Format(status.Data);
var document = JsonDocument.Parse(json);