Bagikan melalui


Membuat pesan Protobuf untuk aplikasi .NET

Oleh James Newton-King dan Mark Rendle

gRPC menggunakan Protobuf sebagai Bahasa Definisi Antarmuka (IDL). Protobuf IDL adalah format netral bahasa untuk menentukan pesan yang dikirim dan diterima oleh layanan gRPC. Pesan Protobuf didefinisikan dalam .proto file. Dokumen ini menjelaskan bagaimana konsep Protobuf memetakan ke .NET.

Pesan protobuf

Pesan adalah objek transfer data utama di Protobuf. Mereka secara konseptual mirip dengan kelas .NET.

syntax = "proto3";

option csharp_namespace = "Contoso.Messages";

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

Definisi pesan sebelumnya menentukan tiga bidang sebagai pasangan nama-nilai. Seperti properti pada jenis .NET, setiap bidang memiliki nama dan jenis. Jenis bidang dapat berupa jenis nilai skalar Protobuf, misalnya int32, atau pesan lain.

Panduan gaya Protobuf merekomendasikan penggunaan underscore_separated_names untuk nama bidang. Pesan Protobuf baru yang dibuat untuk aplikasi .NET harus mengikuti panduan gaya Protobuf. Alat .NET secara otomatis menghasilkan jenis .NET yang menggunakan standar penamaan .NET. Misalnya, first_name bidang Protobuf menghasilkan FirstName properti .NET.

Selain nama, setiap bidang dalam definisi pesan memiliki angka unik. Nomor bidang digunakan untuk mengidentifikasi bidang saat pesan diserialisasikan ke Protobuf. Menserialisasikan angka kecil lebih cepat daripada menserialisasikan seluruh nama bidang. Karena nomor bidang mengidentifikasi bidang, penting untuk berhati-hati saat mengubahnya. Untuk informasi selengkapnya tentang mengubah pesan Protobuf, lihat Penerapan versi layanan gRPC.

Saat aplikasi dibangun, alat Protobuf menghasilkan jenis .NET dari .proto file. Pesan Person menghasilkan kelas .NET:

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

Untuk informasi selengkapnya tentang pesan Protobuf, lihat panduan bahasa Protobuf.

Jenis Nilai Skalar

Protobuf mendukung berbagai jenis nilai skalar asli. Tabel berikut mencantumkan semuanya dengan jenis C# yang setara:

Jenis Protobuf Jenis 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

Nilai skalar selalu memiliki nilai default dan tidak dapat diatur ke null. Batasan ini termasuk string dan ByteString yang merupakan kelas C#. string default ke nilai string kosong dan ByteString default ke nilai byte kosong. Mencoba mengaturnya untuk null melemparkan kesalahan.

Jenis pembungkus null dapat digunakan untuk mendukung nilai null.

Tanggal dan waktu

Jenis skalar asli tidak menyediakan nilai tanggal dan waktu, setara dengan . NET, DateTimeOffset, DateTimedan TimeSpan. Jenis ini dapat ditentukan dengan menggunakan beberapa ekstensi Jenis Terkenal Protobuf. Ekstensi ini menyediakan pembuatan kode dan dukungan runtime untuk jenis bidang yang kompleks di seluruh platform yang didukung.

Tabel berikut ini memperlihatkan jenis tanggal dan waktu:

Jenis .NET Tipe Terkenal 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;
}  

Properti yang dihasilkan di kelas C# bukan jenis tanggal dan waktu .NET. Properti menggunakan kelas Timestamp dan Duration di namespace layanan Google.Protobuf.WellKnownTypes. Kelas-kelas ini menyediakan metode untuk mengonversi ke dan dari DateTimeOffset, DateTime, dan 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();

Catatan

Jenis Timestamp berfungsi dengan waktu UTC. Nilai DateTimeOffset selalu memiliki offset nol, dan properti DateTime.Kind selalu DateTimeKind.Utc.

