ASP.NET Core: Tạo message Protobuf cho ứng dụng .NET


Khóa học qua video:
Lập trình Python All Lập trình C# All SQL Server All Lập trình C All Java PHP HTML5-CSS3-JavaScript
Đăng ký Hội viên
Tất cả các video dành cho hội viên

Trong bài viết này

  1. Message Protobuf
  2. Các kiểu giá trị vô hướng
  3. Bộ sưu tập
  4. Message không có cấu trúc và có điều kiện
  5. Tài nguyên bổ sung

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

Message Protobuf

Message 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.

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ên chỉ định ba trường có dạng cặp tên-giá trị. Giống như các property trên các kiểu .NET, mỗi trường có một tên và một kiểu. Kiểu trường có thể là kiểu giá trị vô hướng Protobuf, ví dụ int32: hoặc một message khác.

Hướng dẫn kiểu Protobuf khuyên bạn nên sử dụng underscore_separated_names cho tên trường. Các message Protobuf mới được tạo cho ứng dụng .NET phải tuân theo các nguyên tắc về kiểu Protobuf. Công cụ .NET tự động tạo ra các kiểu .NET sử dụng tiêu chuẩn đặt tên .NET. Ví dụ: trường Protobuf first_name tạo property .NET FirstName.

Ngoài tên, mỗi trường trong định nghĩa message có một số duy nhất, số này sử dụng để xác định trường khi message được tuần tự hóa thành Protobuf. Việc tuần tự hóa một số nhỏ nhanh hơn việc tuần tự hóa toàn bộ tên trường. Vì số trường xác định một trường nên điều quan trọng là phải cẩn thận khi thay đổi chúng. Để biết thêm thông tin về việc thay đổi message Protobuf, hãy xem Lập phiên bản dịch vụ gRPC.

Khi một ứng dụng được xây dựng, công cụ Protobuf sẽ tạo ra các kiểu .NET từ file .proto. Message Person ở trên sẽ tạo một lớp .NET như thế này:

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

Để biết thêm thông tin về message Protobuf, hãy xem hướng dẫn ngôn ngữ Protobuf.

Các kiểu giá trị vô hướng

Protobuf hỗ trợ nhiều kiểu giá trị vô hướng gốc. Bảng sau liệt kê tất cả chúng với kiểu C# tương đương:

Kiểu Protobuf Kiểu 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

Giá trị vô hướng luôn có giá trị mặc định và không thể đặt thành null. Ràng buộc này bao gồm string và ByteString là các lớp C#. string mặc định là giá trị chuỗi trống và ByteString mặc định là giá trị byte trống. Cố gắng thiết lập chúng thành null sẽ gây ra lỗi.

Các kiểu trình bao bọc nullable có thể được sử dụng để hỗ trợ các giá trị null.

Ngày và giờ

Các kiểu vô hướng gốc không cung cấp giá trị ngày và giờ, tương đương với DateTimeOffsetDateTime và TimeSpan của .NET. Những 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 Kiểu nổi tiếng của Protobuf .Các tiện ích mở rộng này cung cấp hỗ trợ tạo code và thời gian chạy cho các kiểu trường phức tạp trên các nền tảng được hỗ trợ.

Bảng sau đây hiển thị các kiểu ngày và giờ:

Kiểu .NET Kiểu nổi tiếng 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ác property được tạo ra trong lớp C# không phải là kiểu ngày và giờ của .NET. Các property sử dụng các lớp Timestamp và Duration trong namespace Google.Protobuf.WellKnownTypes. Các lớp này cung cấp các phương thức để chuyển đổi sang và từ DateTimeOffsetDateTime và TimeSpan.

// Tạo Timestamp và Duration từ .NET DateTimeOffset và TimeSpan.
var meeting = new Meeting
{
    Time = Timestamp.FromDateTimeOffset(meetingTime), // cũng FromDateTime()
    Duration = Duration.FromTimeSpan(meetingLength)
};

// Chuyển Timestamp và Duration thành .NET DateTimeOffset và TimeSpan.
var time = meeting.Time.ToDateTimeOffset();
var duration = meeting.Duration?.ToTimeSpan();

Lưu ý

Kiểu Timestamp hoạt động với thời gian UTC. Các giá trị DateTimeOffset luôn có độ lệch bằng 0 và property DateTime.Kind luôn là DateTimeKind.Utc.

Các kiểu Nullable

