為 .NET 應用程式建立 Protobuf 訊息
注意
這不是這篇文章的最新版本。 如需目前版本,請參閱本文的 .NET 8 版本。
警告
不再支援此版本的 ASP.NET Core。 如需詳細資訊,請參閱 .NET 和 .NET Core 支援原則。 如需目前版本,請參閱本文的 .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
),或其他訊息。
Protobuf 樣式指南建議使用 underscore_separated_names
作為欄位名稱。 為 .NET 應用程式建立的新 Protobuf 訊息,應遵循 Protobuf 樣式指導方針。 .NET 工具會自動產生使用 .NET 命名標準的 .NET 型別。 例如,first_name
Protobuf 欄位會產生 FirstName
.NET 屬性。
除了名稱之外,訊息定義中的每個欄位都有一個唯一的編號。 在訊息序列化為 Protobuf 時,會使用欄位編號來識別欄位。 序列化少量數字比序列化整個欄位名稱更快。 由於欄位編號會識別欄位,因此在變更欄位時請務必小心。 如需變更 Protobuf 訊息的相關資訊,請參閱 gRPC 服務版本設定。
建置應用程式時,Protobuf 工具會從 .proto
檔案產生 .NET 型別。 訊息 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 值。
日期和時間
原生純量型別不提供日期和時間值,相當於 .NET 的 DateTimeOffset、DateTime 和 TimeSpan。 您可以使用一些 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 日期和時間型別。 屬性會使用 Google.Protobuf.WellKnownTypes
命名空間中的 Timestamp
和 Duration
類別。 這些類別提供從 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 的類型
C# 的 Protobuf 程式碼產生過程會使用原生型別,例如 int32
的 int
。 因此,其中必定會包含這些值,而且不能是 null
。
對於需要明確 null
的值 (例如在 C# 程式碼中使用 int?
),Protobuf 的已知型別會包含編譯為可為 Null C# 型別的包裝函式。 若要使用,請將 wrappers.proto
匯入您的 .proto
檔案,如下列程式碼所示:
syntax = "proto3";
import "google/protobuf/wrappers.proto";
message Person {
// ...
google.protobuf.Int32Value age = 5;
}
wrappers.proto
型別不會在產生的屬性中公開。 Protobuf 會自動將這些型別對應至 C# 訊息中適當的 .NET 可為 Null 型別。 例如,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 |
Bytes
Protobuf 支援具有 bytes
純量實值型別的二進位承載。 C# 中產生的屬性使用 ByteString
作為屬性型別。
使用 ByteString.CopyFrom(byte[] data)
從位元組陣列建立新的執行個體:
var data = await File.ReadAllBytesAsync(path);
var payload = new PayloadResponse();
payload.Data = ByteString.CopyFrom(data);
使用 ByteString.Span
或 ByteString.Memory
直接存取 ByteString
資料。 或者,呼叫 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
所使用的演算法:
- 很容易理解。
- 不受不同平台上的 big-endian 或 little-endian 影響。
- 支援從正
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
新增部分類別。 此部分類別與從.proto
檔案產生的DecimalValue
結合。 產生的類別會宣告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>
屬性沒有公用 setter。 項目應新增至現有集合。
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
屬性沒有公用 setter。 項目應新增至現有集合。
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
是使用先前討論的 oneof
功能的 Protobuf 已知型別。 若要使用 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。 Protobuf 的 JsonFormatter
和 JsonWriter
型別,可與任何 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);