Tworzenie komunikatów Protobuf dla aplikacji platformy .NET

Przez James Newton-King i Mark Rendle

gRPC używa narzędzia Protobuf jako języka IDL (Interface Definition Language). Protobuf IDL to format neutralny dla języka określający komunikaty wysyłane i odbierane przez usługi gRPC. Komunikaty Protobuf są definiowane w .proto plikach. W tym dokumencie opisano sposób mapowania koncepcji narzędzia Protobuf na platformę .NET.

Komunikaty Protobuf

Komunikaty to główny obiekt transferu danych w narzędziu Protobuf. Są one koncepcyjnie podobne do klas platformy .NET.

syntax = "proto3";

option csharp_namespace = "Contoso.Messages";

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

Poprzednia definicja komunikatu określa trzy pola jako pary name-value. Podobnie jak właściwości w typach platformy .NET, każde pole ma nazwę i typ. Typ pola może być typem wartości skalarnej Protobuf, np. int32lub innym komunikatem.

Przewodnik po stylu Protobuf zaleca używanie underscore_separated_names nazw pól. Nowe komunikaty Protobuf utworzone dla aplikacji platformy .NET powinny być zgodne z wytycznymi dotyczącymi stylu Protobuf. Narzędzia platformy .NET automatycznie generują typy platformy .NET, które używają standardów nazewnictwa platformy .NET. Na przykład first_name pole Protobuf generuje FirstName właściwość .NET.

Oprócz nazwy każde pole w definicji komunikatu ma unikatową liczbę. Numery pól są używane do identyfikowania pól, gdy komunikat jest serializowany do protobuf. Serializacja małej liczby jest szybsza niż serializowanie całej nazwy pola. Ponieważ numery pól identyfikują pole, ważne jest, aby zachować ostrożność podczas ich zmieniania. Aby uzyskać więcej informacji na temat zmieniania komunikatów Protobuf, zobacz Przechowywanie wersji usług gRPC.

Gdy aplikacja zostanie skompilowana, narzędzie Protobuf generuje typy platformy .NET z .proto plików. Komunikat Person generuje klasę .NET:

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

Aby uzyskać więcej informacji na temat komunikatów Protobuf, zobacz Przewodnik po języku Protobuf.

Typy wartości skalarnych

Protobuf obsługuje zakres natywnych typów wartości skalarnych. W poniższej tabeli wymieniono je wszystkie z równoważnym typem języka C#:

Typ protobuf Typ języka 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

Wartości skalarne zawsze mają wartość domyślną i nie można jej ustawić na null. To ograniczenie obejmuje string klasy języka ByteString C#. string wartość domyślna to pusta wartość ciągu, a ByteString wartością domyślną jest pusta wartość bajtów. Próba ustawienia ich w celu null zgłoszenia błędu.

Typy otoki dopuszczane do wartości null mogą służyć do obsługi wartości null.

Daty i godziny

Natywne typy skalarne nie zapewniają wartości daty i godziny, co odpowiada wartościom . Net's DateTimeOffset, DateTimei TimeSpan. Te typy można określić przy użyciu niektórych rozszerzeń dobrze znanych typów Protobuf. Te rozszerzenia zapewniają obsługę generowania kodu i środowiska uruchomieniowego dla złożonych typów pól na obsługiwanych platformach.

W poniższej tabeli przedstawiono typy dat i godzin:

Typ platformy .NET Dobrze znany typ 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;
}  

Wygenerowane właściwości w klasie C# nie są typami daty i godziny platformy .NET. Właściwości używają Timestamp klas i Duration w Google.Protobuf.WellKnownTypes przestrzeni nazw. Klasy te udostępniają metody konwersji na i z DateTimeOffset, DateTimei .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();

Uwaga

Typ Timestamp działa z godzinami UTC. DateTimeOffset wartości zawsze mają przesunięcie zera, a DateTime.Kind właściwość jest zawsze DateTimeKind.Utc.

Typy dopuszczające wartości null

