ここでは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 がスローされます。

  • BinaryFormatter.Serialize
  • BinaryFormatter.Deserialize
基本クラス ライブラリの破壊的変更 - .NET Core | Microsoft Docs

新規に作成するコードでは、BinaryFormatter/SoapFormatterの使用を避けるか、使用する場合は慎重に検討してください。 すでに使用しているコードでは、XmlSerializerJsonSerializerの使用、あるいは他の手段への変更を検討してください。

シリアライズ対象からの除外 (NonSerializedAttribute)

NonSerializedAttributeを使用すると、フィールドをシリアライズ対象から除外することが出来ます。 BinaryFormatterおよびSoapFormatterではスコープに関わらずすべてのフィールドをシリアライズしようとしますが、シリアライズする必要の無いフィールドや実行時のみに使用されるフィールドなどは、この属性を使用することでシリアライズしないようにすることが出来ます。

以下の例では、Account.LastLoginフィールドにNonSerializedAttributeを付与しています。 デシリアライズ後にAccount.LastLoginフィールドの値が復元されていない点に注目してください。

using System;
using System.IO;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;

[Serializable]
class Account {
  public int ID;
  public string Name;

  [NonSerialized]
  public DateTime LastLogin;

  public override string ToString()
  {
    return string.Format("{0}:{1} LastLogin={2}", ID, Name, LastLogin);
  }
}

class Sample {
  static void Main()
  {
    Account alice = new Account();

    alice.ID = 2;
    alice.Name = "Alice";
    alice.LastLogin = new DateTime(2010, 10, 20);

    Console.WriteLine(alice);

    // シリアライズ
    using (Stream stream = File.OpenWrite("alice.bin")) {
      BinaryFormatter formatter = new BinaryFormatter();

      formatter.Serialize(stream, alice);
    }

    // デシリアライズ
    using (Stream stream = File.OpenRead("alice.bin")) {
      BinaryFormatter formatter = new BinaryFormatter();

      Account deserialized = (Account)formatter.Deserialize(stream);

      Console.WriteLine(deserialized);
    }
  }
}
Imports System
Imports System.IO
Imports System.Runtime.Serialization
Imports System.Runtime.Serialization.Formatters.Binary

<Serializable> _
Class Account
  Public ID As Integer
  Public Name As String

  <NonSerialized> _
  Public LastLogin As DateTime

  Public Overrides Function ToString() As String
    Return String.Format("{0}:{1} LastLogin={2}", ID, Name, LastLogin)
  End Function
End Class

Class Sample
  Shared Sub Main()
    Dim alice As New Account()

    alice.ID = 2
    alice.Name = "Alice"
    alice.LastLogin = New DateTime(2010, 10, 20)

    Console.WriteLine(alice)

    ' シリアライズ
    Using stream As Stream = File.OpenWrite("alice.bin")
      Dim formatter As New BinaryFormatter()

      formatter.Serialize(stream, alice)
    End Using

    ' デシリアライズ
    Using stream As Stream = File.OpenRead("alice.bin")
      Dim formatter As New BinaryFormatter()

      Dim deserialized As Account = CType(formatter.Deserialize(stream), Account)

      Console.WriteLine(deserialized)
    End Using
  End Sub
End Class
実行結果
2:Alice LastLogin=2010/10/20 0:00:00
2:Alice LastLogin=0001/01/01 0:00:00

追加されたフィールドの無視 (OptionalFieldAttribute)

OptionalFieldAttributeは新しいバージョンで追加されるフィールドに付与します。 例えば、あるバージョンでの型をシリアライズした後、型にフィールドを追加した新しいバージョンでデシリアライズしようとすると、古いバージョンのストリームには追加した新しいフィールドが含まれていないため例外がスローされます。 このような場合に例外をスローしないよう、追加したフィールドにはOptionalFieldAttributeを付与しておく必要があります。

