Tworzenie komunikatów Protobuf dla aplikacji platformy .NET
Uwaga
Nie jest to najnowsza wersja tego artykułu. Aby zapoznać się z bieżącą wersją, zapoznaj się z wersją tego artykułu platformy .NET 8.
Ostrzeżenie
Ta wersja ASP.NET Core nie jest już obsługiwana. Aby uzyskać więcej informacji, zobacz .NET i .NET Core Support Policy (Zasady obsługi platformy .NET Core). Aby zapoznać się z bieżącą wersją, zapoznaj się z wersją tego artykułu platformy .NET 8.
Ważne
Te informacje odnoszą się do produktu w wersji wstępnej, który może zostać znacząco zmodyfikowany, zanim zostanie wydany komercyjnie. Firma Microsoft nie udziela żadnych gwarancji, jawnych lub domniemanych, w odniesieniu do informacji podanych w tym miejscu.
Aby zapoznać się z bieżącą wersją, zapoznaj się z wersją tego artykułu platformy .NET 8.
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, na przykład int32
, lub 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
, DateTime
i .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 null
int?
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());
Miejsca dziesiętne
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 ujemnych9,223,372,036,854,775,808.999999999
z maksymalną dokładnością dziewięciu miejsc dziesiętnych, które nie są pełnym zakresemdecimal
wartoś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 ma następujące działanie:
- Dodaje klasę częściową dla klasy
DecimalValue
. Klasa częściowa jest łączona zDecimalValue
wygenerowaną na podstawie.proto
pliku. Wygenerowana klasa deklarujeUnits
właściwości iNanos
. - Ma niejawne operatory do konwertowania między
DecimalValue
i typu listy BCLdecimal
.
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, podobny do kodu JSON.
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 Any
any.proto
element .
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 oneof
wygenerowany 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ć null
liczba, 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 Value
struct.proto
element .
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 do mapowania komunikatów na format JSON. Typy i JsonWriter
protobuf JsonFormatter
mogą być używane z dowolnym komunikatem Protobuf. Value
jest szczególnie odpowiedni do konwersji na i z formatu JSON.
Jest to odpowiednik JSON 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);