protobuf-net

シリアライゼーションProtocol Buffers.NETC#高性能バイナリフォーマットgRPC

シリアライゼーションライブラリ

protobuf-net

概要

protobuf-netは、Marc Gravellによって開発された.NET向けのProtocol Buffers実装です。Googleが開発したProtocol Buffersのバイナリフォーマットを使用しながら、.NETの慣習に従った使いやすいAPIを提供します。属性ベースのコードファーストアプローチと、.protoファイルを使用したコントラクトファーストアプローチの両方をサポートし、高性能なシリアライゼーションを実現します。

詳細

protobuf-netは、.NET環境に最適化されたProtocol Buffers実装で、以下の特徴を持ちます:

主要な特徴

  1. .NET慣習に従った設計

    • XmlSerializerやDataContractSerializerと同様の使用感
    • 属性ベースの簡単な設定
    • .NET開発者にとって直感的なAPI
  2. 高速・高効率

    • ランタイムコード生成による最適化
    • 最小限のメモリ使用
    • BinaryFormatterと比較して約20倍高速
  3. 柔軟な開発アプローチ

    • コードファースト:C#クラスから開始
    • コントラクトファースト:.protoファイルから開始
    • 両アプローチ間の相互変換も可能
  4. 豊富な.NET機能サポート

    • 継承のサポート(protobuf-net独自拡張)
    • Nullable型の完全サポート
    • コレクションとジェネリクスのサポート
    • シリアライゼーションコールバック
  5. ビルドツール統合

    • MSBuildとの完全統合
    • .protoファイルからの自動コード生成
    • NuGetパッケージによる簡単な導入

パフォーマンス特性

  • シリアライゼーション速度:BinaryFormatterの約8-10倍高速
  • デシリアライゼーション速度:BinaryFormatterの約11-12倍高速
  • データサイズ:XMLの約1/10、JSONの約1/3-1/5
  • メモリ効率:ストリーミングAPIによる低メモリ使用

メリット・デメリット

メリット

  • 高性能:.NET環境で最速レベルのシリアライゼーション
  • 小さいデータサイズ:効率的なバイナリフォーマット
  • 使いやすさ:.NET開発者にとって自然な属性ベースのAPI
  • 柔軟性:コードファーストとコントラクトファーストの両対応
  • 互換性:標準的なProtocol Buffersフォーマットを使用
  • ストリーミング対応:大容量データの効率的な処理
  • 継承サポート:.NET特有の継承関係をサポート

デメリット

  • 人間可読性なし:バイナリフォーマットのため直接読めない
  • スキーマ進化の制限:フィールド番号の管理が必要
  • 学習コスト:Protocol Buffersの概念理解が必要
  • デバッグの難しさ:バイナリデータのデバッグは困難
  • クロスプラットフォーム制約:コードファーストは.NET限定

参考ページ

書き方の例

基本的な使用方法(属性ベース)

using ProtoBuf;
using System;
using System.IO;

// データモデルの定義
[ProtoContract]
public class Person
{
    [ProtoMember(1)]
    public int Id { get; set; }
    
    [ProtoMember(2)]
    public string Name { get; set; }
    
    [ProtoMember(3)]
    public DateTime BirthDate { get; set; }
    
    [ProtoMember(4)]
    public Address Address { get; set; }
}

[ProtoContract]
public class Address
{
    [ProtoMember(1)]
    public string Street { get; set; }
    
    [ProtoMember(2)]
    public string City { get; set; }
    
    [ProtoMember(3)]
    public string PostalCode { get; set; }
}

// シリアライゼーションとデシリアライゼーション
class Program
{
    static void Main()
    {
        var person = new Person
        {
            Id = 1,
            Name = "田中太郎",
            BirthDate = new DateTime(1990, 1, 1),
            Address = new Address
            {
                Street = "東京都千代田区1-1",
                City = "東京",
                PostalCode = "100-0001"
            }
        };

        // シリアライゼーション
        using (var file = File.Create("person.bin"))
        {
            Serializer.Serialize(file, person);
        }

        // デシリアライゼーション
        Person loadedPerson;
        using (var file = File.OpenRead("person.bin"))
        {
            loadedPerson = Serializer.Deserialize<Person>(file);
        }

        Console.WriteLine($"Name: {loadedPerson.Name}");
    }
}

