Creare messaggi Protobuf per le app .NET
Nota
Questa non è la versione più recente di questo articolo. Per la versione corrente, vedere la versione .NET 9 di questo articolo.
Avviso
Questa versione di ASP.NET Core non è più supportata. Per altre informazioni, vedere i criteri di supporto di .NET e .NET Core. Per la versione corrente, vedere la versione .NET 9 di questo articolo.
Importante
Queste informazioni si riferiscono a un prodotto non definitive che può essere modificato in modo sostanziale prima che venga rilasciato commercialmente. Microsoft non riconosce alcuna garanzia, espressa o implicita, in merito alle informazioni qui fornite.
Per la versione corrente, vedere la versione .NET 9 di questo articolo.
Di James Newton-King e Mark Rendle
gRPC usa Protobuf come IDL (Interface Definition Language). Protobuf IDL è un formato indipendente dalla lingua per specificare i messaggi inviati e ricevuti dai servizi gRPC. I messaggi Protobuf sono definiti nei .proto
file. Questo documento illustra il mapping dei concetti protobuf a .NET.
Messaggi protobuf
I messaggi sono l'oggetto di trasferimento dati principale in Protobuf. Sono concettualmente simili alle classi .NET.
syntax = "proto3";
option csharp_namespace = "Contoso.Messages";
message Person {
int32 id = 1;
string first_name = 2;
string last_name = 3;
}
La definizione del messaggio precedente specifica tre campi come coppie nome-valore. Come le proprietà sui tipi .NET, ogni campo ha un nome e un tipo. Il tipo di campo può essere un tipo di valore scalare Protobuf, ad esempio int32
, o un altro messaggio.
La guida di stile Protobuf consiglia di usare underscore_separated_names
per i nomi dei campi. I nuovi messaggi Protobuf creati per le app .NET devono seguire le linee guida per lo stile Protobuf. Gli strumenti .NET generano automaticamente tipi .NET che usano standard di denominazione .NET. Ad esempio, un first_name
campo Protobuf genera una FirstName
proprietà .NET.
Oltre a un nome, ogni campo nella definizione del messaggio ha un numero univoco. I numeri di campo vengono usati per identificare i campi quando il messaggio viene serializzato in Protobuf. La serializzazione di un numero ridotto è più veloce rispetto alla serializzazione dell'intero nome del campo. Poiché i numeri di campo identificano un campo, è importante prestare attenzione quando vengono modificati. Per altre informazioni sulla modifica dei messaggi Protobuf, vedere Controllo delle versioni dei servizi gRPC.
Quando viene compilata un'app, gli strumenti Protobuf generano tipi .NET dai .proto
file. Il Person
messaggio genera una classe .NET:
public class Person
{
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
}
Per altre informazioni sui messaggi Protobuf, vedere la guida al linguaggio Protobuf.
Tipi valore scalari
Protobuf supporta un intervallo di tipi valore scalari nativi. Nella tabella seguente sono elencate tutti con il tipo C# equivalente:
Tipo Protobuf | Tipo 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 |
I valori scalari hanno sempre un valore predefinito e non possono essere impostati su null
. Questo vincolo include string
e ByteString
quali sono classi C#. string
il valore predefinito è un valore stringa vuoto e ByteString
il valore predefinito è un valore di byte vuoto. Il tentativo di impostarli per null
genera un errore.
I tipi wrapper nullable possono essere usati per supportare valori Null.
Date e ore
I tipi scalari nativi non forniscono valori di data e ora, equivalenti a . NET è DateTimeOffset, DateTimee TimeSpan. Questi tipi possono essere specificati usando alcune estensioni dei tipi noti di Protobuf. Queste estensioni forniscono il supporto di generazione e runtime del codice per i tipi di campo complessi nelle piattaforme supportate.
La tabella seguente mostra i tipi di data e ora:
Tipo .NET | Tipo noto 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;
}
Le proprietà generate nella classe C# non sono i tipi di data e ora .NET. Le proprietà usano le classi Timestamp
e Duration
nello spazio dei nomi Google.Protobuf.WellKnownTypes
. Queste classi forniscono metodi per la conversione in e da 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();
Nota
Il tipo Timestamp
funziona con le ore UTC. I valori DateTimeOffset
hanno sempre un offset pari a zero e la proprietà DateTime.Kind
è sempre DateTimeKind.Utc
.
Tipi nullable
La generazione di codice Protobuf per C# usa i tipi nativi, ad esempio int
per int32
. I valori sono quindi sempre inclusi e non possono essere null
.
Per i valori che richiedono espliciti null
, ad esempio l'uso int?
nel codice C#, i tipi noti di Protobuf includono wrapper compilati in tipi C# nullable. Per usarli, importare wrappers.proto
nel .proto
file, come nel codice seguente:
syntax = "proto3";
import "google/protobuf/wrappers.proto";
message Person {
// ...
google.protobuf.Int32Value age = 5;
}
wrappers.proto
I tipi non vengono esposti nelle proprietà generate. Protobuf esegue automaticamente il mapping a tipi nullable .NET appropriati nei messaggi C#. Ad esempio, un google.protobuf.Int32Value
campo genera una int?
proprietà . Le proprietà del tipo di riferimento come string
e ByteString
sono invariate, tranne null
che possono essere assegnate senza errori.
La tabella seguente mostra l'elenco completo dei tipi wrapper con il tipo C# equivalente:
Tipo C# | Wrapper di tipo noto |
---|---|
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 |
Byte
I payload binari sono supportati in Protobuf con il bytes
tipo di valore scalare. Una proprietà generata in C# usa ByteString
come tipo di proprietà.
Usare ByteString.CopyFrom(byte[] data)
per creare una nuova istanza da una matrice di byte:
var data = await File.ReadAllBytesAsync(path);
var payload = new PayloadResponse();
payload.Data = ByteString.CopyFrom(data);
ByteString
l'accesso ai dati viene eseguito direttamente tramite ByteString.Span
o ByteString.Memory
. In alternativa, chiamare ByteString.ToByteArray()
per convertire nuovamente un'istanza in una matrice di byte:
var payload = await client.GetPayload(new PayloadRequest());
await File.WriteAllBytesAsync(path, payload.Data.ToByteArray());
Decimali
Protobuf non supporta in modo nativo il tipo decimal
.NET, solo double
e float
. Nel progetto Protobuf è in corso una discussione sulla possibilità di aggiungere un tipo decimale standard ai tipi noti, con il supporto della piattaforma per linguaggi e framework che lo supportano. Non è ancora stato implementato alcun elemento.
È possibile creare una definizione di messaggio per rappresentare il decimal
tipo che funziona per la serializzazione sicura tra client .NET e server. Tuttavia, gli sviluppatori su altre piattaforme dovranno comprendere il formato usato e implementare la propria gestione.
Creazione di un tipo decimale personalizzato per 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;
}
Il campo nanos
rappresenta i valori da 0.999_999_999
a -0.999_999_999
. Ad esempio, il valore decimal
1.5m
verrebbe rappresentato come { units = 1, nanos = 500_000_000 }
. Questo è il motivo per cui il campo nanos
in questo esempio usa il tipo sfixed32
, che codifica in modo più efficiente rispetto a int32
per i valori più grandi. Se il campo units
è negativo, anche il campo nanos
deve essere negativo.
Nota
Sono disponibili algoritmi aggiuntivi per la codifica decimal
dei valori come stringhe di byte. Algoritmo usato da DecimalValue
:
- Facilità di comprensione
- Non è influenzato da big-endian o little-endian su piattaforme diverse.
- Supporta numeri decimali compresi tra positivi
9,223,372,036,854,775,807.999999999
e negativi9,223,372,036,854,775,808.999999999
con una precisione massima di nove cifre decimali, che non è l'intervallo completo di un oggettodecimal
.
La conversione tra questo tipo e il tipo decimal
BCL potrebbe essere implementata in C# come segue:
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);
}
}
}
Il codice precedente:
- Aggiunge una classe parziale per
DecimalValue
. La classe parziale viene combinata conDecimalValue
generata dal.proto
file . La classe generata dichiara leUnits
proprietà eNanos
. - Dispone di operatori impliciti per la conversione tra
DecimalValue
e il tipo BCLdecimal
.
Raccolte
Elenchi
Gli elenchi in Protobuf vengono specificati usando la repeated
parola chiave prefisso in un campo. L'esempio seguente illustra come creare un elenco:
message Person {
// ...
repeated string roles = 8;
}
Nel codice generato i repeated
campi sono rappresentati dal Google.Protobuf.Collections.RepeatedField<T>
tipo generico.
public class Person
{
// ...
public RepeatedField<string> Roles { get; }
}
RepeatedField<T>
implementa IList<T>. È quindi possibile usare query LINQ o convertirla in una matrice o in un elenco. RepeatedField<T>
le proprietà non hanno un setter pubblico. Gli elementi devono essere aggiunti alla raccolta esistente.
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);
Dizionari
Il tipo .NET IDictionary<TKey,TValue> è rappresentato in Protobuf usando map<key_type, value_type>
.
message Person {
// ...
map<string, string> attributes = 9;
}
Nel codice .NET generato i map
campi sono rappresentati dal Google.Protobuf.Collections.MapField<TKey, TValue>
tipo generico. MapField<TKey, TValue>
implementa IDictionary<TKey,TValue>. Come repeated
le proprietà, map
le proprietà non hanno un setter pubblico. Gli elementi devono essere aggiunti alla raccolta esistente.
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);
Messaggi non strutturati e condizionali
Protobuf è un formato di messaggistica contract-first. I messaggi di un'app, inclusi i relativi campi e tipi, devono essere specificati nei .proto
file al momento della compilazione dell'app. La progettazione contract-first di Protobuf è ideale per applicare il contenuto dei messaggi, ma può limitare gli scenari in cui non è necessario un contratto rigoroso:
- Messaggi con payload sconosciuti. Ad esempio, un messaggio con un campo che può contenere qualsiasi messaggio.
- Messaggi condizionali. Ad esempio, un messaggio restituito da un servizio gRPC potrebbe essere un risultato positivo o un risultato di errore.
- Valori dinamici. Ad esempio, un messaggio con un campo che contiene una raccolta non strutturata di valori, simile a JSON.
Protobuf offre funzionalità e tipi di linguaggio per supportare questi scenari.
Any
Il Any
tipo consente di usare i messaggi come tipi incorporati senza avere la relativa .proto
definizione. Per usare il Any
tipo , importare 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
oneof
i campi sono una funzionalità del linguaggio. Il compilatore gestisce la oneof
parola chiave quando genera la classe messaggio. L'uso oneof
di per specificare un messaggio di risposta che potrebbe restituire o Person
Error
potrebbe essere simile al seguente:
message Person {
// ...
}
message Error {
// ...
}
message ResponseMessage {
oneof result {
Error error = 1;
Person person = 2;
}
}
I campi all'interno del set oneof
devono avere numeri di campo univoci nella dichiarazione di messaggio complessiva.
Quando si usa oneof
, il codice C# generato include un'enumerazione che specifica quale dei campi è stato impostato. È possibile testare l'enumerazione per trovare il campo impostato. I campi che non sono impostati restituiscono null
o il valore predefinito, anziché generare un'eccezione.
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.");
}
Valore
Il Value
tipo rappresenta un valore tipizzato in modo dinamico. Può essere null
, un numero, una stringa, un valore booleano, un dizionario di valori (Struct
) o un elenco di valori (ValueList
). Value
è un tipo noto protobuf che usa la funzionalità descritta oneof
in precedenza. Per usare il Value
tipo , importare 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;
// ...
}
L'uso Value
diretto può essere dettagliato. Un modo alternativo da usare Value
consiste nel supporto predefinito di Protobuf per il mapping dei messaggi a JSON. I tipi e JsonWriter
protobuf JsonFormatter
possono essere usati con qualsiasi messaggio Protobuf. Value
è particolarmente adatto per la conversione in e da JSON.
Questo è l'equivalente JSON del codice precedente:
// 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);