Nguon: Microsoft Learn · .NET 8.0

Tạo Protobuf messages cho ứng dụng .NET

Nguồn: Create Protobuf messages for .NET apps

gRPC sử dụng Protobuf làm Interface Definition Language (IDL - Ngôn ngữ định nghĩa giao diện) của nó. Protobuf IDL là một định dạng trung lập ngôn ngữ để chỉ định các messages được gửi và nhận bởi gRPC services. Protobuf messages được định nghĩa trong tệp .proto. Tài liệu này giải thích cách các khái niệm Protobuf ánh xạ sang .NET.

Protobuf messages

Messages là đối tượng truyền dữ liệu chính trong Protobuf. Chúng có khái niệm tương tự như các lớp .NET.

protobuf
syntax = "proto3";

option csharp_namespace = "Contoso.Messages";

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

Định nghĩa message trước chỉ định ba fields (trường) dưới dạng cặp name-value (tên-giá trị). Giống như properties (thuộc tính) trên các kiểu .NET, mỗi field có tên và kiểu. Kiểu field có thể là Protobuf scalar value type (kiểu giá trị vô hướng), ví dụ int32, hoặc một message khác.

Hướng dẫn phong cách Protobuf khuyến nghị sử dụng underscore_separated_names cho tên field. Các Protobuf messages mới được tạo cho ứng dụng .NET nên tuân theo hướng dẫn phong cách Protobuf. Công cụ .NET tự động tạo các kiểu .NET sử dụng chuẩn đặt tên .NET. Ví dụ, một Protobuf field first_name tạo ra một .NET property FirstName.

Ngoài tên, mỗi field trong định nghĩa message có một unique number (số duy nhất). Field numbers (số field) được dùng để xác định các fields khi message được serialize (tuần tự hóa) sang Protobuf. Serializing một số nhỏ nhanh hơn serializing toàn bộ tên field. Vì field numbers xác định một field, điều quan trọng là phải cẩn thận khi thay đổi chúng. Để biết thêm về thay đổi Protobuf messages xem Version hóa gRPC services.

Khi ứng dụng được build, công cụ Protobuf tạo các kiểu .NET từ tệp .proto. Message Person tạo ra một .NET class:

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

Để biết thêm về Protobuf messages xem Hướng dẫn ngôn ngữ Protobuf.

Scalar Value Types (Kiểu giá trị vô hướng)

Protobuf hỗ trợ một loạt các kiểu giá trị vô hướng native. Bảng sau liệt kê tất cả cùng với kiểu C# tương đương:

Kiểu ProtobufKiểu C#
doubledouble
floatfloat
int32int
int64long
uint32uint
uint64ulong
sint32int
sint64long
fixed32uint
fixed64ulong
sfixed32int
sfixed64long
boolbool
stringstring
bytesByteString

Scalar values (giá trị vô hướng) luôn có giá trị mặc định và không thể được đặt thành null. Ràng buộc này bao gồm stringByteString là các lớp C#. string mặc định là chuỗi rỗng và ByteString mặc định là giá trị bytes rỗng. Cố gắng đặt chúng thành null sẽ gây ra lỗi.

Nullable wrapper types (kiểu bao bọc nullable) có thể được sử dụng để hỗ trợ giá trị null.

Dates và times (Ngày và thời gian)

Các kiểu vô hướng native không cung cấp cho giá trị date và time, tương đương với DateTimeOffset, DateTime, và TimeSpan của .NET. Các kiểu này có thể được chỉ định bằng cách sử dụng một số tiện ích mở rộng Well-Known Types của Protobuf. Các tiện ích mở rộng này cung cấp hỗ trợ tạo mã và runtime (thời gian chạy) cho các kiểu field phức tạp trên các nền tảng được hỗ trợ.

Bảng sau hiển thị các kiểu date và time:

Kiểu .NETProtobuf Well-Known Type
DateTimeOffsetgoogle.protobuf.Timestamp
DateTimegoogle.protobuf.Timestamp
TimeSpangoogle.protobuf.Duration
protobuf
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ác properties được tạo trong lớp C# không phải là các kiểu date và time .NET. Các properties sử dụng các lớp TimestampDuration trong namespace Google.Protobuf.WellKnownTypes. Các lớp này cung cấp methods để chuyển đổi sang và từ DateTimeOffset, DateTime, và TimeSpan.