Generowanie kodu Protobuf dla języka C# używa typów natywnych, takich jak int dla .int32 Dlatego wartości są zawsze uwzględniane i nie mogą być null.

W przypadku wartości, które wymagają jawnego nullint? użycia w kodzie języka C#, dobrze znane typy Protobuf obejmują otoki, które są kompilowane do typów C# dopuszczających wartości null. Aby ich używać, zaimportuj wrappers.proto do .proto pliku, podobnie jak w poniższym kodzie:

syntax = "proto3";

import "google/protobuf/wrappers.proto";

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

wrappers.proto typy nie są widoczne we wygenerowanych właściwościach. Narzędzie Protobuf automatycznie mapuje je na odpowiednie typy dopuszczalne dla platformy .NET w komunikatach języka C#. Na przykład google.protobuf.Int32Value pole generuje int? właściwość. Właściwości typu odwołania, takie jak string i ByteString bez zmian, z wyjątkiem null tych właściwości można przypisać bez błędu.

W poniższej tabeli przedstawiono pełną listę typów otoki o równoważnym typie języka C#:

Typ języka C# Dobrze znana otoka typów
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

Ładunki binarne są obsługiwane w narzędziu Protobuf z typem wartości skalarnych bytes . Wygenerowana właściwość w języku C# jest używana ByteString jako typ właściwości.

Użyj ByteString.CopyFrom(byte[] data) polecenia , aby utworzyć nowe wystąpienie na podstawie tablicy bajtów:

var data = await File.ReadAllBytesAsync(path);

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

ByteString dostęp do danych jest uzyskiwany bezpośrednio przy użyciu polecenia ByteString.Span lub ByteString.Memory. Możesz też wywołać metodę ByteString.ToByteArray() , aby przekonwertować wystąpienie z powrotem na tablicę bajtów:

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

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

Miejsc dziesiętnych

Protobuf nie obsługuje natywnie typu platformy .NET decimal tylko double i float. W projekcie Protobuf o możliwości dodania standardowego typu dziesiętnego do dobrze znanych typów istnieje możliwość dodania standardowego typu dziesiętnego z obsługą platform dla języków i struktur, które go obsługują. Nic nie zostało jeszcze zaimplementowane.

Istnieje możliwość utworzenia definicji komunikatu reprezentującego decimal typ, który działa w celu zapewnienia bezpiecznej serializacji między klientami platformy .NET i serwerami. Jednak deweloperzy na innych platformach musieliby zrozumieć używany format i zaimplementować własną obsługę.

Tworzenie niestandardowego typu dziesiętnego dla 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 reprezentuje wartości z 0.999_999_999 do -0.999_999_999. Na przykład decimal wartość 1.5m będzie reprezentowana jako { units = 1, nanos = 500_000_000 }. nanos Dlatego pole w tym przykładzie używa sfixed32 typu , który koduje wydajniej niż int32 w przypadku większych wartości. units Jeśli pole jest ujemne, nanos pole powinno być również ujemne.

Uwaga

Dodatkowe algorytmy są dostępne dla wartości kodowania decimal jako ciągi bajtów. Algorytm używany przez :DecimalValue

  • Jest łatwa do zrozumienia.
  • Nie ma to wpływu na big-endian lub little-endian na różnych platformach.
  • Obsługuje liczby dziesiętne od dodatnich 9,223,372,036,854,775,807.999999999 do ujemnych 9,223,372,036,854,775,808.999999999 z maksymalną dokładnością dziewięciu miejsc dziesiętnych, które nie są pełnym zakresem decimalwartości .

Konwersja między tym typem a typem BCL decimal może zostać zaimplementowana w języku C# w następujący sposób:

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

Powyższy kod:

  • Dodaje klasę częściową dla klasy DecimalValue. Klasa częściowa jest łączona z DecimalValue wygenerowaną na podstawie .proto pliku. Wygenerowana klasa deklaruje Units właściwości i Nanos .
  • Ma niejawne operatory do konwertowania między DecimalValue i typu listy BCL decimal .

