Skapa Protobuf-meddelanden för .NET-appar

Anmärkning

Det här är inte den senaste versionen av den här artikeln. Den aktuella versionen finns i .NET 10-versionen av den här artikeln.

Varning

Den här versionen av ASP.NET Core stöds inte längre. Mer information finns i supportpolicyn för .NET och .NET Core. För den nuvarande utgåvan, se .NET 9-versionen av den här artikeln .

Av James Newton-King och Mark Rendle

gRPC använder Protobuf som gränssnittsdefinitionsspråk (IDL). Protobuf IDL är ett språkneutralt format för att ange meddelanden som skickas och tas emot av gRPC-tjänster. Protobuf-meddelanden definieras i .proto filer. Det här dokumentet förklarar hur Protobuf-begrepp mappas till .NET.

Protobuf-meddelanden

Meddelanden är huvudobjektet för dataöverföring i Protobuf. De liknar .NET-klasser konceptuellt.

syntax = "proto3";

option csharp_namespace = "Contoso.Messages";

message Person {
    int32 id = 1;
    string first_name = 2;
    string last_name = 3;
}  

Föregående meddelandedefinition anger tre fält som namn/värde-par. Precis som egenskaper för .NET-typer har varje fält ett namn och en typ. Fälttypen kan vara en Protobuf-skalär värdetyp, till exempel int32, eller ett annat meddelande.

Stilguiden för Protobuf rekommenderar att du använder underscore_separated_names för fältnamn. Nya Protobuf-meddelanden som skapats för .NET-appar bör följa riktlinjerna för Protobuf-format. .NET-verktyg genererar automatiskt .NET-typer som använder .NET-namngivningsstandarder. Ett Protobuf-fält genererar till exempel first_name en FirstName .NET-egenskap.

Förutom ett namn har varje fält i meddelandedefinitionen ett unikt tal. Fältnummer används för att identifiera fält när meddelandet serialiseras till Protobuf. Det går snabbare att serialisera ett litet tal än att serialisera hela fältnamnet. Eftersom fältnummer identifierar ett fält är det viktigt att vara försiktig när du ändrar dem. Mer information om hur du ändrar Protobuf-meddelanden finns i Versionshantering av gRPC-tjänster.

När en app skapas genererar Protobuf-verktyget .NET-typer från .proto filer. Meddelandet Person genererar en .NET-klass:

public class Person
{
    public int Id { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
}

Mer information om Protobuf-meddelanden finns i språkguiden för Protobuf.

Skalära värdetyper

Protobuf stöder ett antal interna skalära värdetyper. I följande tabell visas alla med motsvarande C#-typ:

Protobuf-typ C#-typ
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ärvärden har alltid ett standardvärde och kan inte anges till null. Den här begränsningen omfattar string och ByteString som är C#-klasser. string standardvärdet är ett tomt strängvärde och ByteString standardvärdet för tomma byte. Försök att ställa in dem på null genererar ett fel.

Omslutbara omslutningstyper kan användas för att stödja null-värden.

Datum och tider

De inbyggda skalära typerna anger inte datum- och tidsvärden, motsvarande . NET:s DateTimeOffset, DateTimeoch TimeSpan. Dessa typer kan anges med hjälp av några av Protobufs tillägg förWell-Known typer . Dessa tillägg ger stöd för kodgenerering och körning för komplexa fälttyper på de plattformar som stöds.

I följande tabell visas datum- och tidstyperna:

.NET-typ Protobuf Well-Known typ
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;
}  

De genererade egenskaperna i C#-klassen är inte .NET-datum- och tidstyper. Egenskaperna använder klasserna Timestamp och Duration i Google.Protobuf.WellKnownTypes namnområdet. Dessa klasser tillhandahåller metoder för att konvertera till och från DateTimeOffset, DateTimeoch 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();

Anmärkning

Typen Timestamp fungerar med UTC-tider. DateTimeOffset värden har alltid en förskjutning på noll och egenskapen DateTime.Kind är alltid DateTimeKind.Utc.

Typer som kan ogiltigas

Protobuf-kodgenereringen för C# använder de inbyggda typerna, till exempel int för int32. Så värdena inkluderas alltid och kan inte vara null.

För värden som kräver explicit null, till exempel att använda int? i C#-kod, innehåller Protobufs Well-Known Typer omslutningar som kompileras till nullbara C#-typer. Om du vill använda dem importerar wrappers.proto du till filen .proto , till exempel följande kod:

syntax = "proto3";

import "google/protobuf/wrappers.proto";

message Person {
    // ...
    google.protobuf.Int32Value age = 5;
}

wrappers.proto typer exponeras inte i genererade egenskaper. Protobuf mappar dem automatiskt till lämpliga .NET nullable-typer i C#-meddelanden. Ett fält genererar till exempel google.protobuf.Int32Value en int? egenskap. Referenstypegenskaper som string och ByteString är oförändrade förutom null kan tilldelas till dem utan fel.

I följande tabell visas en fullständig lista över omslutningstyper med motsvarande C#-typ:

C#-typ Well-Known Typomslutning
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

Binära nyttolaster stöds i Protobuf med bytes den skalära värdetypen. En genererad egenskap i C# använder ByteString som egenskapstyp.

Använd ByteString.CopyFrom(byte[] data) för att skapa en ny instans från en bytematris:

var data = await File.ReadAllBytesAsync(path);

var payload = new PayloadResponse();
payload.Data = ByteString.CopyFrom(data);

ByteString data nås direkt med hjälp av ByteString.Span eller ByteString.Memory. Eller anropa ByteString.ToByteArray() för att konvertera en instans tillbaka till en bytematris:

var payload = await client.GetPayload(new PayloadRequest());

