Créer des messages Protobuf pour les applications .NET
Remarque
Ceci n’est pas la dernière version de cet article. Pour la version actuelle, consultez la version .NET 9 de cet article.
Avertissement
Cette version d’ASP.NET Core n’est plus prise en charge. Pour plus d’informations, consultez la stratégie de support .NET et .NET Core. Pour la version actuelle, consultez la version .NET 9 de cet article.
Important
Ces informations portent sur la préversion du produit, qui est susceptible d’être en grande partie modifié avant sa commercialisation. Microsoft n’offre aucune garantie, expresse ou implicite, concernant les informations fournies ici.
Pour la version actuelle, consultez la version .NET 9 de cet article.
Par James Newton-King et Mark Rendle
gRPC utilise Protobuf comme langage IDL (Interface Definition Language). Protobuf IDL est un format de langage neutre permettant de spécifier les messages envoyés et reçus par les services gRPC. Les messages Protobuf sont définis dans des fichiers .proto
. Ce document explique comment les concepts Protobuf sont mappés à .NET.
Messages Protobuf
Les messages sont le principal objet de transfert de données dans Protobuf. Ils sont conceptuellement similaires aux classes .NET.
syntax = "proto3";
option csharp_namespace = "Contoso.Messages";
message Person {
int32 id = 1;
string first_name = 2;
string last_name = 3;
}
La définition de message précédente spécifie trois champs sous forme de paires nom-valeur. Comme les propriétés sur les types .NET, chaque champ a un nom et un type. Le type de champ peut être un type de valeur scalaire Protobuf, par exemple int32
, ou un autre message.
Le guide de style Protobuf recommande d’utiliser underscore_separated_names
pour les noms de champs. Les nouveaux messages Protobuf créés pour les applications .NET doivent suivre les instructions relatives au style Protobuf. Les outils .NET génèrent automatiquement des types .NET qui utilisent des normes de nommage .NET. Par exemple, un champ Protobuf first_name
génère une propriété .NET FirstName
.
En plus d’un nom, chaque champ de la définition du message a un numéro unique. Les numéros de champ sont utilisés pour identifier les champs lorsque le message est sérialisé dans Protobuf. La sérialisation d’un petit nombre est plus rapide que la sérialisation du nom de champ entier. Étant donné que les numéros de champ identifient un champ, il est important de faire attention lors de leur modification. Pour plus d’informations sur la modification des messages Protobuf, consultez Gestion des versions des services gRPC.
Lorsqu’une application est générée, les outils Protobuf génèrent des types .NET à partir de fichiers .proto
. Le message Person
génère une classe .NET :
public class Person
{
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
}
Pour plus d’informations sur les messages Protobuf, consultez le guide de langue Protobuf.
Types de valeurs scalaires
Protobuf prend en charge une série de types de valeurs scalaires natifs. Le tableau suivant les répertorie tous avec leur type C# équivalent :
Type Protobuf | Type 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 |
Les valeurs scalaires ont toujours une valeur par défaut et ne peuvent pas être définies sur null
. Cette contrainte inclut string
et ByteString
qui sont des classes C#. string
prend par défaut une valeur de chaîne vide et ByteString
prend par défaut une valeur d’octets vides. La tentative de les définir sur null
génère une erreur.
Les types de wrapper pouvant accepter la valeur Null peuvent être utilisés pour prendre en charge des valeurs Null.
Dates et heures
Les types scalaires natifs ne fournissent pas de valeurs de date et d’heure, équivalents aux DateTimeOffset, DateTime et TimeSpan de .NET. Ces types peuvent être spécifiés à l’aide de certaines extensions types connus de Protobuf. Ces extensions prennent en charge la génération de code et le runtime pour les types de champs complexes sur les plateformes prises en charge.
Le tableau suivant présente les types de date et d’heure :
Type .NET | Type connu 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;
}
Les propriétés générées dans la classe C# ne sont pas les types de date et d’heure .NET. Les propriétés utilisent les classes Timestamp
et Duration
dans l’espace de noms Google.Protobuf.WellKnownTypes
. Ces classes fournissent des méthodes de conversion en et à partir de DateTimeOffset
, DateTime
et 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();
Notes
Le type Timestamp
fonctionne avec les heures UTC. Les valeurs DateTimeOffset
ont toujours un décalage de zéro, et la propriété DateTime.Kind
est toujours DateTimeKind.Utc
.
Types Nullable
La génération de code Protobuf pour C# utilise les types natifs, tels que int
pour int32
. Par conséquent, les valeurs sont toujours incluses et ne peuvent pas être null
.
Pour les valeurs qui exigent explicitement null
, comme l’utilisation de int?
dans votre code C#, les « types connus » de Protobuf incluent des wrappers compilés en types C# pouvant accepter la valeur Null. Pour les utiliser, importez wrappers.proto
dans votre fichier .proto
, comme le code suivant :
syntax = "proto3";
import "google/protobuf/wrappers.proto";
message Person {
// ...
google.protobuf.Int32Value age = 5;
}
Les types wrappers.proto
ne sont pas exposés dans les propriétés générées. Protobuf les mappe automatiquement aux types nullables .NET appropriés dans les messages C#. Par exemple, un champ google.protobuf.Int32Value
génère une propriété int?
. Les propriétés de type de référence telles que string
et ByteString
sont inchangées, sauf que null
peut leur être attribuée sans erreur.
Le tableau suivant présente la liste complète des types de wrapper avec leur type C# équivalent :
Type C# | Wrapper de type connu |
---|---|
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 |
Octets
Les charges utiles binaires sont prises en charge dans Protobuf avec le type de valeur scalaire bytes
. Une propriété générée en C# utilise ByteString
comme type de propriété.
Utilisez ByteString.CopyFrom(byte[] data)
pour créer un instance à partir d’un groupe d’octets :
var data = await File.ReadAllBytesAsync(path);
var payload = new PayloadResponse();
payload.Data = ByteString.CopyFrom(data);
Les données ByteString
sont accessibles directement à l’aide de ByteString.Span
ou ByteString.Memory
. Vous pouvez également appeler ByteString.ToByteArray()
pour convertir une instance en groupe d’octets :
var payload = await client.GetPayload(new PayloadRequest());
await File.WriteAllBytesAsync(path, payload.Data.ToByteArray());
Décimales
Protobuf ne prend pas en charge en mode natif le type .NET decimal
, juste double
et float
. Il y a un débat en cours dans le projet Protobuf sur la possibilité d’ajouter un type décimal standard aux types connus, avec prise en charge des plateformes pour les langages et les infrastructures qui le prennent en charge. Rien n’a encore été mis en œuvre.
Il est possible de créer une définition de message pour représenter le type decimal
qui fonctionne pour la sérialisation sécurisée entre les clients et les serveurs .NET. Mais les développeurs sur d’autres plateformes doivent comprendre le format utilisé et implémenter leur propre gestion.
Création d’un type décimal personnalisé pour 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;
}
Le champ nanos
représente les valeurs de 0.999_999_999
à -0.999_999_999
. Par exemple, la valeur decimal
1.5m
serait représentée comme { units = 1, nanos = 500_000_000 }
. C’est pourquoi le champ nanos
dans cet exemple utilise le type sfixed32
, qui encode plus efficacement que int32
pour les valeurs plus volumineuses. Si le champ units
est négatif, le champ nanos
doit également être négatif.
Notes
Des algorithmes supplémentaires sont disponibles pour l’encodage des valeurs decimal
sous forme de chaînes d’octets. L’algorithme utilisé par DecimalValue
:
- Est facile à comprendre.
- N’est pas affecté par big-endian ou little-endian sur différentes plateformes.
- Prend en charge les nombres décimaux allant de positif
9,223,372,036,854,775,807.999999999
à négatif9,223,372,036,854,775,808.999999999
avec une précision maximale de neuf décimales, ce qui n’est pas la plage complète d’undecimal
.
La conversion entre ce type et le type BCL decimal
peut être implémentée en C# comme suit :
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);
}
}
}
Le code précédent :
- Ajoute une classe partielle pour
DecimalValue
. La classe partielle est combinée avecDecimalValue
généré à partir du fichier.proto
. La classe générée déclare les propriétésUnits
etNanos
. - A des opérateurs implicites pour la conversion entre
DecimalValue
et le type BCLdecimal
.
Collections
Listes
Les listes dans Protobuf sont spécifiées à l’aide du mot clé de préfixe repeated
sur un champ. L'exemple suivant montre comment créer une liste :
message Person {
// ...
repeated string roles = 8;
}
Dans le code généré, les champs repeated
sont représentés par le type générique Google.Protobuf.Collections.RepeatedField<T>
.
public class Person
{
// ...
public RepeatedField<string> Roles { get; }
}
L'objet RepeatedField<T>
implémente l'objet IList<T>. Vous pouvez donc utiliser des requêtes LINQ ou les convertir en tableau ou en liste. Les propriétés RepeatedField<T>
n’ont pas de setter public. Des éléments doivent être ajoutés à la collection existante.
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);
Dictionnaires
Le type .NET IDictionary<TKey,TValue> est représenté dans Protobuf à l’aide de map<key_type, value_type>
.
message Person {
// ...
map<string, string> attributes = 9;
}
Dans le code . NET généré, les champs map
sont représentés par le type générique Google.Protobuf.Collections.MapField<TKey, TValue>
. L'objet MapField<TKey, TValue>
implémente l'objet IDictionary<TKey,TValue>. Comme les propriétés repeated
, les propriétés map
n’ont pas de setter public. Des éléments doivent être ajoutés à la collection existante.
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);
Messages non structurés et conditionnels
Protobuf est un format de messagerie « contrat en premier ». Les messages d’une application, y compris leurs champs et leurs types, doivent être spécifiés dans les fichiers .proto
lorsque l’application est générée. La conception « contrat en premier » de Protobuf est excellente pour appliquer le contenu des messages, mais peut limiter les scénarios où un contrat strict n’est pas obligatoire :
- Messages avec des charges utiles inconnues. Par exemple, un message avec un champ qui peut contenir n’importe quel message.
- Messages conditionnels. Par exemple, un message retourné à partir d’un service gRPC peut être un résultat de réussite ou un résultat d’erreur.
- Valeurs dynamiques. Par exemple, un message avec un champ qui contient une collection non structurée de valeurs, similaire à JSON.
Protobuf offre des fonctionnalités et des types de langage pour prendre en charge ces scénarios.
Quelconque
Le type Any
vous permet d’utiliser des messages en tant que types incorporés sans avoir leur définition .proto
. Pour utiliser le type Any
, importez 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
Les champs oneof
sont une fonctionnalité de langage. Le compilateur gère le mot clé oneof
lorsqu’il génère la classe de message. L’utilisation de oneof
pour spécifier un message de réponse qui peut renvoyer un Person
ou un Error
peut ressembler à ceci :
message Person {
// ...
}
message Error {
// ...
}
message ResponseMessage {
oneof result {
Error error = 1;
Person person = 2;
}
}
Les champs de l’ensemble oneof
doivent avoir des numéros de champ uniques dans la déclaration de message globale.
Lorsque vous utilisez oneof
, le code C# généré inclut une énumération qui spécifie les champs qui ont été définis. Vous pouvez tester l’énumération pour déterminer le champ défini. Les champs qui ne sont pas définis retournent null
ou la valeur par défaut, plutôt que de lever une exception.
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.");
}
Valeur
Le type Value
représente une valeur typée dynamiquement. Il peut s’agir de null
, d’un nombre, d’une chaîne, d’un booléen, d’un dictionnaire de valeurs (Struct
) ou d’une liste de valeurs (ValueList
). Value
est un « type connu » de Protobuf qui utilise la fonctionnalité oneof
décrite précédemment. Pour utiliser le type Value
, importez 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;
// ...
}
L’utilisation directe de Value
peut être détaillée. Une autre façon d’utiliser Value
consiste à utiliser la prise en charge intégrée de Protobuf pour le mappage des messages à JSON. Les types de Protobuf JsonFormatter
et JsonWriter
peuvent être utilisés avec n’importe quel message Protobuf. Value
convient particulièrement à la conversion vers et à partir de JSON.
Il s’agit de l’équivalent JSON du code précédent :
// 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);