Orleans でのシリアル化のカスタマイズ
Orleans の重要な側面の 1 つは、シリアル化のカスタマイズがサポートされていることです。これは、オブジェクトやデータ構造を格納または送信できる形式に変換し、後で再構築するプロセスです。 これにより、開発者はデータがシステムのさまざまな部分間で送信されるときに、データをエンコードおよびデコードする方法を制御できます。 シリアル化のカスタマイズは、パフォーマンス、相互運用性、セキュリティを最適化するために役立つことがあります。
シリアル化プロバイダー
Orleans には、次の 2 つのシリアライザー実装が用意されています。
これらのパッケージのいずれかを構成する場合は、「Orleans でのシリアル化の構成」を参照してください。
カスタム シリアライザーの実装
カスタム シリアライザーの実装を作成する場合は、いくつかの共通する手順があります。 いくつかのインターフェイスを実装してから、シリアライザーを Orleans ランタイムに登録する必要があります。 以降のセクションでは、これらの手順について詳しく説明します。
まず、次の Orleans シリアル化インターフェイスを実装します。
- IGeneralizedCodec: 複数の型をサポートするコーデックです。
- IGeneralizedCopier: 複数の型のオブジェクトをコピーするための機能を提供します。
- ITypeFilter: 型を読み込み、シリアル化と逆シリアル化に使用できるようにする機能です。
次のカスタム シリアライザーの実装例を考えてみましょう。
internal sealed class CustomOrleansSerializer :
IGeneralizedCodec, IGeneralizedCopier, ITypeFilter
{
void IFieldCodec.WriteField<TBufferWriter>(
ref Writer<TBufferWriter> writer,
uint fieldIdDelta,
Type expectedType,
object value) =>
throw new NotImplementedException();
object IFieldCodec.ReadValue<TInput>(
ref Reader<TInput> reader, Field field) =>
throw new NotImplementedException();
bool IGeneralizedCodec.IsSupportedType(Type type) =>
throw new NotImplementedException();
object IDeepCopier.DeepCopy(object input, CopyContext context) =>
throw new NotImplementedException();
bool IGeneralizedCopier.IsSupportedType(Type type) =>
throw new NotImplementedException();
}
上記の実装例では:
- メソッドの名前解決による競合を回避するために、各インターフェイスが明示的に実装されています。
- メソッドが実装されていないことを示すために、各メソッドが NotImplementedException をスローします。 お客様が各メソッドを実装して、目的の機能を提供する必要があります。
次の手順では、シリアライザーを Orleans ランタイムに登録します。 これは通常、ISerializerBuilder を拡張し、カスタム AddCustomSerializer
拡張メソッドを公開することで実現します。 次の例は、一般的なパターンを示しています。
using Microsoft.Extensions.DependencyInjection;
using Orleans.Serialization;
using Orleans.Serialization.Serializers;
using Orleans.Serialization.Cloning;
public static class SerializationHostingExtensions
{
public static ISerializerBuilder AddCustomSerializer(
this ISerializerBuilder builder)
{
var services = builder.Services;
services.AddSingleton<CustomOrleansSerializer>();
services.AddSingleton<IGeneralizedCodec, CustomOrleansSerializer>();
services.AddSingleton<IGeneralizedCopier, CustomOrleansSerializer>();
services.AddSingleton<ITypeFilter, CustomOrleansSerializer>();
return builder;
}
}
その他の考慮事項としては、カスタム実装に固有のカスタム シリアル化のオプションを受け取るオーバーロードを公開することがあります。 これらのオプションは、ビルダーでの登録と共に構成できます。 これらのオプションは、カスタム シリアライザーの実装に依存関係によって挿入できます。
Orleans では、プロバイダー モデルを使用したサードパーティ製シリアライザーとの統合がサポートされています。 これには、この記事のカスタム シリアル化に関するセクションで説明する IExternalSerializer 型の実装が必要です。 一部の一般的なシリアライザーの統合は Orleans と共に管理されています。次に例を示します。
- プロトコル バッファー: Microsoft.Orleans.OrleansGoogleUtils NuGet パッケージの Orleans.Serialization.ProtobufSerializer。
- Bond: Microsoft.Orleans.Serialization.Bond NuGet パッケージからの Orleans.Serialization.BondSerializer。
- Newtonsoft.Json: コア Orleans ライブラリからの Orleans.Serialization.OrleansJsonSerializer。
次のセクションでは、IExternalSerializer
のカスタム実装について説明します。
カスタム外部シリアライザー
シリアル化の自動生成に加えて、アプリ コードで選択した型のカスタム シリアル化を提供することができます。 Orleans では、ほとんどの種類のアプリに対してシリアル化の自動生成を使用し、シリアライザーを手動でコーディングすることでパフォーマンスを向上できると思われるまれな場合にのみ、カスタム シリアライザーを記述することをお勧めします。 このメモでは、その方法について説明し、それが役に立つ可能性がある特定のケースを確認します。
アプリでシリアル化をカスタマイズする方法には、次の 3 つがあります。
- シリアル化メソッドを型に追加し、適切な属性 (CopierMethodAttribute、SerializerMethodAttribute、DeserializerMethodAttribute) でマークする。 この方法は、アプリが所有する型、つまり新しいメソッドを追加できる型に適しています。
IExternalSerializer
を実装し、構成時に登録する。 この方法は、外部シリアル化ライブラリを統合する場合に便利です。[Serializer(typeof(YourType))]
で注釈が付けられた、3 つのシリアル化メソッドと上記と同じ属性を含む個別の静的クラスを記述する。 この方法は、アプリが所有していない型に対して便利です。たとえば、自分のアプリでは制御できない他のライブラリで定義されている型などです。
以下のセクションでは、これらのシリアル化方法についてそれぞれ説明します。
カスタム シリアル化の概要
Orleans のシリアル化は、次の 3 つのステージで行われます。
- 分離を確保するために、オブジェクトがすぐにディープ コピーされる。
- ネットワークに送信される前に、オブジェクトがメッセージ バイト ストリームにシリアル化される。
- ターゲットのアクティブ化に配信されると、オブジェクトが受信したバイト ストリームから再作成 (逆シリアル化) される。
メッセージで送信されるデータ型 (つまり、メソッド引数または戻り値として渡される型) には、これら 3 つの手順を実行するルーチンが関連付けられている必要があります。 これらのルーチンをまとめて、データ型のシリアライザーと呼びます。
型のコピー機能は単独で存在し、シリアライザーと逆シリアライザーは連携するペアです。 カスタム コピー機能のみを提供することも、カスタム シリアライザーとカスタム逆シリアライザーのみを提供することも、3 つすべてのカスタム実装を提供することもできます。
シリアライザーは、サイロの起動時およびアセンブリが読み込まれるたびに、サポートされているデータ型ごとに登録されます。 型のカスタム シリアライザー ルーチンを使用するには、登録が必要です。 シリアライザーの選択は、コピーまたはシリアル化するオブジェクトの動的な型に基づきます。 このため、抽象クラスまたはインターフェイスのシリアライザーは使用されないので、作成する必要はありません。
どのようなときにカスタム シリアライザーを記述するか
手作りのシリアライザー ルーチンのパフォーマンスが、生成されたバージョンよりも優れていることはほとんどありません。 作成したい場合は、最初に次のオプションを検討する必要があります。
シリアル化またはコピーする必要のないフィールドまたはプロパティがデータ型内にある場合は、NonSerializedAttribute でマークできます。 これにより、生成されたコードで、コピー時およびシリアル化時にこれらのフィールドがスキップされます。 可能な場合は ImmutableAttribute と Immutable<T> を使って、変更できないデータのコピーを回避します。 詳細については、「コピーの最適化」を参照してください。 標準のジェネリック コレクション型の使用を避ける場合は、使用しないでください。 Orleans ランタイムには、コレクションのセマンティクスを使用してコピー、シリアル化、逆シリアル化を最適化するジェネリック コレクション用のカスタム シリアライザーが含まれています。 これらのコレクションには、シリアル化されたバイト ストリームでの特別な "省略形" 表現もあり、それによってパフォーマンスがさらに向上します。 たとえば、
Dictionary<string, string>
はList<Tuple<string, string>>
よりも高速になります。カスタム シリアライザーによってパフォーマンスを大幅に向上できる最も一般的なケースは、単にフィールド値をコピーするだけでは使用できない重要なセマンティック情報がデータ型にエンコードされている場合です。 たとえば、まばらに入力されている配列は、その配列をインデックスと値のペアのコレクションとして扱うことで、より効率的にシリアル化できることがよくあります (アプリではそのデータを操作速度のために完全に実現された配列として保持する場合でも)。
カスタム シリアライザーを記述する前に行うべき重要なことは、生成されたシリアライザーではパフォーマンスが低下していることを確認することです。 ここではプロファイルが少し役立ちますが、さらに重要なのは、さまざまなシリアル化の負荷を使用してアプリのエンド ツー エンドのストレス テストを実行し、シリアル化のミクロな影響ではなく、システム レベルの影響を測定することです。 たとえば、グレイン メソッドとの間でパラメーターを渡したり結果を受け取ったりせず、単純に両端で用意された値を使うテスト バージョンを構築すれば、シリアル化とコピーがシステム パフォーマンスに与える影響に焦点を当てることができます。
シリアル化メソッドを型に追加する
すべてのシリアライザー ルーチンは、それらが操作するクラスまたは構造体の静的メンバーとして実装する必要があります。 ここに示す名前は必須ではありません。登録は、メソッド名ではなく、それぞれの属性の存在に基づいています。 シリアライザー メソッドはパブリックである必要はないことに注意してください。
3 つのシリアル化ルーチンをすべて実装する場合を除き、不足しているメソッドが自動的に生成されるように、型を SerializableAttribute でマークする必要があります。
コピー機能
コピーを行うメソッドには、Orleans.CodeGeneration.CopierMethodAttribute でフラグが設定されます。
[CopierMethod]
static private object Copy(object input, ICopyContext context)
{
// ...
}
通常、コピー機能は、最も簡単に記述できるシリアライザー ルーチンです。 (そのコピー機能が定義されている型と同じ型であることが保証された) オブジェクトを受け取り、オブジェクトの意味的に同等のコピーを返す必要があります。
オブジェクトをコピーする一環として、サブオブジェクトをコピーする必要がある場合は、SerializationManager.DeepCopyInner ルーチンを使用するのが最善の方法です。
var fooCopy = SerializationManager.DeepCopyInner(foo, context);
重要
SerializationManager.DeepCopy ではなく SerializationManager.DeepCopyInner を使用して、完全なコピー操作でオブジェクトの同一性のコンテキストを維持することが重要です。
オブジェクトの同一性を維持する
コピー ルーチンの重要な役割は、オブジェクトの同一性を維持することです。 Orleans ランタイムには、この目的のためのヘルパー クラスが用意されています。 サブ オブジェクトを "手動で" (DeepCopyInner
を呼び出すのではなく) コピーする前に、それが次のように既に参照されているかどうかを確認します。
var fooCopy = context.CheckObjectWhileCopying(foo);
if (fooCopy is null)
{
// Actually make a copy of foo
context.RecordObject(foo, fooCopy);
}
最後の行は RecordObject への呼び出しです。これは、foo
の参照と同じオブジェクトへの将来の参照が、CheckObjectWhileCopying によって正しく検出されるようにするために必要です。
注意
これはクラス インスタンスに対してのみ行う必要があり、struct
インスタンスや、string
、Uri
、enum
などの .NET プリミティブに対して行うべきではありません。
DeepCopyInner
を使ってサブオブジェクトをコピーする場合は、オブジェクトの同一性が自動的に処理されます。
シリアライザー
シリアル化メソッドには、Orleans.CodeGeneration.SerializerMethodAttribute でフラグが設定されます。
[SerializerMethod]
static private void Serialize(
object input,
ISerializationContext context,
Type expected)
{
// ...
}
コピー機能と同様に、シリアライザーに渡される "入力" オブジェクトは、定義している型のインスタンスであることが保証されます。 "想定された" 型は無視される場合があります。これは、データ項目に関するコンパイル時の型情報に基づいており、バイト ストリームで型プレフィックスを形成するためにより高いレベルで使用されます。
サブオブジェクトをシリアル化するには、SerializationManager.SerializeInner ルーチンを使用します。
SerializationManager.SerializeInner(foo, context, typeof(FooType));
foo に特定の想定された型がない場合は、想定された型に null を渡すことができます。
BinaryTokenStreamWriter クラスには、バイト ストリームにデータを書き込むためのさまざまなメソッドが用意されています。 このクラスのインスタンスは、context.StreamWriter
プロパティを使用して取得できます。 ドキュメントについては、クラスを参照してください。
逆シリアライザー
逆シリアル化メソッドには、Orleans.CodeGeneration.DeserializerMethodAttribute でフラグが設定されます。
[DeserializerMethod]
static private object Deserialize(
Type expected,
IDeserializationContext context)
{
//...
}
"想定された" 型は無視される場合があります。これは、データ項目に関するコンパイル時の型情報に基づいており、バイト ストリームで型プレフィックスを形成するためにより高いレベルで使用されます。 作成されるオブジェクトの実際の型は、常に逆シリアライザーが定義されているクラスの型になります。
サブオブジェクトを逆シリアル化するには、SerializationManager.DeserializeInner ルーチンを使用します。
var foo = SerializationManager.DeserializeInner(typeof(FooType), context);
または、次のようにします。
var foo = SerializationManager.DeserializeInner<FooType>(context);
foo に特定の想定された型がない場合は、非ジェネリックの DeserializeInner
バリアントを使用し、想定された型に null
を渡します。
BinaryTokenStreamReader クラスには、バイト ストリームからデータを読み取るためのさまざまなメソッドが用意されています。 このクラスのインスタンスは、context.StreamReader
プロパティを使用して取得できます。 ドキュメントについては、クラスを参照してください。
シリアライザー プロバイダーを記述する
この方法では、クライアントの ClientConfiguration とサイロの GlobalConfiguration の両方で、Orleans.Serialization.IExternalSerializer を実装して SerializationProviderOptions.SerializationProviders プロパティに追加します。 構成について詳しくは、「シリアル化プロバイダー」を参照してください。
IExternalSerializer
の実装は、シリアル化について前に説明したパターンに従い、Initialize
メソッドと、シリアライザーが特定の型をサポートしているかどうかを判断するために Orleans が使用する IsSupportedType
メソッドを追加します。 インターフェイスの定義を次に示します。
public interface IExternalSerializer
{
/// <summary>
/// Initializes the external serializer. Called once when the serialization manager creates
/// an instance of this type
/// </summary>
void Initialize(Logger logger);
/// <summary>
/// Informs the serialization manager whether this serializer supports the type for serialization.
/// </summary>
/// <param name="itemType">The type of the item to be serialized</param>
/// <returns>A value indicating whether the item can be serialized.</returns>
bool IsSupportedType(Type itemType);
/// <summary>
/// Tries to create a copy of source.
/// </summary>
/// <param name="source">The item to create a copy of</param>
/// <param name="context">The context in which the object is being copied.</param>
/// <returns>The copy</returns>
object DeepCopy(object source, ICopyContext context);
/// <summary>
/// Tries to serialize an item.
/// </summary>
/// <param name="item">The instance of the object being serialized</param>
/// <param name="context">The context in which the object is being serialized.</param>
/// <param name="expectedType">The type that the deserializer will expect</param>
void Serialize(object item, ISerializationContext context, Type expectedType);
/// <summary>
/// Tries to deserialize an item.
/// </summary>
/// <param name="context">The context in which the object is being deserialized.</param>
/// <param name="expectedType">The type that should be deserialized</param>
/// <returns>The deserialized object</returns>
object Deserialize(Type expectedType, IDeserializationContext context);
}
個々の型のシリアライザーを記述する
この方法では、属性 [SerializerAttribute(typeof(TargetType))]
で注釈が付けられた新しいクラスを記述します。ここで、TargetType
はシリアル化される型であり、3 つのシリアル化ルーチンを実装します。 これらのルーチンを記述する方法の規則は、IExternalSerializer
を実装する場合と同じです。 Orleans では、[SerializerAttribute(typeof(TargetType))]
を使用してこのクラスが TargetType
のシリアライザーであることを判断します。複数の型をシリアル化できる場合は、この属性を同じクラスで複数回指定できます。 このようなクラスの例を次に示します。
public class User
{
public User BestFriend { get; set; }
public string NickName { get; set; }
public int FavoriteNumber { get; set; }
public DateTimeOffset BirthDate { get; set; }
}
[Orleans.CodeGeneration.SerializerAttribute(typeof(User))]
internal class UserSerializer
{
[CopierMethod]
public static object DeepCopier(
object original, ICopyContext context)
{
var input = (User)original;
var result = new User();
// Record 'result' as a copy of 'input'. Doing this
// immediately after construction allows for data
// structures that have cyclic references or duplicate
// references. For example, imagine that 'input.BestFriend'
// is set to 'input'. In that case, failing to record
// the copy before trying to copy the 'BestFriend' field
// would result in infinite recursion.
context.RecordCopy(original, result);
// Deep-copy each of the fields.
result.BestFriend =
(User)context.SerializationManager.DeepCopy(input.BestFriend);
// strings in .NET are immutable, so they can be shallow-copied.
result.NickName = input.NickName;
// ints are primitive value types, so they can be shallow-copied.
result.FavoriteNumber = input.FavoriteNumber;
result.BirthDate =
(DateTimeOffset)context.SerializationManager.DeepCopy(input.BirthDate);
return result;
}
[SerializerMethod]
public static void Serializer(
object untypedInput, ISerializationContext context, Type expected)
{
var input = (User) untypedInput;
// Serialize each field.
SerializationManager.SerializeInner(input.BestFriend, context);
SerializationManager.SerializeInner(input.NickName, context);
SerializationManager.SerializeInner(input.FavoriteNumber, context);
SerializationManager.SerializeInner(input.BirthDate, context);
}
[DeserializerMethod]
public static object Deserializer(
Type expected, IDeserializationContext context)
{
var result = new User();
// Record 'result' immediately after constructing it.
// As with the deep copier, this
// allows for cyclic references and de-duplication.
context.RecordObject(result);
// Deserialize each field in the order that they were serialized.
result.BestFriend =
SerializationManager.DeserializeInner<User>(context);
result.NickName =
SerializationManager.DeserializeInner<string>(context);
result.FavoriteNumber =
SerializationManager.DeserializeInner<int>(context);
result.BirthDate =
SerializationManager.DeserializeInner<DateTimeOffset>(context);
return result;
}
}
ジェネリック型をシリアル化する
[Serializer(typeof(TargetType))]
の TargetType
パラメーターには、オープン ジェネリック型を指定できます (例: MyGenericType<T>
)。 その場合、シリアライザー クラスにはターゲット型と同じジェネリック パラメーターが必要です。 Orleans では、シリアル化されるすべての MyGenericType<T>
の具象型ごとに、実行時にシリアライザーの具象バージョンを作成します。たとえば、MyGenericType<int>
と MyGenericType<string>
のそれぞれに対して 1 つ作成します。
シリアライザーと逆シリアライザーを記述するためのヒント
多くの場合、シリアライザーと逆シリアライザーのペアを記述する最も簡単な方法は、バイト配列を構築し、配列の長さと、それに続けて配列自体をストリームに書き込むことでシリアル化し、逆のプロセスを行って逆シリアル化することです。 配列が固定長の場合は、ストリームから省略できます。 これは、データ型をコンパクトに表すことができ、重複する可能性のあるサブオブジェクトがない (したがって、オブジェクトの同一性について心配する必要がない) 場合に適切に機能します。
重要かつ複雑な内部構造を持つクラスに対しては、別の方法 (Orleans ランタイムがディクショナリなどのコレクションに対して取る方法) が適切に機能します。すなわち、インスタンス メソッドを使用してオブジェクトの意味的内容にアクセスし、その内容をシリアル化し、複雑な内部状態ではなく意味的内容を設定することで逆シリアル化する方法です。 この方法では、内部オブジェクトは SerializeInner を使用して書き込まれ、DeserializeInner を使用して読み取られます。 この場合、カスタム コピー機能も記述するのが一般的です。
カスタム シリアライザーを記述して、それが最終的にクラス内の各フィールドに対する一連の SerializeInner の呼び出しのようになる場合は、そのクラスに対してカスタム シリアライザーは必要ありません。
関連項目
.NET