DOBON.NET プログラミング道: .NET Framework, VB.NET, C#, Visual Basic, Visual Studio, インストーラ, ...

DataContractSerializerで、上位下位互換性を保ってシリアル化、逆シリアル化できるようにする

DataContractSerializerを使って、オブジェクトのXMLシリアル化、逆シリアル化を行う」では、DataContractSerializerを使ってオブジェクトをXMLファイルに保存、復元する方法を紹介しました。例えばこの方法でアプリケーションの設定を保存する場合、各々の設定をプロパティとして持っているクラス(以下、「設定クラス」と呼ぶ)を作成して、そのインスタンスをシリアル化することになるでしょう。アプリケーションのバージョンが上がって設定が増えても(つまり、設定クラスのプロパティが増えても)、正常に保存、復元ができるでしょうか?

バージョンが上がって設定が増えた時、新しいバージョンで古い設定を読み込むと、新たに加えられたプロパティはその型の初期値になります。また、古いバージョンで新しい設定を読み込むと、新たに加えられたプロパティは無視されます。そのため、新たに設定が追加されても大きな問題はありません。

しかし、新しいバージョンの設定を古いバージョンのアプリケーションで読み込み、古いアプリケーションで設定を保存すると、新しいバージョンで追加された設定は消えてしまいます。これはごくあたり前のことですが、このあたり前のことを防ぐ方法があります。その方法は、設定クラスにIExtensibleDataObjectインターフェイスを実装し、ラウンドトリップを有効にすることです。

IExtensibleDataObjectインターフェイスを実装するのは簡単です。以下のように、ExtensionDataObject型のExtensionDataプロパティを作成するだけです。

VB.NET
コードを隠すコードを選択
Imports System.Runtime.Serialization

'IExtensibleDataObjectを実装したクラス
<DataContract()> _
Public Class SampleSettings
    Implements IExtensibleDataObject

    'シリアル化するメンバ
    <DataMember()> _
    Public Message As String

    Private _extensionData As ExtensionDataObject
    Public Property ExtensionData1 As ExtensionDataObject _
        Implements IExtensibleDataObject.ExtensionData
        Get
            Return _extensionData
        End Get
        Set(ByVal value As ExtensionDataObject)
            _extensionData = value
        End Set
    End Property
End Class
C#
コードを隠すコードを選択
using System.Runtime.Serialization;

//IExtensibleDataObjectを実装したクラス
[DataContract]
public class SampleSettings : IExtensibleDataObject
{
    //シリアル化するメンバ
    [DataMember]
    public string Message;

    private ExtensionDataObject _extensionData;
    public ExtensionDataObject ExtensionData
    {
        get
        {
            return _extensionData;
        }
        set
        {
            _extensionData = value;
        }
    }
}

IExtensibleDataObjectを実装したクラスを使うとどうなるか、以下のサンプルで実際に試してみましょう。

VB.NET
コードを隠すコードを選択
Imports System.Xml
Imports System.Runtime.Serialization

'バージョン1の設定クラス
<DataContract(Name:="SampleSettings")> _
Public Class SampleSettingsV1
    Implements IExtensibleDataObject
    <DataMember> _
    Public Message As String

    Private _extensionData As ExtensionDataObject
    Public Property ExtensionData() As ExtensionDataObject _
        Implements IExtensibleDataObject.ExtensionData
        Get
            Return _extensionData
        End Get
        Set(value As ExtensionDataObject)
            _extensionData = value
        End Set
    End Property
End Class

'バージョン2の設定クラス
<DataContract(Name:="SampleSettings")> _
Public Class SampleSettingsV2
    Implements IExtensibleDataObject
    <DataMember> _
    Public Message As String
    '追加された設定
    <DataMember(Order:=2)> _
    Public ID As Integer

    Private _extensionData As ExtensionDataObject
    Public Property ExtensionData() As ExtensionDataObject _
        Implements IExtensibleDataObject.ExtensionData
        Get
            Return _extensionData
        End Get
        Set(value As ExtensionDataObject)
            _extensionData = value
        End Set
    End Property