Việc tạo mã Protobuf cho C# sử dụng các kiểu gốc, 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 rõ ràng null, chẳng hạn như sử dụng int? trong mã C#, các Kiểu nổi tiếng của Protobuf bao gồm các trình bao bọc được biên dịch thành các kiểu C# có thể null. Để sử dụng chúng, hãy import wrappers.proto vào file .proto của bạn, như đoạn mã sau:

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 property được tạo. Protobuf tự động ánh xạ chúng tới các kiểu .NET nullable thích hợp trong message C#. Ví dụ: trường google.protobuf.Int32Value tạo ra một property int?. Các property kiểu tham chiếu như string và ByteString không thay đổi ngoại trừ null có thể được gán cho chúng mà không gặp lỗi.

Bảng sau đây hiển thị danh sách đầy đủ các kiểu trình bao bọc có kiểu C# tương đương:

Kiểu C# Kiểu bao bọc nổi tiếng
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

Tải trọng (payload) nhị phân được hỗ trợ trong Protobuf với kiểu giá trị vô hướng bytes. 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 thể hiện mới từ một mảng byte:

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 cách sử dụng ByteString.Span hoặc ByteString.Memory. Hoặc gọi ByteString.ToByteArray() để chuyển đổi một thể hiện trở lại thành một mảng byte:

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

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

Decimal (Số thập phân)

Protobuf thực sự không hỗ trợ kiểu decimal .NET, mà chỉ hỗ trợ double và float. Có một cuộc thảo luận đang diễn ra trong dự án Protobuf về khả năng thêm kiểu decimal tiêu chuẩn vào Kiểu nổi tiếng, với sự hỗ trợ nền tảng cho các ngôn ngữ và framework hỗ trợ nó.

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

Tạo kiểu decimal tùy chỉnh cho Protobuf:

package CustomTypes;

// Ví dụ: 12345.6789 -> { units = 12345, nanos = 678900000 }
message DecimalValue {

    // Toàn bộ phần đơn vị của số lượng
    int64 units = 1;

    // Đơn vị nano của số lượng (10^-9)
    // Phải cùng dấu với đơn vị
    sfixed32 nanos = 2;
}

Trường 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 biểu thị dưới dạng { units = 1, nanos = 500_000_000 }. Đây là lý do tại sao trường nanos trong ví dụ này sử dụng kiểu sfixed32 mã hóa hiệu quả hơn so với int32 cho các giá trị lớn hơn. Nếu trường units âm thì trường nanos đó cũng phải â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 chuỗi byte. Thuật toán được sử dụng bởi DecimalValue:

  • Dễ hiểu.
  • Không bị ảnh hưởng bởi big-endian hoặc little-endian trên các nền tảng khác nhau.
  • Hỗ trợ các số thập phân từ dương 9.223.372.036.854.775.807,999999999 đến âm 9.223.372.036.854.775.808,999999999 với độ chính xác tối đa là chín chữ số thập phân, không phải là phạm vi đầy đủ của decimal.

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

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

Đoạn code trên:

  • Thêm lớp partial cho DecimalValue. Lớp partial được kết hợp với DecimalValue được tạo ra từ file .proto. Lớp được tạo khai báo các property Units và Nanos.
  • Có các toán tử ngầm để chuyển đổi giữa DecimalValue và kiểu decimal BCL.

Bộ sưu tập (Collection)

List

List trong Protobuf được chỉ định bằng cách sử dụng từ khóa tiền tố repeated trên một trường. Ví dụ sau đây minh họa cách tạo list:

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

Trong đoạn code sau đây, trường repeated được biểu thị bằng kiểu generic Google.Protobuf.Collections.RepeatedField<T>.

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

RepeatedField<T> thực thi IList<T> . Vì vậy bạn có thể sử dụng truy vấn LINQ hoặc chuyển đổi nó thành một mảng hoặc một List. Các property RepeatedField<T> không có trình thiết lập (setter) public. Các mục nên được thêm vào collection hiện có.

var person = new Person();

// Thêm một item.
person.Roles.Add("user");

// Thêm tất cả item từ collection khác.
var roles = new [] { "admin", "manager" };
person.Roles.Add(roles);

Dictionary (Từ điển)

Kiểu .NET IDictionary<TKey,TValue>  được biểu diễn trong Protobuf bằng cách sử dụng map<key_type, value_type>.

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

Trường map được biểu thị bằng kiểu generic Google.Protobuf.Collections.MapField<TKey, TValue>MapField<TKey, TValue> thực thi IDictionary<TKey,TValue>. Giống như property repeated, property map không có phương thức thiết lập public. Các mục nên được thêm vào collection hiện có.

var person = new Person();

// Thêm một item.
person.Attributes["created_by"] = "James";