Jenis yang dapat diubah ke null

Pembuatan kode Protobuf untuk C# menggunakan jenis asli, seperti int untuk int32. Jadi nilai selalu disertakan dan tidak boleh null.

Untuk nilai yang memerlukan eksplisit null, seperti menggunakan int? dalam kode C#, Jenis Terkenal Protobuf menyertakan pembungkus yang dikompilasi ke jenis C# nullable. Untuk menggunakannya, impor wrappers.proto ke file Anda .proto , seperti kode berikut:

syntax = "proto3";

import "google/protobuf/wrappers.proto";

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

wrappers.proto jenis tidak diekspos dalam properti yang dihasilkan. Protobuf secara otomatis memetakannya ke jenis .NET nullable yang sesuai dalam pesan C#. Misalnya, google.protobuf.Int32Value bidang menghasilkan int? properti. Properti jenis referensi seperti string dan ByteString tidak berubah kecuali null dapat ditetapkan kepada mereka tanpa kesalahan.

Tabel berikut ini memperlihatkan daftar lengkap jenis pembungkus dengan jenis C# yang setara:

Jenis C# Pembungkus Jenis Terkenal
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

Payload biner didukung di Protobuf dengan bytes jenis nilai skalar. Properti yang dihasilkan di C# menggunakan ByteString sebagai jenis properti.

Gunakan ByteString.CopyFrom(byte[] data) untuk membuat instans baru dari array byte:

var data = await File.ReadAllBytesAsync(path);

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

ByteString data diakses langsung menggunakan ByteString.Span atau ByteString.Memory. Atau panggil ByteString.ToByteArray() untuk mengonversi instans kembali menjadi array byte:

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

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

Desimal

Protobuf tidak secara asli mendukung jenis .NET decimal, hanya double dan float. Ada diskusi berkelanjutan dalam proyek Protobuf tentang kemungkinan menambahkan jenis desimal standar ke Jenis Terkenal, dengan dukungan platform untuk bahasa dan kerangka kerja yang mendukungnya. Belum ada yang diimplementasikan.

Dimungkinkan untuk membuat definisi pesan untuk mewakili decimal jenis yang berfungsi untuk serialisasi yang aman antara klien .NET dan server. Tetapi pengembang di platform lain harus memahami format yang digunakan dan menerapkan penanganan mereka sendiri untuk itu.

Membuat jenis desimal kustom untuk 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;
}

Bidang nanos mewakili nilai dari 0.999_999_999 ke -0.999_999_999. Misalnya, nilai decimal1.5m akan diwakili sebagai { units = 1, nanos = 500_000_000 }. Inilah sebabnya mengapa bidang nanos dalam contoh ini menggunakan jenis sfixed32, yang dikodekan lebih efisien daripada int32 untuk nilai yang lebih besar. Jika bidang units negatif, bidang nanos juga harus negatif.

Catatan

Algoritma tambahan tersedia untuk mengodekan decimal nilai sebagai string byte. Algoritma yang digunakan oleh DecimalValue:

  • Mudah dimengerti.
  • Tidak terpengaruh oleh big-endian atau little-endian pada platform yang berbeda.
  • Mendukung angka desimal mulai dari positif 9,223,372,036,854,775,807.999999999 hingga negatif 9,223,372,036,854,775,808.999999999 dengan presisi maksimum sembilan tempat desimal, yang bukan rentang penuh dari decimal.

Konversi antara jenis ini dan jenis decimal BCL mungkin diimplementasikan dalam C# seperti ini:

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

Kode sebelumnya:

  • Menambahkan kelas parsial untuk DecimalValue. Kelas parsial dikombinasikan dengan DecimalValue yang dihasilkan dari .proto file. Kelas yang dihasilkan mendeklarasikan Units properti dan Nanos .
  • Memiliki operator implisit untuk mengonversi antara DecimalValue dan jenis BCL decimal .