例として、SoapFormatterを使って、二つのバージョンを跨いだシリアライズ・デシリアライズを行ってみます。 まずは、バージョン1のコードを使ってシリアライズを行います。

using System;
using System.IO;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Soap;

// バージョン1のAccountクラス
[Serializable]
class Account {
  public int ID;
  public string Name;

  public override string ToString()
  {
    return string.Format("{0}:{1}", ID, Name);
  }
}

class Sample {
  static void Main()
  {
    Account alice = new Account();

    alice.ID = 2;
    alice.Name = "Alice";

    Console.WriteLine(alice);

    // シリアライズ
    using (Stream stream = File.OpenWrite("alice.soap.xml")) {
      SoapFormatter formatter = new SoapFormatter();

      formatter.Serialize(stream, alice);
    }
  }
}
Imports System
Imports System.IO
Imports System.Runtime.Serialization
Imports System.Runtime.Serialization.Formatters.Soap

' バージョン1のAccountクラス
<Serializable> _
Class Account
  Public ID As Integer
  Public Name As String

  Public Overrides Function ToString() As String
    Return String.Format("{0}:{1}", ID, Name)
  End Function
End Class

Class Sample
  Shared Sub Main()
    Dim alice As New Account()

    alice.ID = 2
    alice.Name = "Alice"

    Console.WriteLine(alice)

    ' シリアライズ
    Using stream As Stream = File.OpenWrite("alice.soap.xml")
      Dim formatter As New SoapFormatter()

      formatter.Serialize(stream, alice)
    End Using
  End Sub
End Class

バージョン1のAccountクラスには二つのフィールドIDとNameがあります。 このコードをビルドして実行ファイルser.exeを作成、実行します。 シリアライズされたファイルalice.soap.xmlの内容は次のようになります。

alice.soap.xml
<SOAP-ENV:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/" xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:clr="http://schemas.microsoft.com/soap/encoding/clr/1.0" SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
<SOAP-ENV:Body>
<a1:Account id="ref-1" xmlns:a1="http://schemas.microsoft.com/clr/assem/ser%2C%20Version%3D0.0.0.0%2C%20Culture%3Dneutral%2C%20PublicKeyToken%3Dnull">
<ID>2</ID>
<Name id="ref-3">Alice</Name>
</a1:Account>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>

続いて、先ほどのコードを書き換え、バージョン2のコードを使ってデシリアライズを行います。

using System;
using System.IO;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Soap;

// バージョン2のAccountクラス
[Serializable]
class Account {
  public int ID;
  public string Name;

  // バージョン2で新しく追加したフィールド
  [OptionalField]
  public string ContactAddress;

  public override string ToString()
  {
    return string.Format("{0}:{1} ContactAddress={2}", ID, Name, ContactAddress);
  }
}

class Sample {
  static void Main()
  {
    // デシリアライズ
    using (Stream stream = File.OpenRead("alice.soap.xml")) {
      SoapFormatter formatter = new SoapFormatter();

      Account deserialized = (Account)formatter.Deserialize(stream);

      Console.WriteLine(deserialized);
    }
  }
}
Imports System
Imports System.IO
Imports System.Runtime.Serialization
Imports System.Runtime.Serialization.Formatters.Soap

' バージョン2のAccountクラス
<Serializable> _
Class Account
  Public ID As Integer
  Public Name As String

  ' バージョン2で新しく追加したフィールド
  <OptionalField> _
  Public ContactAddress As String

  Public Overrides Function ToString() As String
    Return String.Format("{0}:{1} ContactAddress={2}", ID, Name, ContactAddress)
  End Function
End Class

Class Sample
  Shared Sub Main()
    ' デシリアライズ
    Using stream As Stream = File.OpenRead("alice.soap.xml")
      Dim formatter As New SoapFormatter()

      Dim deserialized As Account = CType(formatter.Deserialize(stream), Account)

      Console.WriteLine(deserialized)
    End Using
  End Sub
End Class

