Vytváření zpráv Protobuf pro aplikace .NET
James Newton-King a Mark Rendle
GRPC používá Protobuf jako svůj jazyk IDL (Interface Definition Language). Protobuf IDL je jazykově neutrální formát pro určení zpráv odesílaných a přijímýchých službami gRPC. Zprávy protobuf se definují v .proto souborech. Tento dokument vysvětluje, jak se koncepty Protobuf mapuje na .NET.
Zprávy ve formátu protobuf
Zprávy jsou hlavním objektem přenosu dat v Protobuf. Jsou koncepčně podobné třídám .NET.
syntax = "proto3";
option csharp_namespace = "Contoso.Messages";
message Person {
int32 id = 1;
string first_name = 2;
string last_name = 3;
}
Předchozí definice zprávy určuje tři pole jako páry název-hodnota. Podobně jako vlastnosti u typů .NET má každé pole název a typ. Typ pole může být skalární typ protobuf, například int32 , nebo jiná zpráva.
Průvodce stylem Protobuf doporučuje jako underscore_separated_names názvy polí použít . Nové zprávy Protobuf vytvořené pro aplikace .NET by měly dodržovat pokyny pro styl Protobuf. Nástroje .NET automaticky generují typy .NET, které používají standardy pojmenování .NET. Například pole first_name Protobuf vygeneruje FirstName vlastnost .NET.
Kromě názvu má každé pole v definici zprávy jedinečné číslo. Čísla polí se používají k identifikaci polí, pokud je zpráva serializována do protobuf. Serializace malého čísla je rychlejší než serializace celého názvu pole. Vzhledem k tomu, že čísla polí identifikují pole, je důležité při jejich změně dát pozor. Další informace o změně zpráv Protobuf najdete v tématu Správa verzí služeb gRPC .
Při sestavení aplikace nástroje Protobuf vygenerují ze souborů typy .proto .NET. Zpráva Person vygeneruje třídu .NET:
public class Person
{
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
}
Další informace o zprávách Protobuf najdete v příručce jazyka Protobuf.
Skalární hodnotové typy
Protobuf podporuje řadu nativních skalárních hodnotových typů. V následující tabulce jsou všechny uvedené s ekvivalentním typem jazyka C#:
| Typ Protobuf | Typ jazyka 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 |
Skalární hodnoty mají vždy výchozí hodnotu a nelze je nastavit na null . Toto omezení zahrnuje a string , což jsou třídy ByteString jazyka C#. string výchozí hodnota je prázdná řetězcová hodnota ByteString a výchozí hodnota je hodnota prázdných bajtů. Pokus o jejich nastavení null vyvolá chybu.
Typy obálky s možnou hodnotou null lze použít pro podporu hodnot null.
Data a časy
Nativní skalární typy neposkytují hodnoty data a času, což odpovídá . NET, DateTimeOffset DateTime , a TimeSpan . Tyto typy je možné zadat pomocí některých rozšíření známých typů Protobuf. Tato rozšíření poskytují podporu generování kódu a modulu runtime pro komplexní typy polí napříč podporovanými platformami.
Následující tabulka uvádí typy data a času:
| Typ .NET | Typ Well-Known 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;
}
Vygenerované vlastnosti ve třídě jazyka C# nejsou typy data a času .NET. Vlastnosti používají třídy Timestamp a v oboru názvů Duration Google.Protobuf.WellKnownTypes . Tyto třídy poskytují metody pro převod na a z DateTimeOffset DateTime , a 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();
Poznámka
Typ Timestamp funguje s časy UTC. DateTimeOffset Hodnoty mají vždy posun nuly a DateTime.Kind vlastnost je vždy DateTimeKind.Utc .
Typy nullable
Generování kódu Protobuf pro jazyk C# používá nativní typy, například int pro int32 . Hodnoty jsou proto vždy zahrnuty a nemůže být null .
Pro hodnoty, které vyžadují explicitní typ , například using v kódu jazyka C#, zahrnují typy Well-Known Protobuf obálky, které jsou zkompilovány do typů jazyka C# s možnou null int? hodnotou null. Pokud je chcete použít, wrappers.proto naimportujte je do .proto souboru, jak je vidět v následujícím kódu:
syntax = "proto3"
import "google/protobuf/wrappers.proto"
message Person {
// ...
google.protobuf.Int32Value age = 5;
}
wrappers.proto Typy nejsou ve vygenerované vlastnosti vystaveny. Protobuf je automaticky mapuje na odpovídající typy s možnou hodnotou null .NET ve zprávách jazyka C#. Například pole google.protobuf.Int32Value vygeneruje int? vlastnost . Vlastnosti typu odkazu, jako string jsou a , se ByteString nemění, s výjimkou null toho, že je lze přiřadit bez chyb.
Následující tabulka obsahuje úplný seznam typů obálky s ekvivalentním typem jazyka C#:
| Typ jazyka C# | Well-Known typ obálky |
|---|---|
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 |
Bajty
V Protobuf se podporují binární datové části s typem bytes skalární hodnoty. Vygenerovaná vlastnost v jazyce C# ByteString používá jako typ vlastnosti .
Pomocí ByteString.CopyFrom(byte[] data) vytvořte novou instanci z pole bajtů:
var data = await File.ReadAllBytesAsync(path);
var payload = new PayloadResponse();
payload.Data = ByteString.CopyFrom(data);
ByteString K datům se přistupuje přímo pomocí ByteString.Span nebo ByteString.Memory . Nebo ByteString.ToByteArray() voláním metody převeďte instanci zpět na pole bajtů:
var payload = await client.GetPayload(new PayloadRequest());
await File.WriteAllBytesAsync(path, payload.Data.ToByteArray());
Desetinná místa
Protobuf nativně nepodporuje typ decimal .NET, pouze a double float . V projektu Protobuf se průběžně diskutuje o možnosti přidání standardního typu desetinného čísla do typů Well-Known s podporou platforem pro jazyky a architektury, které ho podporují. Zatím se nic ne implementoval.
Je možné vytvořit definici zprávy reprezentující typ, který funguje pro bezpečnou decimal serializaci mezi klienty .NET a servery. Vývojáři na jiných platformách by ale muset porozumět používaném formátu a implementovat pro něj vlastní zpracování.
Vytvoření vlastního typu desetinného čísla pro 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;
}
Pole nanos představuje hodnoty od do 0.999_999_999 -0.999_999_999 . Například hodnota by decimal 1.5m byla reprezentována jako { units = 1, nanos = 500_000_000 } . To je důvod, nanos proč pole v tomto příkladu používá typ , který sfixed32 kóduje efektivněji než int32 pro větší hodnoty. Pokud je units pole záporné, nanos mělo by být také záporné.
Poznámka
Existuje několik dalších algoritmů pro kódování hodnot jako řetězců byte, ale tato zpráva je srozumitelnější decimal než kterákoli z nich. Tyto hodnoty nejsou ovlivněny big-endianem ani little-endianem na různých platformách.
Převod mezi tímto typem a typem seznamu BCL decimal může být implementován v jazyce C# tímto způsobem:
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 long Units { get; }
public int Nanos { get; }
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);
}
}
}
Kolekce
Seznamy
Seznamy v Protobuf jsou určené pomocí klíčového repeated slova prefix v poli. Následující příklad ukazuje, jak vytvořit seznam:
message Person {
// ...
repeated string roles = 8;
}
Ve vygenerované kódu jsou repeated pole reprezentována Google.Protobuf.Collections.RepeatedField<T> obecným typem.
public class Person
{
// ...
public RepeatedField<string> Roles { get; }
}
RepeatedField<T> implementuje IList<T> . Můžete tedy použít dotazy LINQ nebo je převést na pole nebo seznam. RepeatedField<T> Vlastnosti nemají veřejný setter. Položky by měly být přidány do existující kolekce.
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);
Slovníky
Typ .NET IDictionary<TKey,TValue> je reprezentován v Protobuf pomocí map<key_type, value_type> .
message Person {
// ...
map<string, string> attributes = 9;
}
Ve vygenerované kódu .NET jsou map pole reprezentována Google.Protobuf.Collections.MapField<TKey, TValue> obecným typem. MapField<TKey, TValue> implementuje IDictionary<TKey,TValue> . Podobně repeated jako vlastnosti nemají vlastnosti veřejný map setter. Položky by měly být přidány do existující kolekce.
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);
Nestrukturované a podmíněné zprávy
Protobuf je formát zasílání zpráv první kontrakt. Zprávy aplikace, včetně jejích polí a typů, musí být při jejím sestavování zadány v .proto souborech. Návrh protobuf s prvním kontraktem skvěle vynucuje obsah zpráv, ale může omezit scénáře, kdy se nevyžaduje striktní kontrakt:
- Zprávy s neznámými daty Například zpráva s polem, které může obsahovat libovolnou zprávu.
- Podmíněné zprávy. Například zpráva vrácená ze služby gRPC může být výsledkem úspěchu nebo výsledkem chyby.
- Dynamické hodnoty. Například zpráva s polem, které obsahuje nestrukturované kolekce hodnot, podobně jako JSON.
Protobuf nabízí jazykové funkce a typy pro podporu těchto scénářů.
Všechny
AnyTyp umožňuje používat zprávy jako vložené typy bez nutnosti jejich .proto definice. Chcete-li použít Any typ, importujte 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 pole jsou funkcí jazyka. Kompilátor zpracovává oneof klíčové slovo, když generuje třídu zprávy. Pomocí oneof můžete zadat zprávu odpovědi, která by mohla vracet, Person nebo Error může vypadat takto:
message Person {
// ...
}
message Error {
// ...
}
message ResponseMessage {
oneof result {
Error error = 1;
Person person = 2;
}
}
Pole v oneof sadě musí mít v celkové deklaraci zprávy jedinečné číselné pole.
Při použití oneof , vygenerovaný kód jazyka C# obsahuje výčet, který určuje, která z polí byla nastavena. Chcete-li zjistit, které pole je nastaveno, můžete otestovat výčet. Pole, která nejsou nastavena jako návratová null hodnota nebo výchozí hodnotu, namísto vyvolání výjimky.
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.");
}
Hodnota
ValueTyp představuje dynamicky typované hodnoty. Může to být buď null číslo, řetězec, logická hodnota, slovník hodnot ( Struct ) nebo seznam hodnot ( ValueList ). Value je Protobuf typ Well-Known, který používá dříve popisovanou oneof funkci. Chcete-li použít Value typ, importujte struct.proto .
import "google/protobuf/struct.proto";
message Status {
// ...
google.protobuf.Value data = 3;
}
// Create dynamic values.
var status = new Status();
status.Data = Value.FromStruct(new Struct
{
Fields =
{
["enabled"] = Value.ForBoolean(true),
["metadata"] = Value.ForList(
Value.FromString("value1"),
Value.FromString("value2"))
}
});
// Read dynamic values.
switch (status.Data.KindCase)
{
case Value.KindOneofCase.StructValue:
foreach (var field in status.Data.StructValue.Fields)
{
// Read struct fields...
}
break;
// ...
}
ValuePřímé použití může být podrobné. Alternativním způsobem, jak použít, Value je integrovanou podporu Protobuf pro mapování zpráv do formátu JSON. Protobuf JsonFormatter a JsonWriter typy lze použít s libovolnou Protobuf zprávou. Value je vhodný zejména pro převod na JSON a z něj.
Toto je ekvivalent JSON předchozího kódu:
// 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);