End Class

Public Class Program
    ''' <summary>
    ''' 設定を保存する
    ''' </summary>
    ''' <typeparam name="T">設定クラスの型</typeparam>
    ''' <param name="fileName">保存先のファイル名</param>
    ''' <param name="settings">設定が格納されたオブジェクト</param>
    Public Shared Sub SaveSettings(Of T)(fileName As String, settings As T)
        Dim serializer As New DataContractSerializer(GetType(T))
        Dim setting As New XmlWriterSettings()
        setting.Encoding = New System.Text.UTF8Encoding()
        Using xw As XmlWriter = XmlWriter.Create(fileName, setting)
            serializer.WriteObject(xw, settings)
        End Using
    End Sub

    ''' <summary>
    ''' 設定を読み込む
    ''' </summary>
    ''' <typeparam name="T">設定クラスの型</typeparam>
    ''' <param name="fileName">読み込むファイル名</param>
    ''' <returns>設定を格納したオブジェクト</returns>
    Public Shared Function LoadSettings(Of T)(fileName As String) As T
        Dim serializer As New DataContractSerializer(GetType(T))
        Using xr As XmlReader = XmlReader.Create(fileName)
            Return DirectCast(serializer.ReadObject(xr), T)
        End Using
    End Function

    'エントリポイント
    Public Shared Sub Main(args As String())
        Dim fileNameV1 As String = "C:\test\V1.xml"
        Dim fileNameV2 As String = "C:\test\V2.xml"

        '保存する設定を作成する
        Dim settingsV2 As New SampleSettingsV2()
        settingsV2.ID = 2
        settingsV2.Message = "こんにちは。"

        'バージョン2の設定を書き込む
        SaveSettings(fileNameV2, settingsV2)

        'バージョン2の設定をバージョン1で読み込む
        Dim settingsV1 As SampleSettingsV1 = _
            LoadSettings(Of SampleSettingsV1)(fileNameV2)

        'バージョン1のまま書き込む
        SaveSettings(fileNameV1, settingsV1)

        'バージョン2でバージョン1の設定を読み込む
        Dim settingsV2_2 As SampleSettingsV2 = _
            LoadSettings(Of SampleSettingsV2)(fileNameV1)

        'バージョン2にしかないIDプロパティが復元されていることを確認する
        Console.WriteLine(settingsV2_2.ID)
    End Sub
End Class
C#
コードを隠すコードを選択
using System;
using System.Xml;
using System.Runtime.Serialization;

//バージョン1の設定クラス
[DataContract(Name = "SampleSettings")]
public class SampleSettingsV1 : IExtensibleDataObject
{
    [DataMember]
    public string Message;

    private ExtensionDataObject _extensionData;
    public ExtensionDataObject ExtensionData
    {
        get
        {
            return _extensionData;
        }
        set
        {
            _extensionData = value;
        }
    }
}

//バージョン2の設定クラス
[DataContract(Name = "SampleSettings")]
public class SampleSettingsV2 : IExtensibleDataObject
{
    [DataMember]
    public string Message;
    //追加された設定
    [DataMember(Order = 2)]
    public int ID;

    private ExtensionDataObject _extensionData;
    public ExtensionDataObject ExtensionData
    {
        get
        {
            return _extensionData;
        }
        set
        {
            _extensionData = value;
        }
    }
}

public class Program
{
    /// <summary>
    /// 設定を保存する
    /// </summary>
    /// <typeparam name="T">設定クラスの型</typeparam>
    /// <param name="fileName">保存先のファイル名</param>
    /// <param name="settings">設定が格納されたオブジェクト</param>
    public static void SaveSettings<T>(string fileName, T settings)
    {
        DataContractSerializer serializer =
            new DataContractSerializer(typeof(T));
        XmlWriterSettings setting = new XmlWriterSettings();
        setting.Encoding = new System.Text.UTF8Encoding();
        using (XmlWriter xw = XmlWriter.Create(fileName,setting))
        {
            serializer.WriteObject(xw, settings);
        }
    }