バージョン2のAccountクラスには、バージョン1から存在するフィールドID・Nameと、バージョン2で新しく追加したフィールドContactAddressがあります。 このコードをビルドして実行ファイルser.exeを作成、実行すると、特に問題なくデシリアライズ出来ます。

実行結果
2:Alice ContactAddress=

しかし、これをOptionalFieldAttributeを付与していない状態で実行すると、以下のようにSerializationExceptionがスローされます。

OptionalFieldAttributeを付与していない状態で実行した場合
ハンドルされていない例外: System.Runtime.Serialization.SerializationException:
メンバの数が間違っています。オブジェクト Account には 3 メンバ含まれていますが、逆シリアル化されたメンバの数は 2 です。
   場所 System.Runtime.Serialization.Formatters.Soap.ReadObjectInfo.PopulateObjectMembers()
   場所 System.Runtime.Serialization.Formatters.Soap.ObjectReader.ParseObjectEnd(ParseRecord pr)
   場所 System.Runtime.Serialization.Formatters.Soap.ObjectReader.Parse(ParseRecord pr)
   場所 System.Runtime.Serialization.Formatters.Soap.SoapHandler.EndElement(String prefix, String name, String urn)
   場所 System.Runtime.Serialization.Formatters.Soap.SoapParser.ParseXml()
   場所 System.Runtime.Serialization.Formatters.Soap.SoapParser.Run()
   場所 System.Runtime.Serialization.Formatters.Soap.ObjectReader.Deserialize(HeaderHandler handler, ISerParser serParser)
   場所 System.Runtime.Serialization.Formatters.Soap.SoapFormatter.Deserialize(Stream serializationStream, HeaderHandler handler)
   場所 System.Runtime.Serialization.Formatters.Soap.SoapFormatter.Deserialize(Stream serializationStream)
   場所 Sample.Main()

なお、BinaryFormatterを使った場合はOptionalFieldAttributeが付与されていなくても特に例外はスローされないようですが、念のため付与しておいたほうがよいと思われます。

シリアライズ前後のコールバック (OnSerializingAttribute, OnSerializedAttribute, OnDeserializingAttribute, OnDeserializedAttribute)

シリアライズ・デシリアライズの前後で値の検証やデフォルト値の設定などの追加の処理を行いたい場合は、次の属性を使用することでシリアライザにコールバックさせることが出来ます。 これらの属性はフィールドではなくコールバック用のメソッドに付与します。

コールバックを指定するための属性
属性 機能
OnSerializingAttribute シリアライズ前にコールバックするメソッドを指定する
OnSerializedAttribute シリアライズ後にコールバックするメソッドを指定する
OnDeserializingAttribute デシリアライズ前にコールバックするメソッドを指定する
OnDeserializedAttribute デシリアライズ後にコールバックするメソッドを指定する

なお、これらの属性を付与するメソッドは、StreamingContextを引数にとる必要があります。

using System;
using System.IO;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;

[Serializable]
class Account {
  public int ID;
  public string Name;

  public override string ToString()
  {
    return string.Format("{0}:{1}", ID, Name);
  }

  [OnSerializing]
  private void OnSerializing(StreamingContext context)
  {
    Console.WriteLine("OnSerializing");
  }

  [OnSerialized]
  private void OnSerialized(StreamingContext context)
  {
    Console.WriteLine("OnSerialized");
  }

  [OnDeserializing]
  private void OnDeserializing(StreamingContext context)
  {
    Console.WriteLine("OnDeserializing");
  }

  [OnDeserialized]
  private void OnDeserialized(StreamingContext context)
  {
    Console.WriteLine("OnDeserialized");
  }
}