csharp
// Create Timestamp and Duration from .NET DateTimeOffset and TimeSpan.
var meeting = new Meeting
{
    Start = Timestamp.FromDateTimeOffset(meetingTime), // also FromDateTime()
    Duration = Duration.FromTimeSpan(meetingLength)
};

// Convert Timestamp and Duration to .NET DateTimeOffset and TimeSpan.
var time = meeting.Start.ToDateTimeOffset();
var duration = meeting.Duration?.ToTimeSpan();

Lưu ý: Kiểu Timestamp hoạt động với UTC times (thời gian UTC). Các giá trị DateTimeOffset luôn có offset bằng không, và property DateTime.Kind luôn là DateTimeKind.Utc.

Nullable types (Kiểu nullable)

Việc tạo mã Protobuf cho C# sử dụng các kiểu native, chẳng hạn như int cho int32. Vì vậy, các giá trị luôn được bao gồm và không thể là null.

Đối với các giá trị yêu cầu null rõ ràng, chẳng hạn như sử dụng int? trong mã C#, Well-Known Types của Protobuf bao gồm các wrappers được biên dịch thành các kiểu C# nullable. Để sử dụng chúng, import wrappers.proto vào tệp .proto của bạn, như mã sau:

protobuf
syntax = "proto3";

import "google/protobuf/wrappers.proto";

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

Các kiểu wrappers.proto không được hiển thị trong các properties được tạo. Protobuf tự động ánh xạ chúng sang các kiểu .NET nullable phù hợp trong C# messages. Ví dụ, một field google.protobuf.Int32Value tạo ra một property int?. Các properties kiểu reference như stringByteString không thay đổi ngoại trừ null có thể được gán cho chúng mà không có lỗi.

Bảng sau hiển thị danh sách đầy đủ các wrapper types cùng với kiểu C# tương đương:

Kiểu C#Well-Known Type wrapper
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
stringgoogle.protobuf.StringValue
ByteStringgoogle.protobuf.BytesValue

Bytes (Dữ liệu nhị phân)

Binary payloads (tải trọng nhị phân) được hỗ trợ trong Protobuf với kiểu giá trị vô hướng bytes. Một property được tạo trong C# sử dụng ByteString làm kiểu property.

Sử dụng ByteString.CopyFrom(byte[] data) để tạo một instance mới từ mảng byte:

csharp
var data = await File.ReadAllBytesAsync(path);

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

Dữ liệu ByteString được truy cập trực tiếp bằng ByteString.Span hoặc ByteString.Memory. Hoặc gọi ByteString.ToByteArray() để chuyển đổi một instance trở lại thành mảng byte:

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

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

Decimals (Số thập phân)

Protobuf không hỗ trợ natively kiểu decimal của .NET, chỉ có doublefloat. Có một cuộc thảo luận đang diễn ra trong project Protobuf về khả năng thêm kiểu decimal tiêu chuẩn vào Well-Known Types, với hỗ trợ nền tảng cho các ngôn ngữ và frameworks hỗ trợ nó. Chưa có gì được triển khai.

Có thể tạo một định nghĩa message để đại diện cho kiểu decimal hoạt động cho serialization an toàn giữa .NET clients và servers. Nhưng các developer trên các nền tảng khác sẽ phải hiểu định dạng đang được sử dụng và tự triển khai xử lý của riêng họ.

Tạo custom decimal type cho Protobuf

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

Field nanos đại diện cho các giá trị từ 0.999_999_999 đến -0.999_999_999. Ví dụ, giá trị decimal 1.5m sẽ được đại diện là { units = 1, nanos = 500_000_000 }. Đây là lý do tại sao field nanos trong ví dụ này sử dụng kiểu sfixed32, mã hóa hiệu quả hơn int32 cho các giá trị lớn hơn. Nếu field units là âm, field nanos cũng nên âm.

Lưu ý: Các thuật toán bổ sung có sẵn để mã hóa các giá trị decimal dưới dạng byte strings. Thuật toán được sử dụng bởi DecimalValue:

Việc chuyển đổi giữa kiểu này và kiểu BCL decimal có thể được triển khai trong C# như sau:

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

Mã trước:

Collections (Tập hợp)

Lists (Danh sách)

Lists trong Protobuf được chỉ định bằng cách sử dụng từ khóa tiền tố repeated trên một field. Ví dụ sau cho thấy cách tạo một list:

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

Trong mã được tạo, các repeated fields được đại diện bởi kiểu generic Google.Protobuf.Collections.RepeatedField<T>.

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

RepeatedField<T> triển khai IList&lt;T&gt;. Vì vậy bạn có thể sử dụng các LINQ queries hoặc chuyển đổi nó thành array hoặc list. Các properties RepeatedField<T> không có public setter (bộ thiết lập công khai). Các items nên được thêm vào collection hiện có.

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

Dictionaries (Từ điển)

Kiểu IDictionary&lt;TKey,TValue&gt; của .NET được đại diện trong Protobuf bằng cách sử dụng map<key_type, value_type>.

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

Trong mã .NET được tạo, các map fields được đại diện bởi kiểu generic Google.Protobuf.Collections.MapField<TKey, TValue>. MapField<TKey, TValue> triển khai IDictionary&lt;TKey,TValue&gt;. Giống như các properties repeated, các properties map không có public setter. Các items nên được thêm vào collection hiện có.

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

Unstructured và conditional messages (Messages không có cấu trúc và có điều kiện)

Protobuf là định dạng messaging (nhắn tin) contract-first (hợp đồng trước). Các messages của ứng dụng, bao gồm các fields và kiểu của nó, phải được chỉ định trong tệp .proto khi ứng dụng được build. Thiết kế contract-first của Protobuf rất tốt trong việc thực thi nội dung message nhưng có thể giới hạn các kịch bản khi hợp đồng nghiêm ngặt không được yêu cầu:

Protobuf cung cấp các tính năng ngôn ngữ và kiểu để hỗ trợ các kịch bản này.

Any (Bất kỳ)

Kiểu Any cho phép bạn sử dụng messages như các embedded types (kiểu nhúng) mà không có định nghĩa .proto của chúng. Để sử dụng kiểu Any, import any.proto.

protobuf
import "google/protobuf/any.proto";

message Status {
    string message = 1;
    google.protobuf.Any detail = 2;
}
csharp
// 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 (Một trong số)

Các oneof fields là một tính năng ngôn ngữ. Compiler (trình biên dịch) xử lý từ khóa oneof khi nó tạo lớp message. Sử dụng oneof để chỉ định một response message có thể trả về Person hoặc Error có thể trông như thế này:

protobuf
message Person {
    // ...
}

message Error {
    // ...
}

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

Các fields trong tập hợp oneof phải có unique field numbers trong toàn bộ khai báo message.

Khi sử dụng oneof, mã C# được tạo bao gồm một enum chỉ định field nào đã được đặt. Bạn có thể kiểm tra enum để tìm field nào được đặt. Các fields không được đặt trả về null hoặc giá trị mặc định, thay vì ném exception.

csharp
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 (Giá trị)

Kiểu Value đại diện cho một giá trị được gõ động. Nó có thể là null, một số, một chuỗi, một boolean, một từ điển giá trị (Struct), hoặc một danh sách giá trị (ValueList). Value là một Protobuf Well-Known Type sử dụng tính năng oneof đã thảo luận trước đó. Để sử dụng kiểu Value, import struct.proto.

protobuf
import "google/protobuf/struct.proto";

message Status {
    // ...
    google.protobuf.Value data = 3;
}
csharp
// 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;
    // ...
}

Sử dụng Value trực tiếp có thể dài dòng. Một cách thay thế để sử dụng Value là với hỗ trợ tích hợp của Protobuf để ánh xạ messages sang JSON. JsonFormatterJsonWriter types của Protobuf có thể được sử dụng với bất kỳ Protobuf message nào. Value đặc biệt phù hợp để chuyển đổi sang và từ JSON.

Đây là phần tương đương JSON của mã trước:

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