// Thêm tất cả các item từ another collection.
var attributes = new Dictionary<string, string>
{
    ["last_modified"] = DateTime.UtcNow.ToString()
};
person.Attributes.Add(attributes);

Message không có cấu trúc và có điều kiện

Protobuf là một định dạng message theo hợp đồng đầu tiên. Message của ứng dụng, bao gồm các trường và kiểu của ứng dụng, phải được chỉ định trong file .proto khi ứng dụng được tạo. Thiết kế ưu tiên hợp đồng của Protobuf rất hiệu quả trong việc thực thi nội dung message nhưng có thể hạn chế các trường hợp không yêu cầu hợp đồng nghiêm ngặt:

  • Message có tải trọng không xác định. Ví dụ: một message có trường có thể chứa bất kỳ message nào.
  • Message có điều kiện. Ví dụ: message được trả về từ dịch vụ gRPC có thể là kết quả thành công hoặc kết quả lỗi.
  • Các giá trị động. Ví dụ: một message có trường chứa tập hợp các giá trị không có cấu trúc, tương tự như JSON.

Protobuf cung cấp các tính năng và loại ngôn ngữ để hỗ trợ các tình huống này.

Any

Kiểu Any cho phép bạn sử dụng message dưới dạng kiểu được nhúng mà không cần .proto định nghĩa chúng. Để sử dụng kiểu Any, hãy import any.proto.

import "google/protobuf/any.proto";

message Status {
    string message = 1;
    google.protobuf.Any detail = 2;
}
// Tạo một trạng thái (status) với một tập message Person tới detail.
var status = new ErrorStatus();
status.Detail = Any.Pack(new Person { FirstName = "James" });

// Đọc message Person từ detail.
if (status.Detail.Is(Person.Descriptor))
{
    var person = status.Detail.Unpack<Person>();
    // ...
}

Oneof

Trường oneof là một tính năng ngôn ngữ. Trình biên dịch xử lý từ khóa oneof khi nó tạo ra lớp message. Sử dụng oneof để chỉ định message phản hồi có thể trả về Person hoặc Error có thể trông như thế này:

message Person {
    // ...
}

message Error {
    // ...
}

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

Các trường trong tập oneof phải có số trường duy nhất trong phần khai báo message tổng thể.

Khi sử dụng oneof, mã C# được tạo bao gồm một enum chỉ định trường nào đã được thiết lập. Bạn có thể kiểm tra enum để tìm trường nào được thiết lập. Các trường không được thiết lập sẽ trả về null hoặc giá trị mặc định, thay vì đưa ra ngoại lệ.

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

Kiểu Value đại diện cho một giá trị được định kiểu động. Nó có thể là null, một số, một chuỗi, một boolean, một từ điển các giá trị (Struct) hoặc một danh sách các giá trị (ValueList). Value là Kiểu Protobuf nổi tiếng sử dụng tính năng oneof đã nói đến ở trên. Để sử dụng kiểu Value, hãy import struct.proto.

import "google/protobuf/struct.proto";

message Status {
    // ...
    google.protobuf.Value data = 3;
}
// Tạo các giá trị động.
var status = new Status();
status.Data = Value.ForStruct(new Struct
{
    Fields =
    {
        ["enabled"] = Value.ForBool(true),
        ["metadata"] = Value.ForList(
            Value.ForString("value1"),
            Value.ForString("value2"))
    }
});

// Đọc các giá trị động.
switch (status.Data.KindCase)
{
    case Value.KindOneofCase.StructValue:
        foreach (var field in status.Data.StructValue.Fields)
        {
            // Đọc các trường struct...
        }
        break;
    // ...
}

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

Đây là JSON tương đương với đoạn code trên:

// Tạo các giá trị động từ JSON.
var status = new Status();
status.Data = Value.Parser.ParseJson(@"{
    ""enabled"": true,
    ""metadata"": [ ""value1"", ""value2"" ]
}");

// Chuyển các giá trị động từ JSON.
// JSON có thể đọc với thư viện như System.Text.Json hoặc Newtonsoft.Json
var json = JsonFormatter.Default.Format(status.Data);
var document = JsonDocument.Parse(json);

Tài nguyên bổ sung

Nguồn: learn.microsoft.com
» Tiếp: Phiên bản dịch vụ gRPC
« Trước: Tạo các dịch vụ và phương thức gRPC
Khóa học qua video:
Lập trình Python All Lập trình C# All SQL Server All Lập trình C All Java PHP HTML5-CSS3-JavaScript
Đăng ký Hội viên
Tất cả các video dành cho hội viên
Copied !!!