class Sample {
  static void Main()
  {
    Account alice = new Account();

    alice.ID = 2;
    alice.Name = "Alice";

    Console.WriteLine(alice);

    using (Stream stream = File.OpenWrite("alice.bin")) {
      BinaryFormatter formatter = new BinaryFormatter();

      Console.WriteLine("Serialize");

      formatter.Serialize(stream, alice);
    }

    using (Stream stream = File.OpenRead("alice.bin")) {
      BinaryFormatter formatter = new BinaryFormatter();

      Console.WriteLine("Deserialize");

      Account deserialized = (Account)formatter.Deserialize(stream);

      Console.WriteLine(deserialized);
    }
  }
}
Imports System
Imports System.IO
Imports System.Runtime.Serialization
Imports System.Runtime.Serialization.Formatters.Binary

<Serializable> _
Class Account
  Public ID As Integer
  Public Name As String

  Public Overrides Function ToString() As String
    Return String.Format("{0}:{1}", ID, Name)
  End Function

  <OnSerializing> _
  Private Sub OnSerializing(ByVal context As StreamingContext)
    Console.WriteLine("OnSerializing")
  End Sub

  <OnSerialized> _
  Private Sub OnSerialized(ByVal context As StreamingContext)
    Console.WriteLine("OnSerialized")
  End Sub

  <OnDeserializing> _
  Private Sub OnDeserializing(ByVal context As StreamingContext)
    Console.WriteLine("OnDeserializing")
  End Sub

  <OnDeserialized> _
  Private Sub OnDeserialized(ByVal context As StreamingContext)
    Console.WriteLine("OnDeserialized")
  End Sub
End Class

Class Sample
  Shared Sub Main()
    Dim alice As New Account()

    alice.ID = 2
    alice.Name = "Alice"

    Console.WriteLine(alice)

    Using stream As Stream = File.OpenWrite("alice.bin")
      Dim formatter As New BinaryFormatter()

      Console.WriteLine("Serialize")

      formatter.Serialize(stream, alice)
    End Using

    Using stream As Stream = File.OpenRead("alice.bin")
      Dim formatter As New BinaryFormatter()

      Console.WriteLine("Deserialize")

      Dim deserialized As Account = CType(formatter.Deserialize(stream), Account)

      Console.WriteLine(deserialized)
    End Using
  End Sub
End Class
実行結果
2:Alice
Serialize
OnSerializing
OnSerialized
Deserialize
OnDeserializing
OnDeserialized
2:Alice

シリアライズ動作の制御 (ISerializable)

ISerializableインターフェイスを使うことで、ここまでに解説してきた属性を用いるよるも柔軟にシリアライズ・デシリアライズ時の動作を制御することが出来ます。 例えば、シリアライズする値とその名前を指定したり、シリアライズ・デシリアライズの前後でデフォルト値の設定や値のチェックの処理を追加することも出来ます。

ISerializableを実装する場合は、シリアライズを行うためのメソッドISerializable.GetObjectDataとデシリアライズを行うためのコンストラクタを用意する必要があります。 これらはどちらもパブリックである必要はありません。

このメソッドとコンストラクタでは、引数の一つにシリアライズ・デシリアライズする値を格納するためのSerializationInfoを取ります。 シリアライズする値を追加するにはAddValueメソッド、デシリアライズされた値を取得するにはGetStringメソッドGetValueメソッドなどを使います。 この時、値とその名前を指定出来ますが、名前にはフィールド名ではなく任意の名前を代わりに指定することも出来ます。

次の例は、ISerializableを使ってシリアライズ・デシリアライズを行う例です。

using System;
using System.IO;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Soap;

[Serializable]
class Account : ISerializable {
  public int ID;
  public string Name;
  public Uri[] ContactAddresses;
  public DateTime LastLogin;

  public Account(int id, string name)
  {
    this.ID = id;
    this.Name = name;
  }

  // デシリアライズの際に呼び出されるコンストラクタ
  private Account(SerializationInfo info, StreamingContext context)
  {
    this.ID = info.GetInt32("ID");
    this.Name = info.GetString("名前");
    this.ContactAddresses = (Uri[])info.GetValue("連絡先", typeof(Uri[]));

    this.LastLogin = new DateTime(2000, 1, 1);
  }

