Orleans でのシリアル化
Orleans で使用されるシリアル化は、大きく分けて以下の 2 種類があります。
- グレイン呼び出しのシリアル化 - グレインとの間で受け渡しされるオブジェクトをシリアル化するために使用されます。
- グレイン ストレージのシリアル化 - ストレージ システムとの間でオブジェクトをシリアル化するために使用されます。
この記事の大部分は、Orleans に含まれるシリアル化フレームワークを介したグレイン呼び出しのシリアル化に特化しています。 「グレイン ストレージ シリアライザー」セクションでは、グレイン ストレージのシリアル化について説明します。
Orleans シリアル化を使用する
Orleans には、Orleans.Serialization と呼ばれる高度で拡張可能なシリアル化フレームワークがあります。 Orleans に含まれるシリアル化フレームワークは、次の目標を満たすように設計されています。
- 高パフォーマンス - シリアライザーは、パフォーマンスのために設計および最適化されています。 詳細については、このプレゼンテーションをご覧ください。
- 高忠実度 - シリアライザーは、ジェネリック、ポリモーフィズム、継承階層、オブジェクト ID、循環グラフのサポートなど、.NET 型システムの大部分を忠実に表します。 ポインターはプロセス間で移植できないため、サポートされていません。
- 柔軟性 - シリアライザーは、サロゲートを作成するか、System.Text.Json、Newtonsoft.Json、Google.Protobuf などの外部シリアル化ライブラリに委任することで、サードパーティのライブラリをサポートするようにカスタマイズできます。
- バージョンの許容範囲 - シリアライザーを使用すると、時間の経過と共にアプリケーションの種類が進化し、次の機能をサポートできるようになります。
- メンバーの追加と削除
- サブクラス
- 数値の拡大と縮小 (例:
int
とlong
との間、float
とdouble
との間) - 型の名前を変更
忠実度の高い型の表現はシリアライザーではそれほど一般的ではありません。そのため、以下の点でさらに詳しく理解する必要があります。
動的な型と任意のポリモーフィズム: Orleans は、グレイン呼び出しで渡すことができる型に制限を適用せず、実際のデータ型の動的な性質を維持します。 たとえば、グレイン インターフェイス内のメソッドが IDictionary を受け入れるように宣言されているものの、実行時に送信者が SortedDictionary<TKey,TValue> を渡すと、受信者は実際には
SortedDictionary
を取得します (ただし、"静的コントラクト"/グレイン インターフェイスではこの動作は指定されませんでした)。オブジェクト ID の維持: 同じオブジェクトがグレイン呼び出しの引数で複数の型を渡されるか、引数から間接的に複数回ポイントされている場合、Orleans は 1 回だけシリアル化されます。 受信側では、Orleans は逆シリアル化後も同じオブジェクトをポイントする 2 つのポインターが同じオブジェクトをポイントするように、すべての参照が正しく復元されます。 オブジェクト ID は、次のようなシナリオで保持することが重要です。 グレイン A が、グレイン B に 100 個のエントリを含むディクショナリを送信し、ディクショナリ内の 10 個のキーが A 側の同じオブジェクトである
obj
をポイントしている場合を想像してください。 オブジェクト ID を保持しない場合、B は 100 個のエントリのディクショナリを受け取り、そのうち 10 個のキーはobj
の 10 個の異なる複製をポイントします。 オブジェクト ID を保持する場合、B 側のディクショナリは、単一のオブジェクトobj
をポイントする 10 個のキーを持つ A 側とまったく同じように見えます。 .NET の既定の文字列ハッシュ コードの実装はプロセスごとにランダム化されるため、(たとえば) ディクショナリとハッシュ セットの値の順序は保持されない場合があることに注意してください。
バージョンの許容範囲をサポートするために、シリアライザーでは、シリアル化する型とメンバーを開発者が明示的に指定する必要があります。 これを可能な限り痛みを伴わないものにしようとしました。 型のシリアライザー コードを生成するように Orleans に指示するには、Orleans.GenerateSerializerAttribute にすべてのシリアル化可能な型をマークする必要があります。 これを行うと、次に示すように、含まれているコード修正を使用して、型のシリアル化可能メンバーに必要な Orleans.IdAttribute を追加できます。
属性を適用する方法を示す、Orleans でシリアル化可能な型の例を次に示します。
[GenerateSerializer]
public class Employee
{
[Id(0)]
public string Name { get; set; }
}
Orleans は継承をサポートし、階層内で個々のレイヤーを個別にシリアル化し、個別のメンバー ID を持つことができます。
[GenerateSerializer]
public class Publication
{
[Id(0)]
public string Title { get; set; }
}
[GenerateSerializer]
public class Book : Publication
{
[Id(0)]
public string ISBN { get; set; }
}
前のコードでは、Book
が Publication
から派生しているにもかかわらず、Publication
と Book
の両方に [Id(0)]
を含むメンバーがいることに注意してください。 メンバー識別子は、型全体ではなく継承レベルに範囲指定されるため、これは Orleans で推奨されている方法です。 メンバーの追加と削除は Publication
と Book
で個別に行うことができますが、特別な考慮なしにアプリケーションをデプロイすると、新しい基底クラスを階層に挿入することはできません。
Orleans は、次の例の型のように、internal
、private
、readonly
のメンバーの型のシリアル化もサポートします。
[GenerateSerializer]
public struct MyCustomStruct
{
public MyCustom(int intProperty, int intField)
{
IntProperty = intProperty;
_intField = intField;
}
[Id(0)]
public int IntProperty { get; }
[Id(1)] private readonly int _intField;
public int GetIntField() => _intField;
public override string ToString() => $"{nameof(_intField)}: {_intField}, {nameof(IntProperty)}: {IntProperty}";
}
既定では、Orleans はフル ネームをエンコードして型をシリアル化します。 Orleans.AliasAttribute を追加することで、これを上書きすることができます。 その結果、基になるクラスの名前を変更したり、アセンブリ間で移動したりする回復性のある名前を使用して型がシリアル化されます。 型のエイリアスはグローバルに範囲指定され、アプリケーションで 2 つのエイリアスに同じ値を持たせることはできません。 ジェネリック型の場合、エイリアス値には、バッククォートの前にジェネリック パラメーターの数を含める必要があります。たとえば、MyGenericType<T, U>
には エイリアス [Alias("mytype`2")]
を指定できます。
record
型のシリアル化
レコードのプライマリ コンストラクターで定義されているメンバーには、既定で暗黙的な ID があります。 言い換えると、Orleans では record
型のシリアル化がサポートされます。 つまり、アプリケーションの以前のバージョン (ローリング アップグレードの場合) やストレージとストリーム内のその型のシリアル化されたインスタンスとの互換性が解除されるため、既にデプロイされている型のパラメーター順序を変更することはできません。 レコード型の本体で定義されたメンバーは、プライマリ コンストラクター パラメーターと ID を共有しません。
[GenerateSerializer]
public record MyRecord(string A, string B)
{
// ID 0 won't clash with A in primary constructor as they don't share identities
[Id(0)]
public string C { get; init; }
}
プライマリ コンストラクター パラメーターを Serializable フィールドとして自動的に含めない場合は、[GenerateSerializer(IncludePrimaryConstructorParameters = false)]
を使用できます。
外部型をシリアル化するためのサロゲート
完全に制御できないグレイン間で型を渡すことが必要な場合があります。 このような場合は、アプリケーション コード内のカスタム定義型との手動変換は実用的でない可能性があります。 Orleans は、サロゲート型の形式でこのような状況に対するソリューションを提供します。 サロゲートはターゲット型の代わりにシリアル化され、ターゲット型との間で変換される機能があります。 次の外部型と対応するサロゲートとコンバーターの例を考えてみましょう。
// This is the foreign type, which you do not have control over.
public struct MyForeignLibraryValueType
{
public MyForeignLibraryValueType(int num, string str, DateTimeOffset dto)
{
Num = num;
String = str;
DateTimeOffset = dto;
}
public int Num { get; }
public string String { get; }
public DateTimeOffset DateTimeOffset { get; }
}
// This is the surrogate which will act as a stand-in for the foreign type.
// Surrogates should use plain fields instead of properties for better performance.
[GenerateSerializer]
public struct MyForeignLibraryValueTypeSurrogate
{
[Id(0)]
public int Num;
[Id(1)]
public string String;
[Id(2)]
public DateTimeOffset DateTimeOffset;
}
// This is a converter that converts between the surrogate and the foreign type.
[RegisterConverter]
public sealed class MyForeignLibraryValueTypeSurrogateConverter :
IConverter<MyForeignLibraryValueType, MyForeignLibraryValueTypeSurrogate>
{
public MyForeignLibraryValueType ConvertFromSurrogate(
in MyForeignLibraryValueTypeSurrogate surrogate) =>
new(surrogate.Num, surrogate.String, surrogate.DateTimeOffset);
public MyForeignLibraryValueTypeSurrogate ConvertToSurrogate(
in MyForeignLibraryValueType value) =>
new()
{
Num = value.Num,
String = value.String,
DateTimeOffset = value.DateTimeOffset
};
}
上のコードでは以下の操作が行われます。
MyForeignLibraryValueType
は、使用するライブラリで定義されている、コントロールの外側の型です。MyForeignLibraryValueTypeSurrogate
はMyForeignLibraryValueType
にマップされるサロゲート型です。- RegisterConverterAttribute は、
MyForeignLibraryValueTypeSurrogateConverter
が 2 つの型との間でマップするコンバーターとして機能することを指定します。 このクラスは、IConverter<TValue,TSurrogate> インターフェイスの実装です。
Orleans では、型階層 (他の型から派生した型) の型のシリアル化がサポートされています。 外部型が型階層に表示される可能性がある場合 (独自の型の基底クラスとしてなど)、Orleans.IPopulator<TValue,TSurrogate> インターフェイスを追加で実装する必要があります。 次に例を示します。
// The foreign type is not sealed, allowing other types to inherit from it.
public class MyForeignLibraryType
{
public MyForeignLibraryType() { }
public MyForeignLibraryType(int num, string str, DateTimeOffset dto)
{
Num = num;
String = str;
DateTimeOffset = dto;
}
public int Num { get; set; }
public string String { get; set; }
public DateTimeOffset DateTimeOffset { get; set; }
}
// The surrogate is defined as it was in the previous example.
[GenerateSerializer]
public struct MyForeignLibraryTypeSurrogate
{
[Id(0)]
public int Num;
[Id(1)]
public string String;
[Id(2)]
public DateTimeOffset DateTimeOffset;
}
// Implement the IConverter and IPopulator interfaces on the converter.
[RegisterConverter]
public sealed class MyForeignLibraryTypeSurrogateConverter :
IConverter<MyForeignLibraryType, MyForeignLibraryTypeSurrogate>,
IPopulator<MyForeignLibraryType, MyForeignLibraryTypeSurrogate>
{
public MyForeignLibraryType ConvertFromSurrogate(
in MyForeignLibraryTypeSurrogate surrogate) =>
new(surrogate.Num, surrogate.String, surrogate.DateTimeOffset);
public MyForeignLibraryTypeSurrogate ConvertToSurrogate(
in MyForeignLibraryType value) =>
new()
{
Num = value.Num,
String = value.String,
DateTimeOffset = value.DateTimeOffset
};
public void Populate(
in MyForeignLibraryTypeSurrogate surrogate, MyForeignLibraryType value)
{
value.Num = surrogate.Num;
value.String = surrogate.String;
value.DateTimeOffset = surrogate.DateTimeOffset;
}
}
// Application types can inherit from the foreign type, assuming they're not sealed
// since Orleans knows how to serialize it.
[GenerateSerializer]
public sealed class DerivedFromMyForeignLibraryType : MyForeignLibraryType
{
public DerivedFromMyForeignLibraryType() { }
public DerivedFromMyForeignLibraryType(
int intValue, int num, string str, DateTimeOffset dto) : base(num, str, dto)
{
IntValue = intValue;
}
[Id(0)]
public int IntValue { get; set; }
}
バージョン管理ルール
開発者が型の変更時に一連の規則に従う場合、バージョンの許容範囲がサポートされます。 開発者が Google プロトコル バッファー (Protobuf) などのシステムに精通している場合、これらのルールは使いやすいものになります。
複合型 (class
と struct
)
- 継承はサポートされていますが、オブジェクトの継承階層の変更はサポートされていません。 クラスの基底クラスを追加したり、別のクラスに変更したり、削除したりすることはできません。
- 以下の「数値」セクションで説明されている一部の数値型を除き、フィールド型は変更できません。
- フィールドは、継承階層内の任意の時点で追加または削除できます。
- フィールド ID は変更できません。
- フィールド ID は、型階層の各レベルで一意である必要がありますが、基底クラスとサブクラスの間で再利用できます。 たとえば、
Base
クラスは ID0
を持つフィールドを宣言でき、別のフィールドは同じ ID0
を持つSub : Base
で宣言できます。
数値
- 数値フィールドの署名は変更できません。
int
とuint
の間の変換は無効です。
- 数値フィールドの幅は変更できます。
- 例:
int
からlong
、ulong
、またはushort
への変換がサポートされています。 - フィールドのランタイム値でオーバーフローが発生する場合、幅を狭くする変換がスローされます。
ulong
からushort
への変換は、実行時の値がushort.MaxValue
未満である場合のみサポートされます。double
からfloat
への変換は、ランタイム値がfloat.MinValue
とfloat.MaxValue
の間にある場合にのみサポートされます。double
とfloat
の両方よりも範囲が狭いdecimal
の場合も同様です。
- 例:
コピー機
Orleans は、既定で安全性を促進します。 これには、コンカレンシー バグの一部のクラスからの安全性が含まれます。 特に、Orleans は既定のグレイン呼び出しで渡されたオブジェクトをすぐにコピーします。 このコピーは Orleans によって促進されます。シリアル化と Orleans.CodeGeneration.GenerateSerializerAttribute が型に適用された場合に、Orleans でその型のコピー機も生成されます。 Orleans は、ImmutableAttribute を使用してマークされた型または個々のメンバーのコピーを回避します。 詳細については、「Orleans での不変型のシリアル化」を参照してください。
推奨されるシリアル化の手順
✅
[Alias("my-type")]
属性を使用して型にエイリアスを指定します。 エイリアスを持つ型は、互換性を損なうことなく名前を変更できます。❌
record
を通常のclass
に変更したり、その逆をしたりしないでください。 レコードとクラスには、通常のメンバーに加えてプライマリ コンストラクター メンバーが存在し、両者のメンバーは交換できないため、同じ方法で表されることはありません。❌シリアル化可能な型の既存の型階層に新しい型を追加しないでください。 既存の型に新しい基底クラスを追加することはできません。 新しいサブクラスを既存の型に安全に追加できます。
✅SerializableAttribute の使用を GenerateSerializerAttribute に置き換え、IdAttribute 宣言に対応させます。
✅すべてのメンバー ID を型ごとに 0 から開始します。 サブクラスとその基底クラスの ID は、安全に重複させることができます。 次の例の両方のプロパティは、
0
と同じ ID になります。[GenerateSerializer] public sealed class MyBaseClass { [Id(0)] public int MyBaseInt { get; set; } } [GenerateSerializer] public sealed class MySubClass : MyBaseClass { [Id(0)] public int MyBaseInt { get; set; } }
✅必要に応じて、数値メンバー型を拡大します。
sbyte
からshort
、int
、long
と拡大させることができます。- 数値メンバー型を絞り込むことができますが、絞り込まれた型で観測値を正しく表すことができない場合、ランタイム例外が発生します。 たとえば、
int.MaxValue
をshort
フィールドで表すことができないため、int
フィールドをshort
に絞り込むと、このような値が検出された場合にランタイム例外が発生する可能性があります。
- 数値メンバー型を絞り込むことができますが、絞り込まれた型で観測値を正しく表すことができない場合、ランタイム例外が発生します。 たとえば、
❌数値型メンバーの署名を変更しないでください。 たとえば、メンバーの型を
uint
からint
またはint
からuint
に変更することはできません。
グレイン ストレージ シリアライザー
Orleans には、State プロパティ経由でアクセスするか、グレインに 1 つ以上の IPersistentState<TState> 値を挿入することによってアクセスする、グレイン用のプロバイダーに基づく永続化モデルが含まれます。 Orleans 7.0 以前は、各プロバイダーにシリアル化を構成するためのさまざまなメカニズムがありました。 Orleans 7.0 では、汎用グレイン状態シリアライザー インターフェイス IGrainStorageSerializer が追加されました。これにより、各プロバイダーの状態シリアル化を一貫した方法でカスタマイズできます。 サポートされているストレージ プロバイダーは、プロバイダーのオプション クラスに IStorageProviderSerializerOptions.GrainStorageSerializer プロパティを設定する次のようなパターンを実装します。
- DynamoDBStorageOptions.GrainStorageSerializer
- AzureBlobStorageOptions.GrainStorageSerializer
- AzureTableStorageOptions.GrainStorageSerializer
- GrainStorageSerializer
グレイン ストレージのシリアル化の現在の既定は、状態をシリアル化する Newtonsoft.Json
です。 構成時にそのプロパティを変更することで、これを置き換えることができます。 次の例では、OptionsBuilder<TOptions> を使用してこれを示しています。
siloBuilder.AddAzureBlobGrainStorage(
"MyGrainStorage",
(OptionsBuilder<AzureBlobStorageOptions> optionsBuilder) =>
{
optionsBuilder.Configure<IMySerializer>(
(options, serializer) => options.GrainStorageSerializer = serializer);
});
詳細については、「OptionsBuilder API」を参照してください。
Orleans には、高度で拡張可能なシリアル化フレームワークがあります。 Orleans は、グレイン要求および応答メッセージで渡されたデータ型とグレイン永続状態オブジェクトをシリアル化します。 このフレームワークの一部として、Orleans でこれらのデータ型のシリアル化コードが自動的に生成されます。 既に .NET シリアル化可能である型に対して、より効率的なシリアル化/逆シリアル化を生成することに加えて、Orleans では .NET シリアル化可能でないグレイン インターフェイスで使用される型のシリアライザーの生成も試みます。 フレームワークには、よく使用される型 (リスト、ディクショナリ、文字列、プリミティブ、配列など) の効率的な組み込みシリアライザーのセットもあります。
Orleans のシリアライザーの 2 つの重要な機能は、動的な型/任意のポリモーフィズムとオブジェクト IDなど、他の多くのサードパーティ製シリアル化フレームワークとは別に設定されています。
動的な型と任意のポリモーフィズム: Orleans は、グレイン呼び出しで渡すことができる型に制限を適用せず、実際のデータ型の動的な性質を維持します。 たとえば、グレイン インターフェイス内のメソッドが IDictionary を受け入れるように宣言されているものの、実行時に送信者が SortedDictionary<TKey,TValue> を渡すと、受信者は実際には
SortedDictionary
を取得します (ただし、"静的コントラクト"/グレイン インターフェイスではこの動作は指定されませんでした)。オブジェクト ID の維持: 同じオブジェクトがグレイン呼び出しの引数で複数の型を渡されるか、引数から間接的に複数回ポイントされている場合、Orleans は 1 回だけシリアル化されます。 受信側では、Orleans は逆シリアル化後も同じオブジェクトをポイントする 2 つのポインターが同じオブジェクトをポイントするように、すべての参照が正しく復元されます。 オブジェクト ID は、次のようなシナリオで保持することが重要です。 グレイン A が、グレイン B に 100 個のエントリを含むディクショナリを送信し、ディクショナリ内の 10 個のキーが A 側の同じオブジェクトである obj をポイントしている場合を想像してください。 オブジェクト ID を保持しない場合、B は 100 個のエントリのディクショナリを受け取り、10 個のキーが obj の 10 個の異なるクローンをポイントします。オブジェクト ID が保持されている場合、B 側のディクショナリは、1 個のオブジェクト obj をポイントする 10 個のキーを持つ A 側とまったく同じように見えます。
上記の 2 つの動作は標準の .NET バイナリ シリアライザーによって指定されるため、この標準の使い慣れた動作 Orleans もサポートすることが重要でした。
生成されたシリアライザー
Orleans では、次の規則を使用して、生成するシリアライザーを決定します。 規則は以下のとおりです。
- コア Orleans ライブラリを参照するすべてのアセンブリですべての型をスキャンします。
- これらのアセンブリから: グレイン インターフェイス メソッドのシグネチャまたは状態クラスのシグネチャで直接参照される型、または SerializableAttribute でマークされている任意の型のシリアライザーを生成します。
- さらに、グレイン インターフェイスまたは実装プロジェクトは、KnownTypeAttribute または KnownAssemblyAttribute アセンブリ レベルの属性を追加して、特定の型またはアセンブリ内のすべての対象となる型のシリアライザーを生成するようにコード ジェネレーターに指示することで、シリアル化生成用の任意の型をポイントすることができます。 アセンブリ レベルの属性の詳細については、「アセンブリ レベルで属性を適用する」を参照してください。
フォールバック シリアル化
Orleans は実行時に任意の型の転送をサポートするため、組み込みのコード ジェネレーターで事前に送信される一連の型全体を判断することはできません。 また、特定の型では、アクセスできない (private
など) 場合やアクセスできないフィールド (readonly
など) があるため、シリアライザーを生成できません。 そのため、予期しない型、または事前にシリアライザーを生成できなかった型の Just-In-Time シリアル化が必要です。 これらの型を担当するシリアライザーは、フォールバック シリアライザーと呼ばれます。 Orleans には、次の 2 つのフォールバック シリアライザーが付属しています。
- .NET の BinaryFormatter を使用する Orleans.Serialization.BinaryFormatterSerializer と
- 実行時に CIL 手順を生成する Orleans.Serialization.ILBasedSerializer では、Orleans シリアル化フレームワークを利用して各フィールドをシリアル化するシリアライザーを作成します。 つまり、アクセスできない型
MyPrivateType
にカスタム シリアライザーを持つフィールドMyType
が含まれている場合、そのカスタム シリアライザーがシリアル化に使用されます。
フォールバック シリアライザーは、クライアントの ClientConfiguration とサイロの GlobalConfiguration の両方で FallbackSerializationProvider プロパティを使用して構成できます。
// Client configuration
var clientConfiguration = new ClientConfiguration();
clientConfiguration.FallbackSerializationProvider =
typeof(FantasticSerializer).GetTypeInfo();
// Global configuration
var globalConfiguration = new GlobalConfiguration();
globalConfiguration.FallbackSerializationProvider =
typeof(FantasticSerializer).GetTypeInfo();
または、フォールバック シリアル化プロバイダーを XML 構成で指定することもできます。
<Messaging>
<FallbackSerializationProvider
Type="GreatCompany.FantasticFallbackSerializer, GreatCompany.SerializerAssembly"/>
</Messaging>
BinaryFormatterSerializer は、既定のフォールバック シリアライザーです。
警告
BinaryFormatter
によるバイナリ シリアル化は危険が伴う場合があります。 詳細については、「BinaryFormatter セキュリティ ガイド」および「BinaryFormatter 移行ガイド」を参照してください。
例外のシリアル化
例外は、フォールバック シリアライザーを使用してシリアル化されます。 既定の構成を使用する BinaryFormatter
はフォールバック シリアライザーであるため、例外の型のすべてのプロパティを正しくシリアル化するには、ISerializable パターンに従う必要があります。
正しく実装されたシリアル化を使用した例外の型の例を次に示します。
[Serializable]
public class MyCustomException : Exception
{
public string MyProperty { get; }
public MyCustomException(string myProperty, string message)
: base(message)
{
MyProperty = myProperty;
}
public MyCustomException(string transactionId, string message, Exception innerException)
: base(message, innerException)
{
MyProperty = transactionId;
}
// Note: This is the constructor called by BinaryFormatter during deserialization
public MyCustomException(SerializationInfo info, StreamingContext context)
: base(info, context)
{
MyProperty = info.GetString(nameof(MyProperty));
}
// Note: This method is called by BinaryFormatter during serialization
public override void GetObjectData(SerializationInfo info, StreamingContext context)
{
base.GetObjectData(info, context);
info.AddValue(nameof(MyProperty), MyProperty);
}
}
推奨されるシリアル化の手順
シリアル化は、Orleans の以下の 2 つの主な目的を果たします。
- 実行時にグレインとクライアントの間でデータを送信するためのワイヤ形式として。
- 後で取得するために有効期間の長いデータを保持するためのストレージ形式として。
Orleans で生成されるシリアライザーは、柔軟性、パフォーマンス、および汎用性により、最初の目的に適しています。 これらは明示的にバージョンの許容範囲でないため、2 番目の目的には適していません。 ユーザーは、永続的なデータ用のプロトコル バッファーなどのバージョン許容範囲シリアライザーを構成することをお勧めします。 プロトコル バッファーは、Microsoft.Orleans.OrleansGoogleUtils NuGet の Orleans.Serialization.ProtobufSerializer
を経由してサポートされます。 バージョンの許容範囲を確保するには、選択した特定のシリアライザーのベスト プラクティスを使用する必要があります。 サード パーティ製シリアライザーは、前述のように SerializationProviders
構成プロパティを使用して構成できます。
.NET