Erstellen von Protobuf-Nachrichten für .NET-Apps
Hinweis
Dies ist nicht die neueste Version dieses Artikels. Die aktuelle Version finden Sie in der .NET 9-Version dieses Artikels.
Warnung
Diese Version von ASP.NET Core wird nicht mehr unterstützt. Weitere Informationen finden Sie in der Supportrichtlinie für .NET und .NET Core. Informationen zum aktuellen Release finden Sie in der .NET 8-Version dieses Artikels.
Wichtig
Diese Informationen beziehen sich auf ein Vorabversionsprodukt, das vor der kommerziellen Freigabe möglicherweise noch wesentlichen Änderungen unterliegt. Microsoft gibt keine Garantie, weder ausdrücklich noch impliziert, hinsichtlich der hier bereitgestellten Informationen.
Die aktuelle Version finden Sie in der .NET 9-Version dieses Artikels.
Von James Newton-King und Mark Rendle
gRPC verwendet Protobuf als Interface Definition Language (IDL). Protobuf-IDL ist ein sprachunabhängiges Format zum Angeben der Nachrichten, die von gRPC-Diensten gesendet und empfangen werden. Protobuf-Nachrichten werden in .proto
-Dateien definiert. In dieser Dokumentation wird erläutert, wie Protobuf-Konzepte auf .NET übertragen werden können.
Protobuf-Nachrichten
Nachrichten sind das primäre Datenübertragungsobjekt in Protobuf. Sie sind konzeptionell mit .NET-Klassen vergleichbar.
syntax = "proto3";
option csharp_namespace = "Contoso.Messages";
message Person {
int32 id = 1;
string first_name = 2;
string last_name = 3;
}
In der obigen Nachrichtendefinition werden drei Felder als Name/Wert-Paare angegeben. Wie Eigenschaften von .NET-Typen verfügt jedes Feld über einen Namen und einen Typ. Der Feldtyp kann ein Protobuf-Skalarwerttyp sein, zum Beispiel int32
, oder eine andere Nachricht.
Im Protobuf-Styleguide wird underscore_separated_names
für die Feldnamen empfohlen. Die neuen Protobuf-Nachrichten, die für .NET-Apps erstellt wurden, sollten den Protobuf-Stilrichtlinien entsprechen. .NET-Tools generieren automatisch .NET-Typen, die .NET-Benennungsstandards verwenden. Beispielsweise generiert ein Protobuf-Feld first_name
die .NET-Eigenschaft FirstName
.
Zusätzlich zu einem Namen verfügt jedes Feld in der Nachrichtendefinition über eine eindeutige Zahl. Feldzahlen werden dazu verwendet, Felder zu identifizieren, wenn die Nachricht an Protobuf serialisiert wird. Die Serialisierung einer kleinen Zahl ist schneller als die Serialisierung des gesamten Feldnamens. Da Feldzahlen zur Identifikation eines Felds dienen, ist es wichtig, dass Änderung mit Bedacht vorgenommen werden. Weitere Informationen zum Ändern von Protobuf-Nachrichten finden Sie unter Versionsverwaltung für gRPC-Dienstes.
Wenn eine App erstellt wird, generieren die Protobuf-Tools .NET-Typen aus .proto
-Dateien. Die Person
-Nachricht generiert eine .NET-Klasse:
public class Person
{
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
}
Weitere Informationen über Protobuf-Nachrichten finden Sie im Leitfaden zur Protobuf-Sprache.
Skalarwerttypen
Protobuf unterstützt eine Reihe nativer Skalarwerttypen. In der folgenden Tabelle werden alle dieser Typen mit dem entsprechenden C#-Typ aufgeführt:
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 |
Skalarwerte weisen immer einen Standardwert auf und können nicht auf null
festgelegt werden. Diese Einschränkung umfasst string
und ByteString
, die C#-Klassen sind. string
ist standardmäßig ein leerer Zeichenfolgenwert, ByteString
ist standardmäßig ein leerer Bytewert. Wenn Sie versuchen, diese Angaben auf null
festzulegen, wird ein Fehler ausgelöst.
Nullable-Wrappertypen können zur Unterstützung von NULL-Werten verwendet werden.
Datums- und Zeitangaben
Die nativen Skalartypen stellen wie die .NET-Typen DateTimeOffset, DateTime und TimeSpan keine Datums- und Zeitwerte bereit. Diese Typen können mithilfe der Protobuf-Erweiterung Well Known Types angegeben werden. Diese Erweiterungen bieten Codegenerierung und Runtime-Unterstützung für komplexe Feldtypen für die unterstützten Plattformen.
In der folgenden Tabelle werden die Datums- und Zeittypen aufgeführt:
.NET-Typ | Bekannter Protobuf-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;
}
Die in der C#-Klasse generierten Eigenschaften sind nicht die Datums- und Zeittypen von .NET. Die Eigenschaften nutzen die Klassen Timestamp
und Duration
im Namespace Google.Protobuf.WellKnownTypes
. Diese Klassen stellen Methoden zum Konvertieren in und aus DateTimeOffset
, DateTime
und TimeSpan
bereit.
// 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();
Hinweis
Der Timestamp
-Typ kann mit UTC-Zeiten verwendet werden. DateTimeOffset
-Werte weisen immer ein Offset von 0 (null) auf, und die DateTime.Kind
-Eigenschaft ist immer DateTimeKind.Utc
.
Nullable-Typen
Bei der Protobuf-Codegenerierung für C# werden die nativen Typen verwendet, z. B. int
für int32
. Daher können die Werte, die immer enthalten sind, nicht null
sein.
Für Werte, die einen expliziten null
-Wert erfordern, z. B. int?
in C#-Code, umfasst die Protobuf-Erweiterung „Well Known Types“ Wrapper, die in C#-Nullable-Typen kompiliert werden. Importieren Sie wrappers.proto
in Ihre .proto
-Datei wie im folgenden Code, um sie zu verwenden:
syntax = "proto3";
import "google/protobuf/wrappers.proto";
message Person {
// ...
google.protobuf.Int32Value age = 5;
}
wrappers.proto
-Typen werden in den generierten Eigenschaften nicht verfügbar gemacht. Protobuf ordnet diese automatisch entsprechenden .NET-Nullable-Typen in C#-Nachrichten zu. Beispielsweise generiert ein google.protobuf.Int32Value
-Feld eine int?
-Eigenschaft. Verweistypeigenschaften wie string
und ByteString
bleiben unverändert, außer dass null
ihnen ohne Fehler zugewiesen werden kann.
In der folgenden Tabelle finden Sie eine vollständige Liste der Wrappertypen mit dem entsprechenden C#-Typ:
C#-Typ | Wrapper für bekannte Typen |
---|---|
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äre Nutzlasten werden in Protobuf mit dem bytes
-Skalarenwerttyp unterstützt. Eine generierte Eigenschaft in C# verwendet ByteString
als Eigenschaftstyp.
Verwenden Sie ByteString.CopyFrom(byte[] data)
, um eine neue Instanz aus einem Bytearray zu erstellen:
var data = await File.ReadAllBytesAsync(path);
var payload = new PayloadResponse();
payload.Data = ByteString.CopyFrom(data);
Der Zugriff auf ByteString
-Daten erfolgt direkt über ByteString.Span
oder ByteString.Memory
. Oder rufen Sie ByteString.ToByteArray()
auf, um eine Instanz zurück in ein Bytearray zu konvertieren:
var payload = await client.GetPayload(new PayloadRequest());
await File.WriteAllBytesAsync(path, payload.Data.ToByteArray());
Dezimalstellen
Protobuf unterstützt den .NET-Typ decimal
nicht nativ, nur double
und float
. Im Protobuf-Projekt gibt es eine fortlaufende Diskussion darüber, einen Standarddezimaltyp mit Plattformunterstützung für Sprachen und Frameworks, die den Typ unterstützen, zur Erweiterung „Well-Known Types“ hinzuzufügen. Bisher wurde keine Implementierung vorgenommen.
Eine Nachrichtendefinition zum Darstellen des decimal
-Typs kann erstellt werden, die für die sichere Serialisierung zwischen .NET-Clients und -Servern verwendet werden kann. Entwickler anderer Plattformen müssten jedoch das verwendete Format verstehen und ihr eigenes Format implementieren, um dieses zu verarbeiten.
Erstellen eines benutzerdefinierten Dezimaltyps 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;
}
Das nanos
-Feld stellt Werte von 0.999_999_999
bis -0.999_999_999
dar. Beispielsweise würde der decimal
-Wert 1.5m
als { units = 1, nanos = 500_000_000 }
dargestellt werden. Aus diesem Grund wird in diesem Beispiel für das nanos
-Feld der sfixed32
-Typ verwendet, der bei größeren Werten effizienter als int32
codiert wird. Wenn das units
-Feld negativ ist, sollte auch das nanos
-Feld negativ sein.
Hinweis
Für die Codierung von decimal
-Werten als Bytezeichenfolgen gibt es noch weitere Algorithmen. Der von DecimalValue
verwendete Algorithmus:
- Leicht verständlich
- wird von „big-endian“ und „little-endian“ auf verschiedenen Plattformen nicht beeinflusst
- unterstützt Dezimalzahlen von
9,223,372,036,854,775,807.999999999
(positiv) bis9,223,372,036,854,775,808.999999999
(negativ) mit einer maximalen Genauigkeit von neun Dezimalstellen, was nicht dem vollständigen Wertebereich vondecimal
entspricht
Konvertierungen zwischen diesem Typ und dem BCL-Typ decimal
können wie folgt in C# implementiert werden:
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);
}
}
}
Der vorangehende Code:
- Fügt eine partielle Klasse für
DecimalValue
hinzu. Die partielle Klasse wird mit demDecimalValue
vereint, welcher aus der.proto
Datei erzeugt wurde. Die generierte Klasse deklariert dieUnits
undNanos
Eigenschaften. - Enthält implizite Operatoren zur Konvertierung zwischen
DecimalValue
und dem BCL-decimal
-Typ.
Auflistungen
Listen
Listen werden in Protobuf mithilfe des Präfixschlüsselworts repeated
in einem Feld angegeben. Im folgenden Beispiel wird das Erstellen einer Liste veranschaulicht:
message Person {
// ...
repeated string roles = 8;
}
Im generierten Code werden repeated
-Felder vom generischen Typ Google.Protobuf.Collections.RepeatedField<T>
dargestellt.
public class Person
{
// ...
public RepeatedField<string> Roles { get; }
}
RepeatedField<T>
implementiert IList<T>. Sie können also LINQ-Abfragen verwenden oder in ein Array oder eine Liste konvertieren. RepeatedField<T>
-Eigenschaften verfügen über keinen öffentlichen Setter. Zur vorhandenen Sammlung sollten Elemente hinzugefügt werden.
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);
Wörterbücher
Der .NET-Typ IDictionary<TKey,TValue> wird in Protobuf mit map<key_type, value_type>
dargestellt.
message Person {
// ...
map<string, string> attributes = 9;
}
Im generierten .NET-Code werden map
-Felder vom generischen Typ Google.Protobuf.Collections.MapField<TKey, TValue>
dargestellt. MapField<TKey, TValue>
implementiert IDictionary<TKey,TValue>. Wie repeated
-Eigenschaften verfügen auch map
-Eigenschaften über keinen öffentlichen Setter. Zur vorhandenen Sammlung sollten Elemente hinzugefügt werden.
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);
Nicht strukturiere und bedingte Nachrichten
Protobuf ist ein Contract-First-Messagingformat. Die Nachrichten einer App, einschließlich der Felder und Typen, müssen beim Kompilieren der App in .proto
-Dateien angegeben werden. Das Contract-First-Design von Protobuf ist gut geeignet, um Nachrichteninhalte zu erzwingen, kann jedoch zu Einschränkungen führen, falls kein strenger Vertrag erforderlich ist:
- Nachrichten mit unbekannten Nutzdaten, z. B. eine Nachricht mit einem Feld, das eine beliebige Nachricht enthalten könnte
- Bedingte Nachrichten, z. B. eine von einem gRPC-Dienst zurückgegebene Nachricht, ob ein Vorgang erfolgreich war oder ein Fehler aufgetreten ist
- Dynamische Werte, z. B. eine Nachricht mit einem Feld, das eine ungeordnete Wertesammlung (ähnlich wie JSON) enthält
Protobuf bietet Sprachfeatures und Typen, um diese Szenarios zu unterstützen.
Any
Mithilfe des Any
-Typs können Sie Nachrichten als eingebettete Typen ohne .proto
-Definition verwenden. Importieren Sie any.proto
, um den Any
-Typ zu verwenden.
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
-Felder sind ein Sprachfeature. Der Compiler verarbeitet das oneof
-Schlüsselwert, wenn die Message-Klasse generiert wird. Die Verwendung von oneof
zum Angeben einer Antwortnachricht, die entweder Person
oder Error
zurückgibt, könnte wie folgt aussehen:
message Person {
// ...
}
message Error {
// ...
}
message ResponseMessage {
oneof result {
Error error = 1;
Person person = 2;
}
}
Felder in der oneof
-Gruppe müssen über eindeutige Feldnummern in der gesamten Nachrichtendeklaration verfügen.
Bei Verwendung von oneof
enthält der generierte C#-Code eine Enumeration, die angibt, welche Felder festgelegt wurden. Sie können die Enumeration testen, um herauszufinden, welches Feld festgelegt wurde. Für nicht festgelegte Felder wird null
oder der Standardwert zurückgegeben, anstatt dass eine Ausnahme ausgelöst wird.
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.");
}
Wert
Der Value
-Typ stellt einen dynamisch typisierten Wert dar. Dabei kann es sich um null
, eine Zahl, eine Zeichenfolge, einen booleschen Wert, ein Wörterbuch mit Werten (Struct
) oder eine Liste von Werten (ValueList
) handeln. Value
ist ein bekannter Protobuf-Typ, der das zuvor beschriebene oneof
-Feature nutzt. Importieren Sie struct.proto
, um den Value
-Typ zu verwenden.
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;
// ...
}
Die direkte Verwendung von Value
kann kompliziert sein. Eine alternative Möglichkeit zum Verwenden von Value
besteht darin, die integrierte Unterstützung von Protobuf zum Zuordnen von Nachrichten zu JSON zu verwenden. Die Typen JsonFormatter
und JsonWriter
von Protobuf können mit allen Protobuf-Nachrichten verwendet werden. Value
eignet sich insbesondere für die Konvertierung in und aus JSON.
Hier sehen Sie das JSON-Äquivalent des vorherigen Codes:
// 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);