  // ISerializable.GetObjectDataの明示的な実装
  void ISerializable.GetObjectData(SerializationInfo info, StreamingContext context)
  {
    info.AddValue("ID", ID);
    info.AddValue("名前", Name);
    info.AddValue("連絡先", ContactAddresses, typeof(Uri[]));
  }

  public override string ToString()
  {
    return string.Format("{0}:{1} LastLogin={2} ContactAddresses={3}",
                         ID,
                         Name,
                         LastLogin,
                         string.Join(", ", Array.ConvertAll<Uri, string>(ContactAddresses, Convert.ToString)));
  }
}

class Sample {
  static void Main()
  {
    Account alice = new Account(2, "Alice");

    alice.ContactAddresses = new Uri[] {
      new Uri("mailto:alice@mail.example.net"),
      new Uri("http://example.com/~alice/")
    };
    alice.LastLogin = new DateTime(2010, 10, 20);

    Console.WriteLine(alice);

    // シリアライズ
    using (Stream stream = File.OpenWrite("alice.soap.xml")) {
      SoapFormatter formatter = new SoapFormatter();

      formatter.Serialize(stream, alice);
    }

    // デシリアライズ
    using (Stream stream = File.OpenRead("alice.soap.xml")) {
      SoapFormatter formatter = new SoapFormatter();

      Account deserialized = (Account)formatter.Deserialize(stream);

      Console.WriteLine(deserialized);
    }
  }
}
Imports System
Imports System.IO
Imports System.Runtime.Serialization
Imports System.Runtime.Serialization.Formatters.Soap

<Serializable> _
Class Account
  Implements ISerializable

  Public ID As Integer
  Public Name As String
  Public ContactAddresses() As Uri
  Public LastLogin As DateTime

  Public Sub New(ByVal id As Integer, ByVal name As String)
    MyClass.ID = id
    MyClass.Name = name
  End SUb

  ' デシリアライズの際に呼び出されるコンストラクタ
  Private Sub New(ByVal info As SerializationInfo, ByVal context As StreamingContext)
    MyClass.ID = info.GetInt32("ID")
    MyClass.Name = info.GetString("名前")
    MyClass.ContactAddresses = CType(info.GetValue("連絡先", GetType(Uri())), Uri())

    MyClass.LastLogin = new DateTime(2000, 1, 1)
  End SUb

  ' ISerializable.GetObjectDataの実装
  Private Sub GetObjectData(ByVal info As SerializationInfo, ByVal context As StreamingContext) Implements ISerializable.GetObjectData
    info.AddValue("ID", ID)
    info.AddValue("名前", Name)
    info.AddValue("連絡先", ContactAddresses, GetType(Uri()))
  End Sub

  Public Overrides Function ToString() As String
    Return String.Format("{0}:{1} LastLogin={2} ContactAddresses={3}", _
                         ID, _
                         Name, _
                         LastLogin, _
                         String.Join(", ", Array.ConvertAll(ContactAddresses, AddressOf Convert.ToString)))
  End Function
End Class

Class Sample
  Shared Sub Main()
    Dim alice As New Account(2, "Alice")

    alice.ContactAddresses = New Uri() { _
      New Uri("mailto:alice@mail.example.net"), _
      New Uri("http://example.com/~alice/") _
    }
    alice.LastLogin = New DateTime(2010, 10, 20)

    Console.WriteLine(alice)

    ' シリアライズ
    Using stream As Stream = File.OpenWrite("alice.soap.xml")
      Dim formatter As New SoapFormatter()

      formatter.Serialize(stream, alice)
    End Using

    ' デシリアライズ
    Using stream As Stream = File.OpenRead("alice.soap.xml")
      Dim formatter As New SoapFormatter()

      Dim deserialized As Account = CType(formatter.Deserialize(stream), Account)

      Console.WriteLine(deserialized)
    End Using
  End Sub