コントラクトファーストアプローチ

// person.proto
syntax = "proto3";

package example;

message Person {
    int32 id = 1;
    string name = 2;
    string birth_date = 3;
    Address address = 4;
}

message Address {
    string street = 1;
    string city = 2;
    string postal_code = 3;
}
<!-- .csprojファイルに追加 -->
<ItemGroup>
    <PackageReference Include="protobuf-net.BuildTools" Version="3.*" 
                      PrivateAssets="all" />
    <AdditionalFiles Include="person.proto" />
</ItemGroup>

継承のサポート

[ProtoContract]
[ProtoInclude(7, typeof(Employee))]
[ProtoInclude(8, typeof(Customer))]
public abstract class Person
{
    [ProtoMember(1)]
    public int Id { get; set; }
    
    [ProtoMember(2)]
    public string Name { get; set; }
}

[ProtoContract]
public class Employee : Person
{
    [ProtoMember(1)]
    public string Department { get; set; }
    
    [ProtoMember(2)]
    public decimal Salary { get; set; }
}

[ProtoContract]
public class Customer : Person
{
    [ProtoMember(1)]
    public string CustomerNumber { get; set; }
    
    [ProtoMember(2)]
    public DateTime RegistrationDate { get; set; }
}

コレクションとジェネリクス

[ProtoContract]
public class OrderList
{
    [ProtoMember(1)]
    public List<Order> Orders { get; set; } = new List<Order>();
    
    [ProtoMember(2)]
    public Dictionary<int, Customer> CustomerMap { get; set; } 
        = new Dictionary<int, Customer>();
    
    [ProtoMember(3, OverwriteList = true)]
    public HashSet<string> Tags { get; set; } = new HashSet<string>();
}

[ProtoContract]
public class Order
{
    [ProtoMember(1)]
    public int OrderId { get; set; }
    
    [ProtoMember(2)]
    public decimal Amount { get; set; }
    
    [ProtoMember(3)]
    public DateTime OrderDate { get; set; }
}

// 使用例
var orderList = new OrderList();
orderList.Orders.Add(new Order 
{ 
    OrderId = 1, 
    Amount = 1000.0m, 
    OrderDate = DateTime.Now 
});

// メモリストリームへのシリアライゼーション
using var ms = new MemoryStream();
Serializer.Serialize(ms, orderList);
var bytes = ms.ToArray();

パフォーマンスチューニング

// RuntimeTypeModelを使用した高度な設定
var model = RuntimeTypeModel.Default;

// 型の事前コンパイル
model.CompileInPlace();

// または完全なコンパイル済みモデルの生成
var compiledModel = model.Compile();

// カスタムシリアライゼーション設定
model[typeof(Person)]
    .Add(1, "Id")
    .Add(2, "Name")
    .Add(3, "BirthDate");

// ストリーミングAPIの使用
public static async Task SerializeLargeDataAsync<T>(
    Stream stream, IEnumerable<T> items)
{
    foreach (var item in items)
    {
        Serializer.SerializeWithLengthPrefix(stream, item, 
            PrefixStyle.Base128);
        await stream.FlushAsync();
    }
}

// 読み込み時のストリーミング
public static async IAsyncEnumerable<T> DeserializeLargeDataAsync<T>(
    Stream stream)
{
    while (stream.Position < stream.Length)
    {
        var item = Serializer.DeserializeWithLengthPrefix<T>(
            stream, PrefixStyle.Base128);
        if (item == null) yield break;
        yield return item;
    }
}

Null値の処理

[ProtoContract]
public class NullableExample
{
    // 通常のnull許容型
    [ProtoMember(1)]
    public int? OptionalInt { get; set; }
    
    // Null値をラップして送信
    [ProtoMember(2), NullWrappedValue]
    public int? WrappedInt { get; set; }
    
    // Nullコレクションの処理
    [ProtoMember(3), NullWrappedCollection]
    public List<string> OptionalList { get; set; }
    
    // 条件付きシリアライゼーション
    [ProtoMember(4)]
    public string ConditionalField { get; set; }
    
    public bool ShouldSerializeConditionalField()
    {
        return !string.IsNullOrEmpty(ConditionalField);
    }
}

これらの例は、protobuf-netの主要な機能を示しています。高性能なバイナリシリアライゼーションが必要な.NETアプリケーションに最適な選択肢です。