await File.WriteAllBytesAsync(path, payload.Data.ToByteArray());

Decimaler

Protobuf har inte inbyggt stöd för .NET-typen decimal , bara double och float. Det pågår en diskussion i Protobuf-projektet om möjligheten att lägga till en standard decimaltyp i Well-Known Types, med plattformsstöd för språk och ramverk som stöder det. Ingenting har genomförts ännu.

Det går att skapa en meddelandedefinition som representerar den decimal typ som fungerar för säker serialisering mellan .NET-klienter och -servrar. Men utvecklare på andra plattformar måste förstå vilket format som används och implementera sin egen hantering för det.

Skapa en anpassad decimaltyp för 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;
}

Fältet nanos representerar värden från 0.999_999_999 till -0.999_999_999. Till exempel skulle värdet decimal1.5m representeras som { units = 1, nanos = 500_000_000 }. Därför använder nanos fältet sfixed32 i det här exemplet typen, som kodar mer effektivt än int32 för större värden. Om fältet units är negativt bör fältet nanos också vara negativt.

Anmärkning

Ytterligare algoritmer är tillgängliga för kodning av decimal värden som bytesträngar. Algoritmen som används av DecimalValue:

  • Är lätt att förstå.
  • Påverkas inte av big-endian eller little-endian på olika plattformar.
  • Stöder decimaltal som sträcker sig från positiva 9,223,372,036,854,775,807.999999999 till negativa 9,223,372,036,854,775,808.999999999 med en maximal precision på nio decimaler, vilket inte är hela intervallet för en decimal.

Konvertering mellan den här typen och BCL-typen decimal kan implementeras i C# så här:

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);
        }
    }
}

Föregående kod:

  • Lägger till en partiell klass för DecimalValue. Den partiella klassen kombineras med DecimalValue genererad från .proto filen. Den genererade klassen deklarerar Units egenskaperna och Nanos .
  • Har implicita operatorer för att konvertera mellan DecimalValue och BCL-typen decimal .

Collections

Lists

Listor i Protobuf anges med hjälp av nyckelordet repeated prefix i ett fält. I följande exempel visas hur du skapar en lista:

message Person {
    // ...
    repeated string roles = 8;
}

I den genererade koden repeated representeras fälten av den Google.Protobuf.Collections.RepeatedField<T> allmänna typen.

public class Person
{
    // ...
    public RepeatedField<string> Roles { get; }
}

RepeatedField<T> implementerar IList<T>. Så du kan använda LINQ-frågor eller konvertera dem till en matris eller en lista. RepeatedField<T> egenskaper har ingen offentlig setter. Objekt ska läggas till i den befintliga samlingen.

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);

Ordböcker

.NET-typen IDictionary<TKey,TValue> representeras i Protobuf med .map<key_type, value_type>

message Person {
    // ...
    map<string, string> attributes = 9;
}

I genererad .NET-kod map representeras fälten av den Google.Protobuf.Collections.MapField<TKey, TValue> allmänna typen. MapField<TKey, TValue> implementerar IDictionary<TKey,TValue>. Precis som repeated egenskaper map har egenskaperna ingen offentlig setter. Objekt ska läggas till i den befintliga samlingen.

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);

Ostrukturerade och villkorsstyrda meddelanden

Protobuf är ett meddelandeformat för kontrakt först. En apps meddelanden, inklusive dess fält och typer, måste anges i .proto filer när appen skapas. Protobufs kontrakt-första design är bra på att framtvinga meddelandeinnehåll men kan begränsa scenarier där ett strikt kontrakt inte krävs:

  • Meddelanden med okända nyttolaster. Till exempel ett meddelande med ett fält som kan innehålla valfritt meddelande.
  • Villkorsstyrda meddelanden. Ett meddelande som returneras från en gRPC-tjänst kan till exempel vara ett lyckat resultat eller ett felresultat.
  • Dynamiska värden. Till exempel ett meddelande med ett fält som innehåller en ostrukturerad samling värden, liknande JSON.

Protobuf erbjuder språkfunktioner och typer för att stödja dessa scenarier.

Vilken som helst

Med Any typen kan du använda meddelanden som inbäddade typer utan att ha deras .proto definition. Om du vill använda typen Any importerar du 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 fält är en språkfunktion. Kompilatorn hanterar nyckelordet oneof när det genererar meddelandeklassen. Använd oneof för att ange ett svarsmeddelande som antingen kan returnera en Person eller Error kan se ut så här:

message Person {
    // ...
}

message Error {
    // ...
}

message ResponseMessage {
  oneof result {
    Error error = 1;
    Person person = 2;
  }
}

Fält i uppsättningen oneof måste ha unika fältnummer i den övergripande meddelandedeklarationen.

När du använder oneofinnehåller den genererade C#-koden en uppräkning som anger vilket av fälten som har angetts. Du kan testa uppräkningen för att hitta vilket fält som har angetts. Fält som inte anges returnerar null eller standardvärdet, i stället för att utlösa ett undantag.

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.");
}

Värde

Typen Value representerar ett dynamiskt typat värde. Det kan vara antingen null, ett tal, en sträng, ett booleskt värde, en ordlista med värden (Struct) eller en lista med värden (ValueList). Value är en Protobuf-Well-Known typ som använder den tidigare diskuterade oneof funktionen. Om du vill använda typen Value importerar du 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;
    // ...
}

Att använda Value direkt kan vara utförligt. Ett annat sätt att använda Value är med Protobufs inbyggda stöd för att mappa meddelanden till JSON. Protobufs JsonFormatter och JsonWriter typer kan användas med alla Protobuf-meddelanden. Value passar särskilt bra för att konverteras till och från JSON.

Det här är JSON-motsvarigheten till den tidigare koden:

// 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);

Ytterligare resurser