End Class
実行結果
2:Alice LastLogin=2010/10/20 0:00:00 ContactAddresses=mailto:alice@mail.example.net, http://example.com/~alice/
2:Alice LastLogin=2000/01/01 0:00:00 ContactAddresses=mailto:alice@mail.example.net, http://example.com/~alice/

結果を見てのとおり、適切にデシリアライズされていることが分かると思います。 なお、SerializationInfo.AddValueで指定した値の名前は、値を格納するXML要素の名前としても使われます。 以下はシリアライズされたファイルalice.soap.xmlの内容です。 Account.LastLoginフィールドの値はSerializationInfoに追加していないため、シリアライズされた内容には含まれていない点にも注目してください。

alice.soap.xml
<SOAP-ENV:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/" xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:clr="http://schemas.microsoft.com/soap/encoding/clr/1.0" SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
<SOAP-ENV:Body>
<a1:Account id="ref-1" xmlns:a1="http://schemas.microsoft.com/clr/assem/ser%2C%20Version%3D0.0.0.0%2C%20Culture%3Dneutral%2C%20PublicKeyToken%3Dnull">
<ID>2</ID>
<名前 id="ref-4">Alice</名前>
<連絡先 href="#ref-5"/>
</a1:Account>
<SOAP-ENC:Array id="ref-5" SOAP-ENC:arrayType="a2:Uri[2]" xmlns:a2="http://schemas.microsoft.com/clr/nsassem/System/System%2C%20Version%3D2.0.0.0%2C%20Culture%3Dneutral%2C%20PublicKeyToken%3Db77a5c561934e089">
<item href="#ref-6"/>
<item href="#ref-7"/>
</SOAP-ENC:Array>
<a2:Uri id="ref-6" xmlns:a2="http://schemas.microsoft.com/clr/nsassem/System/System%2C%20Version%3D2.0.0.0%2C%20Culture%3Dneutral%2C%20PublicKeyToken%3Db77a5c561934e089">
<AbsoluteUri id="ref-8">mailto:alice@mail.example.net</AbsoluteUri>
</a2:Uri>
<a2:Uri id="ref-7" xmlns:a2="http://schemas.microsoft.com/clr/nsassem/System/System%2C%20Version%3D2.0.0.0%2C%20Culture%3Dneutral%2C%20PublicKeyToken%3Db77a5c561934e089">
<AbsoluteUri id="ref-9">http://example.com/~alice/</AbsoluteUri>
</a2:Uri>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>

ISerializableインターフェイスを実装するクラスを継承可能にするには、コンストラクタとメソッドを派生クラスに公開し、オーバーライドできるようにしておく必要があります。 派生クラスでは、SerializationInfoを基底クラスに引き渡すことで基底クラスのフィールドをシリアライズ・デシリアライズさせるようにします。 以下の例では、基底クラスでISerializableを実装し、その派生クラスをシリアライズしています。

using System;
using System.IO;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Soap;

[Serializable]
class AccountBase : ISerializable {
  protected int ID;
  protected string Name;

  protected AccountBase(int id, string name)
  {
    this.ID = id;
    this.Name = name;
  }

  // デシリアライズの際に呼び出されるコンストラクタ
  protected AccountBase(SerializationInfo info, StreamingContext context)
  {
    this.ID = info.GetInt32("ID");
    this.Name = info.GetString("名前");
  }

  // ISerializable.GetObjectDataの明示的な実装
  void ISerializable.GetObjectData(SerializationInfo info, StreamingContext context)
  {
    // オーバーライド可能なメソッドを呼び出す
    GetObjectData(info, context);
  }

  // シリアライズする値を追加する
  protected virtual void GetObjectData(SerializationInfo info, StreamingContext context)
  {
    info.AddValue("ID", ID);
    info.AddValue("名前", Name);
  }
}

[Serializable]
class Account : AccountBase {
  public Uri[] ContactAddresses;

