ここではBinaryFormatterおよびSoapFormatterを使ってシリアライズする場合の動作の制御について解説します。
以下の解説で登場するBinaryFormatter/SoapFormatterについて、.NET 5以降においては使用は推奨されず、できるだけ早く使用をやめる必要があるとされています。 またこれを使用したコードでは、コンパイル時警告SYSLIB0011が出力されます。 特にASP.NET 5.0以降では、明示的にBinaryFormatterの使用を有効にしない限り常に例外NotSupportedExceptionがスローされます。
BinaryFormatter 型は危険であり、データ処理用としては "推奨されません"。 アプリケーションでは、処理するデータが信頼できると思われる場合でも、できるだけ早く BinaryFormatter の使用をやめる必要があります。 BinaryFormatter は安全ではなく、セキュリティで保護することはできません。
BinaryFormatter セキュリティ ガイド | Microsoft Docs
BinaryFormatter シリアル化メソッドが古い形式になり、ASP.NET アプリでは使用不可に
BinaryFormatter、Formatter、および IFormatter の Serialize と Deserialize のメソッドが古いと見なされ、警告が示されるようになりました。 また、ASP.NET アプリでは、BinaryFormatter のシリアル化が既定で禁止されます。
変更の説明
BinaryFormatter のセキュリティ脆弱性により、次のメソッドは古いと見なされ、ID SYSLIB0011 のコンパイル時警告が生成されるようになりました。 また、ASP.NET Core 5.0 以降のアプリでは、Web アプリによって BinaryFormatter 機能が再有効化されていない限り、NotSupportedException がスローされます。
基本クラス ライブラリの破壊的変更 - .NET Core | Microsoft Docs
- BinaryFormatter.Serialize
- BinaryFormatter.Deserialize
新規に作成するコードでは、BinaryFormatter/SoapFormatterの使用を避けるか、使用する場合は慎重に検討してください。 すでに使用しているコードでは、XmlSerializerやJsonSerializerの使用、あるいは他の手段への変更を検討してください。
シリアライズ対象からの除外 (NonSerializedAttribute)
NonSerializedAttributeを使用すると、フィールドをシリアライズ対象から除外することが出来ます。 BinaryFormatterおよびSoapFormatterではスコープに関わらずすべてのフィールドをシリアライズしようとしますが、シリアライズする必要の無いフィールドや実行時のみに使用されるフィールドなどは、この属性を使用することでシリアライズしないようにすることが出来ます。
以下の例では、Account.LastLoginフィールドにNonSerializedAttributeを付与しています。 デシリアライズ後にAccount.LastLoginフィールドの値が復元されていない点に注目してください。
追加されたフィールドの無視 (OptionalFieldAttribute)
OptionalFieldAttributeは新しいバージョンで追加されるフィールドに付与します。 例えば、あるバージョンでの型をシリアライズした後、型にフィールドを追加した新しいバージョンでデシリアライズしようとすると、古いバージョンのストリームには追加した新しいフィールドが含まれていないため例外がスローされます。 このような場合に例外をスローしないよう、追加したフィールドにはOptionalFieldAttributeを付与しておく必要があります。
例として、SoapFormatterを使って、二つのバージョンを跨いだシリアライズ・デシリアライズを行ってみます。 まずは、バージョン1のコードを使ってシリアライズを行います。
バージョン1のAccountクラスには二つのフィールドIDとNameがあります。 このコードをビルドして実行ファイルser.exeを作成、実行します。 シリアライズされたファイルalice.soap.xml
の内容は次のようになります。
続いて、先ほどのコードを書き換え、バージョン2のコードを使ってデシリアライズを行います。
バージョン2のAccountクラスには、バージョン1から存在するフィールドID・Nameと、バージョン2で新しく追加したフィールドContactAddressがあります。 このコードをビルドして実行ファイルser.exeを作成、実行すると、特に問題なくデシリアライズ出来ます。
しかし、これをOptionalFieldAttributeを付与していない状態で実行すると、以下のようにSerializationExceptionがスローされます。
なお、BinaryFormatterを使った場合はOptionalFieldAttributeが付与されていなくても特に例外はスローされないようですが、念のため付与しておいたほうがよいと思われます。
シリアライズ前後のコールバック (OnSerializingAttribute, OnSerializedAttribute, OnDeserializingAttribute, OnDeserializedAttribute)
シリアライズ・デシリアライズの前後で値の検証やデフォルト値の設定などの追加の処理を行いたい場合は、次の属性を使用することでシリアライザにコールバックさせることが出来ます。 これらの属性はフィールドではなくコールバック用のメソッドに付与します。
属性 | 機能 |
---|---|
OnSerializingAttribute | シリアライズ前にコールバックするメソッドを指定する |
OnSerializedAttribute | シリアライズ後にコールバックするメソッドを指定する |
OnDeserializingAttribute | デシリアライズ前にコールバックするメソッドを指定する |
OnDeserializedAttribute | デシリアライズ後にコールバックするメソッドを指定する |
なお、これらの属性を付与するメソッドは、StreamingContextを引数にとる必要があります。
シリアライズ動作の制御 (ISerializable)
ISerializableインターフェイスを使うことで、ここまでに解説してきた属性を用いるよるも柔軟にシリアライズ・デシリアライズ時の動作を制御することが出来ます。 例えば、シリアライズする値とその名前を指定したり、シリアライズ・デシリアライズの前後でデフォルト値の設定や値のチェックの処理を追加することも出来ます。
ISerializableを実装する場合は、シリアライズを行うためのメソッドISerializable.GetObjectDataとデシリアライズを行うためのコンストラクタを用意する必要があります。 これらはどちらもパブリックである必要はありません。
このメソッドとコンストラクタでは、引数の一つにシリアライズ・デシリアライズする値を格納するためのSerializationInfoを取ります。 シリアライズする値を追加するにはAddValueメソッド、デシリアライズされた値を取得するにはGetStringメソッドやGetValueメソッドなどを使います。 この時、値とその名前を指定出来ますが、名前にはフィールド名ではなく任意の名前を代わりに指定することも出来ます。
次の例は、ISerializableを使ってシリアライズ・デシリアライズを行う例です。
結果を見てのとおり、適切にデシリアライズされていることが分かると思います。 なお、SerializationInfo.AddValueで指定した値の名前は、値を格納するXML要素の名前としても使われます。 以下はシリアライズされたファイルalice.soap.xml
の内容です。 Account.LastLoginフィールドの値はSerializationInfoに追加していないため、シリアライズされた内容には含まれていない点にも注目してください。
ISerializableインターフェイスを実装するクラスを継承可能にするには、コンストラクタとメソッドを派生クラスに公開し、オーバーライドできるようにしておく必要があります。 派生クラスでは、SerializationInfoを基底クラスに引き渡すことで基底クラスのフィールドをシリアライズ・デシリアライズさせるようにします。 以下の例では、基底クラスでISerializableを実装し、その派生クラスをシリアライズしています。
デシリアライズ後のコールバック (IDeserializationCallback)
IDeserializationCallbackインターフェイスもデシリアライズが完了した時点で呼び出されるコールバックメソッドを実装するためのものです。 OnDeserializedAttributeと機能と動作は同じです。 引数としてStreamingContextの代わりにobjectをとりますが、有効な値は渡されないようです。