はじめに
今回はC#用のMessagePack
シリアライザーであるMessagePack for C#
の基礎的な使い方を紹介したいと思います。
概要
MessagePack for C#
はハイパフォーマンスなMessagePack
シリアライザーです。GitHubのReadmeにも記載されていますが、Json.NETは当然のことながら、protobuf-netやMsgPack-Cliよりもパフォーマンスやシリアライズ後のバイナリサイズともに優秀です。
The extremely fast MessagePack serializer for C#. It is 10x faster than MsgPack-Cli and outperforms other C# serializers. MessagePack for C# also ships with built-in support for LZ4 compression - an extremely fast compression algorithm. Performance is important, particularly in applications like games, distributed computing, microservices, or data caches.
// DeepL翻訳
C#用の超高速MessagePackシリアライザー。MsgPack-Cliの10倍高速で、他のC#シリアライザを凌駕します。また、MessagePack for C#は、非常に高速な圧縮アルゴリズムであるLZ4圧縮をビルトインでサポートしています。特にゲーム、分散コンピューティング、マイクロサービス、データキャッシュなどのアプリケーションでは、パフォーマンスが重要です。
GitHub - MessagePack-CSharp/MessagePack-CSharp: Extremely Fast MessagePack Serializer for C#(.NET, .NET Core, Unity, Xamarin). / msgpack.org[C#]
ちなみにMessagePack
自体の説明は以下の公式サイトに掲載されています。
MessagePackは、効率の良いバイナリ形式のオブジェクト・シリアライズ フォーマットです。JSONの置き換えとして使うことができ、様々なプログラミング言語をまたいでデータを交換することが可能です。しかも、JSONよりも速くてコンパクトです。例えば、小さな整数値はたった1バイト、短い文字列は文字列自体の長さ+1バイトでシリアライズできます。
↓ MessagePack
の仕様
github.com
環境
- Rider 2023.1.3
- Console Application
- .net7.0
- C#11
インストール方法
Rider上のエクスプローラーから.csproj
を右クリックし、NuGetパッケージの管理
を選択します。
あとはMessagePack
と検索して右上の+
ボタンを押せばインストール完了です。ちなみに現在(2024/1/3)の最新はv2.6.100-alpha
でしたが、この記事では2.5.140
を利用しています。
使い方
シリアライズ・デシリアライズ対象に対して以下の操作を行います。
- クラスまたは構造体に対して
[MessagePackObject]
を付与する - シリアライズ対象のメンバ(フィールド+プロパティ)に
[Key]
を付与する
[MessagePackObject] public class Person { // 一意のインデックス(もしくは文字列)をKeyに指定する [Key(0)] public int Age { get; set; } [Key(1)] public string FirstName { get; set; } [Key(2)] public string LastName { get; set; } // シリアライズされるべきではないフィールド・プロパティは[IgnoreMember]を付与する [IgnoreMember] public string FullName => FirstName + LastName; }
シリアライズ・デシリアライズを行う際はMessagePackSerializer.Serialize
とMessagePackSerializer.Deserialize
を利用します。
public static void Main() { var person = new Person { Age = 20, FirstName = "John", LastName = "Doe" }; // シリアライズ byte[] bytes = MessagePackSerializer.Serialize(person); // 中身(バイナリ) : 93 14 A4 4A 6F 68 6E A3 44 6F 65 Console.WriteLine(string.Join(" ", bytes.Select(x => x.ToString("X")))); // Jsonへの変換も可能 // [20,"John","Doe"] var json = MessagePackSerializer.ConvertToJson(bytes); Console.WriteLine(json); // デシリアライズ Person newPerson = MessagePackSerializer.Deserialize<Person>(bytes); // 20 Console.WriteLine(newPerson.Age); // John Console.WriteLine(newPerson.FirstName); // Doe Console.WriteLine(newPerson.LastName); }
めちゃくちゃ簡単ですね。またMessagePackSerializer.ConvertToJson
とMessagePackSerializer.ConvertFromJson
を利用する事でJsonとの相互変換も可能になります。
Keyのインデックスについて
ちなみに先ほどのサンプルでは[Key]
のインデックスを連番にしていましたが、以下のように連番でなくするとシリアライズ結果も変わります。
[MessagePackObject] public class MyClass { // 一意のインデックス(もしくは文字列)をKeyに指定する [Key(0)] public int Age { get; set; } [Key(1)] public string FirstName { get; set; } [Key(10)] public string LastName { get; set; } // シリアライズされるべきではないフィールド・プロパティは[IgnoreMember]を付与する [IgnoreMember] public string FullName => FirstName + LastName; }
// 連番のときのシリアライズ結果 (バイナリと対応するJson) 93 14 A4 4A 6F 68 6E A3 44 6F 65 [20,"John","Doe"] // 上記のシリアライズ結果 (バイナリと対応するJson) 9B 14 A4 4A 6F 68 6E C0 C0 C0 C0 C0 C0 C0 C0 A3 44 6F 65 [20,"John",null,null,null,null,null,null,null,null,"Doe"]
この後紹介しますが、[Key]
にインデックスを指定すると配列でシリアライズされます。
配列かマップ(辞書)か
[Key]
にインデックスを指定すると配列の形になりますが、文字列を指定するとマップ(辞書)になります。
[MessagePackObject] public class Person { [Key("Age")] public int Age { get; set; } [Key("FirstName")] public string FirstName { get; set; } [Key("LastName")] public string LastName { get; set; } [IgnoreMember] public string FullName => FirstName + LastName; }
// Keyに連番のインデックスを指定したときのシリアライズ結果(バイナリ + Json) 93 14 A4 4A 6F 68 6E A3 44 6F 65 [20,"John","Doe"] // Keyに文字列を指定したときのシリアライズ結果(バイナリ + Json) 83 A3 41 67 65 14 A9 46 69 72 73 74 4E 61 6D 65 A4 4A 6F 68 6E A8 4C 61 73 74 4E 61 6D 65 A3 44 6F 65 {"Age":20,"FirstName":"John","LastName":"Doe"}
パフォーマンスとバイナリサイズの観点から言うと配列に軍配があがりますが、汎用性の観点から言うとマップに軍配があがるでしょう。これは適材適所ですね。
また例外として[MessagePackObject(keyAsPropertyName: true)]
を利用すると、明示的に[Key]
を指定する必要なくマップで表現されるようになります。
[MessagePackObject(keyAsPropertyName:true)] public class Person { public int Age { get; set; } public string FirstName { get; set; } public string LastName { get; set; } [IgnoreMember] public string FullName => FirstName + LastName; }
// シリアライズ結果(バイナリ + Json) 83 A3 41 67 65 14 A9 46 69 72 73 74 4E 61 6D 65 A4 4A 6F 68 6E A8 4C 61 73 74 4E 61 6D 65 A3 44 6F 65 {"Age":20,"FirstName":"John","LastName":"Doe"}
シリアライズの前・デシリアライズの後に処理を挟む
IMessagePackSerializationCallbackReceiver
を実装することで、シリアライズの前・デシリアライズの後に処理を挟む事ができます。
[MessagePackObject] public class Person : IMessagePackSerializationCallbackReceiver { [Key(0)] public int Age { get; set; } [Key(1)] public string FirstName { get; set; } [Key(2)] public string LastName { get; set; } [IgnoreMember] public string FullName => FirstName + LastName; public void OnBeforeSerialize() { Console.WriteLine("OnBeforeSerialize"); } public void OnAfterDeserialize() { Console.WriteLine("OnAfterDeserialize"); } }