Koleksi

Daftar

Daftar dalam Protobuf ditentukan dengan menggunakan kata kunci awalan repeated pada bidang. Contoh berikut ini memperlihatkan cara membuat daftar:

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

Dalam kode yang dihasilkan, repeated bidang diwakili oleh Google.Protobuf.Collections.RepeatedField<T> jenis generik.

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

RepeatedField<T> penerapan IList<T>. Jadi Anda dapat menggunakan kueri LINQ atau mengonversinya menjadi array atau daftar. RepeatedField<T> properti tidak memiliki setter publik. Item harus ditambahkan ke koleksi yang ada.

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

Kamus

Jenis .NET IDictionary<TKey,TValue> diwakili dalam Protobuf menggunakan map<key_type, value_type>.

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

Dalam kode .NET yang dihasilkan, map bidang diwakili oleh Google.Protobuf.Collections.MapField<TKey, TValue> jenis generik. MapField<TKey, TValue> penerapan IDictionary<TKey,TValue>. Seperti repeated properti, map properti tidak memiliki setter publik. Item harus ditambahkan ke koleksi yang ada.

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

Pesan tidak terstruktur dan bersyarat

Protobuf adalah format olahpesan pertama kontrak. Pesan aplikasi, termasuk bidang dan jenisnya, harus ditentukan dalam .proto file saat aplikasi dibuat. Desain pertama kontrak Protobuf sangat bagus dalam memberlakukan konten pesan tetapi dapat membatasi skenario di mana kontrak yang ketat tidak diperlukan:

  • Pesan dengan payload yang tidak diketahui. Misalnya, pesan dengan bidang yang dapat berisi pesan apa pun.
  • Pesan bersyar. Misalnya, pesan yang dikembalikan dari layanan gRPC mungkin merupakan hasil yang berhasil atau hasil kesalahan.
  • Nilai dinamis. Misalnya, pesan dengan bidang yang berisi kumpulan nilai yang tidak terstruktur, mirip JSdengan AKTIF.

Protobuf menawarkan fitur dan jenis bahasa untuk mendukung skenario ini.

Mana pun

Jenis ini Any memungkinkan Anda menggunakan pesan sebagai jenis yang disematkan tanpa memiliki definisinya .proto . Untuk menggunakan jenis , Any impor 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 bidang adalah fitur bahasa. Pengkompilasi menangani oneof kata kunci saat menghasilkan kelas pesan. Menggunakan oneof untuk menentukan pesan respons yang dapat mengembalikan Person atau Error mungkin terlihat seperti ini:

message Person {
    // ...
}

message Error {
    // ...
}

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

Bidang dalam set oneof harus memiliki nomor bidang unik dalam keseluruhan deklarasi pesan.

Saat menggunakan oneof, kode C# yang dihasilkan menyertakan enum yang menentukan bidang mana yang telah diatur. Anda dapat menguji enum untuk menemukan bidang mana yang diatur. Bidang yang tidak diatur mengembalikan null atau nilai default, daripada melemparkan pengecualian.

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

Nilai

Jenis mewakili Value nilai yang di ketik secara dinamis. Ini bisa berupa null, angka, string, boolean, kamus nilai (Struct), atau daftar nilai (ValueList). Value adalah Jenis Terkenal Protobuf yang menggunakan fitur yang dibahas oneof sebelumnya. Untuk menggunakan jenis , Value impor 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;
    // ...
}

Menggunakan Value secara langsung bisa verbose. Cara alternatif untuk digunakan Value adalah dengan dukungan bawaan Protobuf untuk memetakan pesan ke JSAKTIF. Jenis dan JsonWriter Protobuf JsonFormatter dapat digunakan dengan pesan Protobuf apa pun. Value sangat cocok untuk dikonversi ke dan dari JSON.

Ini adalah ON yang JSsetara dengan kode sebelumnya:

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

Sumber Daya Tambahan: