いろいろ備忘録日記

主に .NET とか Go とか Flutter とか Python絡みのメモを公開しています。

シリアライズ不可なオブジェクトをシリアライズ/デシリアライズする. (シリアル化サロゲート, ISerializationSurrogate, SurrogateSelector, SerializationInfo, StreamingContext)


知ってると、ちょっと便利かもしれないTips。メモメモ。


通常、Serializable属性が付与されていないクラスはシリアライズすることが出来ません。
でも、.NETにはシリアル化サロゲートという機能がありまして、これを利用するとSerializable属性が
付与されていないクラスでもシリアライズ/デシリアライズが可能になります。


シリアル化サロゲートを利用するには、以下のインターフェースを実装したクラスを作成する必要があります。

System.Runtime.Serialization.ISerializationSurrogate


インターフェースに定義されているメソッドは以下の2つです。

void GetObjectData(
	object obj,
	SerializationInfo info,
	StreamingContext context
)

object SetObjectData(
	object obj,
	SerializationInfo info,
	StreamingContext context,
	ISurrogateSelector selector
)


GetObjectDataメソッドがシリアライズする際に呼び出されるメソッドで
SetObjectDataメソッドがデシリアライズする際に呼び出されるメソッドとなります。


実装クラスを作成したら、後はBinaryFormatterなどのシリアライザに
設定します。設定方法は以下のようになります。

// Formaterを作成
var formatter = new BinaryFormatter();
// Selectorを作成
var selector  = new SurrogateSelector();
// Surrogateを作成
var surrogate = new XXSerializationSurrogate();

// まず、SelectorにISerializationSurrogateを設定
selector.AddSurrogate(surrogate)

// FormatterにSelectorを設定.
formatter.SurrogateSelector = selector;

以下、サンプルです。
サンプルでは、最初にシリアライズ可能なオブジェクトを普通にシリアライズ/デシリアライズ
次に、シリアライズ不可なオブジェクトをシリアライズ/デシリアライズ
最後に、シリアル化サロゲートを利用して2番目のオブジェクトをシリアライズ/デシリアライズしています。

  #region SerializationSurrogateSamples-01
  /// <summary>
  /// シリアライズに関するサンプルです。
  /// </summary>
  /// <remarks>
  /// シリアル化サロゲートについて。 (ISerializationSurrogate)
  /// </remarks>
  public class SerializationSurrogateSamples01 : IExecutable
  {
    public void Execute()
    {
      //
      // 普通のシリアライズ処理.
      //
      var obj = MakeSerializableObject();
      using (var stream = new MemoryStream())
      {
        var formatter = new BinaryFormatter();
        
        // 成功する.
        formatter.Serialize(stream, obj);
        
        stream.Position = 0;
        Console.WriteLine(formatter.Deserialize(stream));
      }
      
      //
      // シリアライズ不可 (Serializable属性をつけていない)
      //
      var obj2 = MakeNotSerializableObject();
      using (var stream = new MemoryStream())
      {
        var formatter = new BinaryFormatter();
        
        try
        {
          // 対象クラスにSerializable属性が付与されていないので
          // 以下を実行すると例外が発生する.
          formatter.Serialize(stream, obj2);
          
          stream.Position = 0;
          Console.WriteLine(formatter.Deserialize(stream));
        }
        catch (SerializationException ex)
        {
          Console.WriteLine("[ERROR]: {0}", ex.Message);
        }
      }
      
      //
      // シリアル化サロゲート. (ISerializationSurrogate)
      //
      var obj3 = MakeNotSerializableObject();
      using (var stream = new MemoryStream())
      {
        var formatter = new BinaryFormatter();
        
        //
        // シリアル化サロゲートを行うために、以下の手順で設定を行う.
        //
        // 1.SurrogateSelectorオブジェクトを用意.
        // 2.自作Surrogateクラスを用意.
        // 3.SurrogateSelector.AddSurrogateでSurrogateオブジェクトを設定
        // 4.SurrogateSelectorをFormatterに設定.
        //
        // これにより、シリアライズ不可なオブジェクトをFormatterにてシリアライズ/デシリアライズ
        // する際にシリアル化サロゲートが行われるようになる。
        //
        var selector  = new SurrogateSelector();
        var surrogate = new CanNotSerializeSurrogate();
        var context   = new StreamingContext(StreamingContextStates.All);
        
        selector.AddSurrogate(typeof(CanNotSerialize), context, surrogate);
        
        formatter.SurrogateSelector = selector;
        
        try
        {
          // 通常、以下を実行すると例外が発生するが
          // シリアル化サロゲートを行うので、エラーとならずシリアライズが成功する.
          formatter.Serialize(stream, obj3);
          
          stream.Position = 0;
          Console.WriteLine(formatter.Deserialize(stream));
        }
        catch (SerializationException ex)
        {
          Console.WriteLine("[ERROR]: {0}", ex.Message);
        }
      }
    }
    
    IHasNameAndAge MakeSerializableObject()
    {
      return new CanSerialize 
                 { 
                    Name = "hoge"
                   ,Age = 99 
                 };
    }
    
    IHasNameAndAge MakeNotSerializableObject()
    {
      return new CanNotSerialize
                 {
                    Name = "hehe"
                   ,Age = 98
                 };
    }
    
    #region SampleInterfaceAndClasses
    interface IHasNameAndAge
    {
      string Name { get; set; }
      int    Age  { get; set; }
    }
    
    // シリアライズ可能なクラス
    [Serializable]
    class CanSerialize : IHasNameAndAge
    {
      string _name;
      int    _age;
      
      public string Name
      {
        get { return _name; }
        set { _name = value; }
      }
      
      public int Age 
      {
        get { return _age; }
        set { _age = value; }
      }
      
      public override string ToString() 
      { 
        return string.Format("[CanSerialize] Name={0}, Age={1}", Name, Age); 
      }
    }
    
    // シリアライズ不可なクラス
    class CanNotSerialize : IHasNameAndAge
    {
      string _name;
      int    _age;
      
      public string Name
      {
        get { return _name; }
        set { _name = value; }
      }
      
      public int Age 
      {
        get { return _age; }
        set { _age = value; }
      }
      
      public override string ToString() 
      { 
        return string.Format("[CanNotSerialize] Name={0}, Age={1}", Name, Age); 
      }
    }
    
    // CanNotSerializeクラスのためのサロゲートクラス.
    class CanNotSerializeSurrogate : ISerializationSurrogate
    {
      // シリアライズ時に呼び出されるメソッド
      public void GetObjectData(object obj, SerializationInfo info, StreamingContext context)
      {
        CanNotSerialize targetObj = obj as CanNotSerialize;
        
        //
        // シリアライズする項目と値を以下のようにinfoに設定していく.
        //
        info.AddValue("Name", targetObj.Name);
        info.AddValue("Age",  targetObj.Age);
      }
      
      // デシリアライズ時に呼び出されるメソッド.
      public object SetObjectData(object obj, SerializationInfo info, StreamingContext context, ISurrogateSelector selector)
      {
        CanNotSerialize targetObj = obj as CanNotSerialize;
        
        //
        // infoから値を取得し、対象となるオブジェクトに設定.
        //
        targetObj.Name = info.GetString("Name");
        targetObj.Age  = info.GetInt32("Age");
        
        // Formatterは, この戻り値を無視するので戻り値はnullで良い.
        return null;
      }
    }
    #endregion
  }
  #endregion


実行結果は、以下のようになります。

  [CanSerialize] Name=hoge, Age=99
  [ERROR]: アセンブリ 'MySamples, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null' の型 'SerializationSurrogateSamples01+CanNotSerialize' はシリアル化可能として設定されていません。
  [CanNotSerialize] Name=hehe, Age=98


3つめの実行では、ちゃんとシリアライズ/デシリアライズされています。


以下、参考情報です。

================================
過去の記事については、以下のページからご参照下さい。