  public Account(int id, string name)
    : base(id, name)
  {
  }

  // デシリアライズの際に呼び出されるコンストラクタ
  protected Account(SerializationInfo info, StreamingContext context)
    : base(info, context)
  {
    this.ContactAddresses = (Uri[])info.GetValue("連絡先", typeof(Uri[]));
  }

  // GetObjectDataをオーバーライド
  protected override void GetObjectData(SerializationInfo info, StreamingContext context)
  {
    // 基底クラスのシリアライズ情報を取得
    base.GetObjectData(info, context);

    info.AddValue("連絡先", ContactAddresses, typeof(Uri[]));
  }

  public override string ToString()
  {
    return string.Format("{0}:{1} ContactAddresses={2}",
                         ID,
                         Name,
                         string.Join(", ", Array.ConvertAll<Uri, string>(ContactAddresses, Convert.ToString)));
  }
}

class Sample {
  static void Main()
  {
    Account alice = new Account(2, "Alice");

    alice.ContactAddresses = new Uri[] {
      new Uri("mailto:alice@mail.example.net"),
      new Uri("http://example.com/~alice/")
    };

    Console.WriteLine(alice);

    // シリアライズ
    using (Stream stream = File.OpenWrite("alice.soap.xml")) {
      SoapFormatter formatter = new SoapFormatter();

      formatter.Serialize(stream, alice);
    }

    // デシリアライズ
    using (Stream stream = File.OpenRead("alice.soap.xml")) {
      SoapFormatter formatter = new SoapFormatter();

      Account deserialized = (Account)formatter.Deserialize(stream);

      Console.WriteLine(deserialized);
    }
  }
}
Imports System
Imports System.IO
Imports System.Runtime.Serialization
Imports System.Runtime.Serialization.Formatters.Soap

<Serializable> _
Class AccountBase
  Implements ISerializable

  Protected ID As Integer
  Protected Name As String

  Protected Sub New(ByVal id As Integer, ByVal name As String)
    MyClass.ID = id
    MyClass.Name = name
  End SUb

  ' デシリアライズの際に呼び出されるコンストラクタ
  Protected Sub New(ByVal info As SerializationInfo, ByVal context As StreamingContext)
    MyClass.ID = info.GetInt32("ID")
    MyClass.Name = info.GetString("名前")
  End SUb

  ' ISerializable.GetObjectDataの実装
  Protected Overridable Sub GetObjectData(ByVal info As SerializationInfo, ByVal context As StreamingContext) Implements ISerializable.GetObjectData
    ' シリアライズする値を追加する
    info.AddValue("ID", ID)
    info.AddValue("名前", Name)
  End Sub
End Class

<Serializable> _
Class Account
  Inherits AccountBase

  Public ContactAddresses() As Uri

  Public Sub New(ByVal id As Integer, ByVal name As String)
    MyBase.New(id, name)
  End SUb

  ' デシリアライズの際に呼び出されるコンストラクタ
  Protected Sub New(ByVal info As SerializationInfo, ByVal context As StreamingContext)
    MyBase.New(info, context)

    MyClass.ContactAddresses = CType(info.GetValue("連絡先", GetType(Uri())), Uri())
  End SUb

  ' GetObjectDataをオーバーライド
  Protected Overrides Sub GetObjectData(ByVal info As SerializationInfo, ByVal context As StreamingContext)
    ' 基底クラスのシリアライズ情報を取得
    MyBase.GetObjectData(info, context)

    info.AddValue("連絡先", ContactAddresses, GetType(Uri()))
  End Sub

  Public Overrides Function ToString() As String
    Return String.Format("{0}:{1} ContactAddresses={2}", _
                         ID, _
                         Name, _
                         String.Join(", ", Array.ConvertAll(ContactAddresses, AddressOf Convert.ToString)))
  End Function
End Class

