Создание сообщений protobuf для приложений .NET
Примечание.
Это не последняя версия этой статьи. В текущем выпуске см . версию .NET 8 этой статьи.
Предупреждение
Эта версия ASP.NET Core больше не поддерживается. Дополнительные сведения см. в статье о политике поддержки .NET и .NET Core. В текущем выпуске см . версию .NET 8 этой статьи.
Внимание
Эта информация относится к предварительному выпуску продукта, который может быть существенно изменен до его коммерческого выпуска. Майкрософт не предоставляет никаких гарантий, явных или подразумеваемых, относительно приведенных здесь сведений.
В текущем выпуске см . версию .NET 8 этой статьи.
Авторы: Джеймс Ньютон-Кинг (James Newton-King) и Марк Рендл (Mark Rendle)
gRPC использует protobuf в качестве языка определения интерфейса (IDL). Protobuf IDL — это не зависящий от языка формат для указания сообщений, отправляемых и получаемых службами gRPC. Сообщения Protobuf определяются в .proto
файлах. В этом документе объясняется, как концепции protobuf соотносятся с .NET.
Сообщения Protobuf
Сообщения — это основной объект для обмена данными в protobuf. Они концептуально схожи с классами .NET.
syntax = "proto3";
option csharp_namespace = "Contoso.Messages";
message Person {
int32 id = 1;
string first_name = 2;
string last_name = 3;
}
В предыдущем определении сообщения указывается три поля в качестве пар "имя — значение". Как и свойства типов .NET, каждое поле имеет имя и тип. Тип поля может быть скалярным типом Protobuf, например int32
или другим сообщением.
В инструкции по стилю protobufunderscore_separated_names
рекомендуется использовать для имен полей. Новые сообщения protobuf, созданные для приложений .NET, должны следовать рекомендациям по стилю protobuf. Инструментарий .NET автоматически создает типы .NET, использующие стандарты именования .NET. Например, поле protobuf first_name
формирует свойство .NET FirstName
.
Помимо имени, каждое поле в определении сообщения имеет уникальный номер. Номера полей используются для задания полей при сериализации сообщения в protobuf. Сериализация небольшого числа выполняется быстрее, чем сериализация всего имени поля. Поскольку номера полей указывают на поле, важно соблюдать осторожность при их изменении. Дополнительные сведения об изменении сообщений protobuf см. в статье Управление версиями gRPC Services.
При создании приложения средство Protobuf создает типы .NET из .proto
файлов. Сообщение Person
создает класс .NET:
public class Person
{
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
}
Дополнительные сведения о сообщениях protobuf см. в разделе Руководство по языку protobuf.
Скалярные типы значений
Protobuf поддерживает ряд собственных скалярных типов значений. В следующей таблице перечислены все типы с эквивалентными им типами в C#.
Тип protobuf | Тип 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 |
Скалярные значения всегда имеют значение по умолчанию. Им невозможно задать значение null
. Это ограничение действует для string
и ByteString
, которые являются классами C#. Значением по умолчанию string
является пустое строковое значение, а для ByteString
по умолчанию используется пустое байтовое значение. При попытке задать для них значение null
возникает ошибка.
Типы оболочек, допускающие значение NULL, могут использоваться для поддержки значений NULL.
Дата и время
Собственные скалярные типы не предоставляют значения даты и времени, что эквивалентно DateTimeOffset, DateTime и TimeSpan в .NET. Эти типы можно задавать с помощью некоторых из расширений хорошо известных типов Protobuf. Эти расширения обеспечивают поддержку создания кода и среды выполнения для сложных типов полей в поддерживаемых платформах.
В следующей таблице показаны типы даты и времени.
Тип .NET | Хорошо известный тип 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;
}
Созданные свойства в классе C# не являются типами даты и времени .NET. Свойства используют классы Timestamp
и Duration
в пространстве имен Google.Protobuf.WellKnownTypes
. Эти классы предоставляют методы для преобразования в DateTimeOffset
, DateTime
и 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();
Примечание.
Тип Timestamp
работает с временем в формате UTC. Значения DateTimeOffset
всегда имеют нулевое смещение, а свойство DateTime.Kind
всегда имеет значение DateTimeKind.Utc
.
Типы, допускающие значения NULL
При создании кода protobuf для C# используются собственные типы, например int
для int32
. Поэтому значения всегда включаются и не могут быть null
.
Для значений, требующих явного задания null
, например при использовании int?
в коде C#, хорошо известные типы Protobuf включают оболочки, которые компилируются в типы C#, допускающие значение NULL. Чтобы использовать их, импортируйте wrappers.proto
в файл .proto
, как в следующем коде.
syntax = "proto3";
import "google/protobuf/wrappers.proto";
message Person {
// ...
google.protobuf.Int32Value age = 5;
}
Типы wrappers.proto
не представлены в созданных свойствах. Protobuf автоматически сопоставляет их с соответствующими типами .NET, допускающими значение NULL, в сообщениях C#. Например, поле google.protobuf.Int32Value
создает свойство int?
. Свойства ссылочного типа, такие как string
и ByteString
, не изменяются, за исключением того, что значение null
может быть назначено им без ошибок.
В следующей таблице приведен полный список типов оболочек с эквивалентным им типом C#.
Тип C# | Оболочка хорошо известного типа |
---|---|
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 |
Байт
Двоичные полезные данные поддерживаются в Protobuf со скалярным типом значений bytes
. Созданное свойство в C# использует ByteString
как тип свойства.
Используйте ByteString.CopyFrom(byte[] data)
для создания экземпляра из массива байтов:
var data = await File.ReadAllBytesAsync(path);
var payload = new PayloadResponse();
payload.Data = ByteString.CopyFrom(data);
Доступ к данным ByteString
осуществляется напрямую с помощью ByteString.Span
или ByteString.Memory
. Или вызовите ByteString.ToByteArray()
, чтобы преобразовать экземпляр обратно в массив байтов:
var payload = await client.GetPayload(new PayloadRequest());
await File.WriteAllBytesAsync(path, payload.Data.ToByteArray());
Десятичные знаки
Protobuf изначально не поддерживает тип .NET decimal
, просто double
и float
. В проекте Protobuf ведется обсуждение возможности добавления к хорошо известным типам стандартного десятичного типа с платформенной поддержкой языков и инфраструктур, поддерживающих эту возможность. Однако к настоящему моменту еще ничего не реализовано.
Можно создать определение сообщения, представляющее тип decimal
, которое подходит для безопасной сериализации между клиентами и серверами .NET. Однако разработчикам на других платформах следует понимать, какой формат используется, и реализовать свою собственную обработку.
Создание настраиваемого десятичного типа для 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;
}
Поле nanos
представляет значения из 0.999_999_999
в -0.999_999_999
. Например, значение decimal
1.5m
будет представлено как { units = 1, nanos = 500_000_000 }
. Именно поэтому в поле nanos
в этом примере используется тип sfixed32
, который более эффективно кодируется, чем int32
для больших значений. Если поле units
имеет отрицательное значение, поле nanos
также должно быть отрицательным.
Примечание.
Для кодирования значений decimal
в виде байтовых строк доступны дополнительные алгоритмы. Алгоритм, используемый DecimalValue
:
- несложный в эксплуатации;
- не затрагивается обратным порядком байтов или прямым порядком байтов в разных платформах;
- поддерживает десятичные числа в диапазоне от положительного
9,223,372,036,854,775,807.999999999
до отрицательного9,223,372,036,854,775,808.999999999
с максимальной точностью до девяти десятичных разрядов, что не является полным диапазоном дляdecimal
.
Преобразование между этим типом и типом BCL decimal
может быть реализовано в C# следующим образом:
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);
}
}
}
Предыдущий код:
- Добавляет разделяемый класс для
DecimalValue
. Разделяемый класс объединяется сDecimalValue
, полученным из файла.proto
. Созданный класс объявляет свойстваUnits
иNanos
. - Содержит неявные операторы для преобразования между типами
DecimalValue
и BCLdecimal
.
Коллекции
Списки
Списки в protobuf указываются с помощью ключевого слова префикса repeated
в поле. В следующем примере показано, как создать список.
message Person {
// ...
repeated string roles = 8;
}
В созданном коде поля repeated
представлены универсальным типом Google.Protobuf.Collections.RepeatedField<T>
.
public class Person
{
// ...
public RepeatedField<string> Roles { get; }
}
RepeatedField<T>
реализует IList<T>. Поэтому можно использовать запросы LINQ или преобразовать их в массив или список. У свойств RepeatedField<T>
нет открытого метода задания. Элементы должны быть добавлены в существующую коллекцию.
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);
Словари
Тип .NET IDictionary<TKey,TValue> представлен в protobuf с помощью map<key_type, value_type>
.
message Person {
// ...
map<string, string> attributes = 9;
}
В созданном коде .NET поля map
представлены универсальным типом Google.Protobuf.Collections.MapField<TKey, TValue>
. MapField<TKey, TValue>
реализует IDictionary<TKey,TValue>. Как и свойства repeated
, свойства map
не имеют открытого метода задания. Элементы должны быть добавлены в существующую коллекцию.
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);
Неструктурированные и условные сообщения
Protobuf — это формат обмена сообщениями на основе контракта. Сообщения приложения, включая поля и типы, должны быть указаны в файлах .proto
при сборке приложения. Проектирование на основе контракта Protobuf отлично подходит для обеспечения содержимого сообщения, но может ограничивать сценарии, в которых не требуется строгий контракт:
- Сообщения с неизвестными полезными данными. Например, сообщение с полем, которое может содержать любое сообщение.
- Условные сообщения. Например, служба gRPC может возвращать сообщение об успехе или ошибке.
- Динамические значения. Например, сообщение с полем, которое содержит неструктурированную коллекцию значений, аналогично JSON.
Для поддержки этих сценариев Protobuf предлагает языковые функции и типы.
Любое
Тип Any
позволяет использовать сообщения в качестве внедренных типов без их .proto
определения. Чтобы использовать тип Any
, импортируйте 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
— это функция языка. Компилятор обрабатывает ключевое слово oneof
при формировании класса сообщения. Использование oneof
для указания ответного сообщения, которое может возвращать Person
или Error
, может выглядеть следующим образом.
message Person {
// ...
}
message Error {
// ...
}
message ResponseMessage {
oneof result {
Error error = 1;
Person person = 2;
}
}
Поля в наборе oneof
должны иметь уникальные номера полей в общем объявлении сообщения.
При использовании oneof
созданный код C# включает перечисление, указывающее, какое из полей было задано. Можно проверить перечисление, чтобы узнать, какое поле задается. Поля, которые не заданы, возвращают null
или значение по умолчанию вместо создания исключения.
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.");
}
Значение
Тип Value
представляет динамически типизированное значение. Это может быть либо null
, число, строка, логическое значение, словарь значений (Struct
), либо список значений (ValueList
). Value
— это хорошо известный тип Protobuf, использующий описанную выше функцию oneof
. Чтобы использовать тип Value
, импортируйте 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;
// ...
}
Использование Value
напрямую может быть подробным. Альтернативный способ использования Value
— встроенная поддержка protobuf для сопоставления сообщений с JSON. Типы JsonFormatter
и JsonWriter
protobuf можно использовать с любым сообщением protobuf. Value
особенно хорошо подходит для преобразования в JSON и обратно.
Это эквивалент JSON предыдущего кода.
// 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);
Дополнительные ресурсы
ASP.NET Core