Kolekcje

Listy

Listy w narzędziu Protobuf są określane przy użyciu słowa kluczowego prefiksu repeated w polu. W poniższym przykładzie pokazano, jak utworzyć listę:

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

W wygenerowany kod repeated pola są reprezentowane przez Google.Protobuf.Collections.RepeatedField<T> typ ogólny.

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

RepeatedField<T> implementuje IList<T>. W związku z tym można użyć zapytań LINQ lub przekonwertować je na tablicę lub listę. RepeatedField<T> właściwości nie mają publicznego modułu ustawiania. Elementy należy dodać do istniejącej kolekcji.

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

Słowniki

Typ platformy .NET IDictionary<TKey,TValue> jest reprezentowany w narzędziu Protobuf przy użyciu polecenia map<key_type, value_type>.

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

W wygenerowany kod map platformy .NET pola są reprezentowane przez Google.Protobuf.Collections.MapField<TKey, TValue> typ ogólny. MapField<TKey, TValue> implementuje IDictionary<TKey,TValue>. Podobnie jak repeated właściwości, map właściwości nie mają publicznego modułu ustawiania. Elementy należy dodać do istniejącej kolekcji.

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

Komunikaty nieustrukturyzowane i warunkowe

Protobuf to format komunikatów z pierwszym kontraktem. Komunikaty aplikacji, w tym jej pola i typy, muszą być określone w .proto plikach podczas tworzenia aplikacji. Projekt protobuf's contract-first doskonale sprawdza się w wymuszaniu zawartości komunikatów, ale może ograniczać scenariusze, w których nie jest wymagana ścisła umowa:

  • Komunikaty z nieznanymi ładunkami. Na przykład komunikat z polem, które może zawierać dowolny komunikat.
  • Komunikaty warunkowe. Na przykład komunikat zwrócony z usługi gRPC może być wynikiem powodzenia lub wynikiem błędu.
  • Wartości dynamiczne. Na przykład komunikat z polem zawierającym nieustrukturyzowany zbiór wartości, podobnie jak JSWŁ.

Protobuf oferuje funkcje i typy językowe do obsługi tych scenariuszy.

Dowolne

Typ Any umożliwia używanie komunikatów jako typów osadzonych bez konieczności posiadania ich .proto definicji. Aby użyć typu, zaimportuj Anyany.protoelement .

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

Jedenof

oneof pola są funkcją języka. Kompilator obsługuje oneof słowo kluczowe podczas generowania klasy komunikatów. Użyj polecenia oneof , aby określić komunikat odpowiedzi, który może zwrócić Person element lub Error może wyglądać następująco:

message Person {
    // ...
}

message Error {
    // ...
}

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

Pola w oneof zestawie muszą zawierać unikatowe numery pól w ogólnej deklaracji komunikatu.

W przypadku korzystania z metody oneofwygenerowany kod języka C# zawiera wyliczenie określające, które pola zostały ustawione. Możesz przetestować wyliczenie, aby znaleźć, które pole jest ustawione. Pola, które nie są ustawione, zwracają null lub wartość domyślną, a nie zgłaszają wyjątku.

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

Wartość

Typ Value reprezentuje dynamicznie typową wartość. Może to być nullliczba, ciąg, wartość logiczna, słownik wartości (Struct) lub lista wartości (ValueList). Value jest dobrze znanym typem Protobuf, który używa wcześniej omówionej oneof funkcji. Aby użyć typu, zaimportuj Valuestruct.protoelement .

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;
    // ...
}

Użycie Value bezpośrednio może być pełne. Alternatywnym sposobem użycia Value jest wbudowana obsługa protokołu Protobuf na potrzeby mapowania komunikatów na JSWŁ. Typy i JsonWriter protobuf JsonFormatter mogą być używane z dowolnym komunikatem Protobuf. Value jest szczególnie odpowiedni do konwersji na i z JSON.

Jest JSto odpowiednik ON poprzedniego kodu:

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

Dodatkowe zasoby