Class Sample
  Shared Sub Main()
    Dim alice As New Account(2, "Alice")

    alice.ContactAddresses = New Uri() { _
      New Uri("mailto:alice@mail.example.net"), _
      New Uri("http://example.com/~alice/") _
    }

    Console.WriteLine(alice)

    ' シリアライズ
    Using stream As Stream = File.OpenWrite("alice.soap.xml")
      Dim formatter As New SoapFormatter()

      formatter.Serialize(stream, alice)
    End Using

    ' デシリアライズ
    Using stream As Stream = File.OpenRead("alice.soap.xml")
      Dim formatter As New SoapFormatter()

      Dim deserialized As Account = CType(formatter.Deserialize(stream), Account)

      Console.WriteLine(deserialized)
    End Using
  End Sub
End Class
実行結果
2:Alice ContactAddresses=mailto:alice@mail.example.net, http://example.com/~alice/
2:Alice ContactAddresses=mailto:alice@mail.example.net, http://example.com/~alice/

デシリアライズ後のコールバック (IDeserializationCallback)

IDeserializationCallbackインターフェイスもデシリアライズが完了した時点で呼び出されるコールバックメソッドを実装するためのものです。 OnDeserializedAttributeと機能と動作は同じです。 引数としてStreamingContextの代わりにobjectをとりますが、有効な値は渡されないようです。

using System;
using System.IO;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;

[Serializable]
class Account : IDeserializationCallback {
  public int ID;
  public string Name;

  [NonSerialized]
  private bool deserialized = false;

  public override string ToString()
  {
    return string.Format("{0}:{1} {2}", ID, Name, deserialized ? "(deserialized)" : string.Empty);
  }

  void IDeserializationCallback.OnDeserialization(object sender)
  {
    deserialized = true;

    Console.WriteLine("OnDeserialization: sender={0}", sender);
  }
}

class Sample {
  static void Main()
  {
    Account alice = new Account();

    alice.ID = 2;
    alice.Name = "Alice";

    Console.WriteLine(alice);

    using (Stream stream = File.OpenWrite("alice.bin")) {
      BinaryFormatter formatter = new BinaryFormatter();

      Console.WriteLine("Serialize");

      formatter.Serialize(stream, alice);
    }

    using (Stream stream = File.OpenRead("alice.bin")) {
      BinaryFormatter formatter = new BinaryFormatter();

      Console.WriteLine("Deserialize");

      Account deserialized = (Account)formatter.Deserialize(stream);

      Console.WriteLine(deserialized);
    }
  }
}
Imports System
Imports System.IO
Imports System.Runtime.Serialization
Imports System.Runtime.Serialization.Formatters.Binary

<Serializable> _
Class Account
  Implements IDeserializationCallback

  Public ID As Integer
  Public Name As String

  <NonSerialized> _
  Private deserialized As Boolean = False

  Public Overrides Function ToString() As String
    Return String.Format("{0}:{1} {2}", ID, Name, IIf(deserialized, "(deserialized)", String.Empty))
  End Function

  Private Sub OnDeserialization(ByVal sender As Object) Implements IDeserializationCallback.OnDeserialization
    deserialized = True

    Console.WriteLine("OnDeserialization: sender={0}", sender)
  End Sub
End Class

Class Sample
  Shared Sub Main()
    Dim alice As New Account()

    alice.ID = 2
    alice.Name = "Alice"

    Console.WriteLine(alice)

    Using stream As Stream = File.OpenWrite("alice.bin")
      Dim formatter As New BinaryFormatter()

      Console.WriteLine("Serialize")

      formatter.Serialize(stream, alice)
    End Using

    Using stream As Stream = File.OpenRead("alice.bin")
      Dim formatter As New BinaryFormatter()

      Console.WriteLine("Deserialize")

      Dim deserialized As Account = CType(formatter.Deserialize(stream), Account)

      Console.WriteLine(deserialized)
    End Using
  End Sub
End Class
実行結果
2:Alice
Serialize
Deserialize
OnDeserialization: sender=
2:Alice (deserialized)