    /// <summary>
    /// 設定を読み込む
    /// </summary>
    /// <typeparam name="T">設定クラスの型</typeparam>
    /// <param name="fileName">読み込むファイル名</param>
    /// <returns>設定を格納したオブジェクト</returns>
    public static T LoadSettings<T>(string fileName)
    {
        DataContractSerializer serializer =
            new DataContractSerializer(typeof(T));
        using (XmlReader xr = XmlReader.Create(fileName))
        {
            return (T)serializer.ReadObject(xr);
        }
    }

    //エントリポイント
    public static void Main(string[] args)
    {
        string fileNameV1 = @"C:\test\V1.xml";
        string fileNameV2 = @"C:\test\V2.xml";

        //保存する設定を作成する
        SampleSettingsV2 settingsV2 = new SampleSettingsV2();
        settingsV2.ID = 2;
        settingsV2.Message = "こんにちは。";

        //バージョン2の設定を書き込む
        SaveSettings(fileNameV2, settingsV2);

        //バージョン2の設定をバージョン1で読み込む
        SampleSettingsV1 settingsV1 =
            LoadSettings<SampleSettingsV1>(fileNameV2);

        //バージョン1のまま書き込む
        SaveSettings(fileNameV1, settingsV1);

        //バージョン2でバージョン1の設定を読み込む
        SampleSettingsV2 settingsV2_2 =
            LoadSettings<SampleSettingsV2>(fileNameV1);

        //バージョン2にしかないIDプロパティが復元されていることを確認する
        Console.WriteLine(settingsV2_2.ID);
    }
}

このサンプルでは、バージョン1の設定クラスを想定した「SampleSettingsV1」と、バージョン2の設定クラスを想定した「SampleSettingsV2」の2つのクラスを用意しています。SampleSettingsV2にはSampleSettingsV1にはない「IDプロパティ」が追加されています。ここではテストのために2つの設定クラスを作っただけで、実際は1つの設定クラス「SampleSettings」の2つのバージョンだと思ってください。2つの設定クラスが同じものとして扱われるように、DataContractAttributeのNameプロパティを双方とも「SampleSettings」にしています。

SampleSettingsV2クラスのIDプロパティでは、DataMemberAttribute属性のOrderプロパティに値を設定しています。これは、「ベスト プラクティス : データ コントラクトのバージョン管理」にあるガイドラインに従った処置です。このガイドラインによると、新しく追加されたメンバはOrderプロパティを使用して既存のデータメンバの後に配置されるようにすべきとされています。Orderプロパティについては、「DataContractSerializerを使って、オブジェクトのXMLシリアル化、逆シリアル化を行う」で説明しています。

このコードを実行すると、まず新規に作成したSampleSettingsV2オブジェクトをXMLシリアル化して、ファイル「V2.xml」に保存します。次に「V2.xml」を読み込んで、SampleSettingsV1オブジェクトに逆シリアル化しています。通常ならばこの時SampleSettingsV2クラスにしかないIDプロパティの値は失われます。

IDプロパティの値が失われていないことを確認するために、SampleSettingsV1オブジェクトを「V1.xml」に保存し、「V1.xml」からSampleSettingsV2を復元します。すると元の値に復元されていることが確認できます。

V2.xmlとV1.xmlの中身を確認してみましょう。V2.xmlは次のようになります(見やすいように改行を入れています)。

V1.xmlは以下のようになります。V2.xmlと全く同じ内容で、IDプロパティの値も保存されていることが確認できます。

  • 履歴:
  • 2014/5/12 FileStreamの代わりにXmlWriterとXmlReaderを使うようにした。
  • 2014/8/19 「DataContractSerializerを使って、オブジェクトのXMLシリアル化、逆シリアル化を行う」のリンク先が間違えていたのを修正。

注意:この記事では、基本的な事柄の説明が省略されているかもしれません。初心者の方は、特に以下の点にご注意ください。

  • Windows Vista以降でUACが有効になっていると、ファイルへの書き込みに失敗する可能性があります。詳しくは、こちらをご覧ください。
  • .NET Tipsをご利用いただく際は、注意事項をお守りください。