はじめに
実行環境に合わせてアプリケーションの動作を変更したり、必要な情報を指定したりするために動作設定ファイルを用いることがあります。私はそのような場合に XmlSerializer クラスでシリアライズした XML ファイルを利用することが多いです。
サンプル
private void SerializeSample()
{
string path = "SampleAppConfig.xml";
// シリアライズ
XmlSerializeUtility.Serialize(CreateConfig(), path);
// デシリアライズ
SampleAppConfig config = XmlSerializeUtility.Deserialize<SampleAppConfig>(path);
}
private static SampleAppConfig CreateConfig()
{
SampleAppConfig config = new SampleAppConfig();
DatabaseConfig db1 = new DatabaseConfig
{
ConnectionString = "User ID=user1; Password=password1; Host=localhost; Database=sampleDb1;",
ConnectionTimeout = 30,
};
DatabaseConfig db2 = new DatabaseConfig
{
ConnectionString = "User ID=user2; Password=password2; Host=localhost; Database=sampleDb2;",
ConnectionTimeout = 30,
};
config.Databases = new DatabaseConfig[] { db1, db2 };
return config;
}
public class SampleAppConfig
{
public SampleAppConfig() { }
public DatabaseConfig[] Databases { get; set; }
}
public class DatabaseConfig
{
public DatabaseConfig() { }
public string ConnectionString { get; set; }
public int ConnectionTimeout { get; set; }
}
public static class XmlSerializeUtility
{
public static void Serialize<T>(T config, string filePath)
{
XmlWriterSettings settings = new XmlWriterSettings
{
NewLineChars = Environment.NewLine,
Indent = true,
IndentChars = "\t",
Encoding = Encoding.UTF8
};
using (FileStream fs = new FileStream(filePath, FileMode.Create))
using (XmlWriter writer = XmlWriter.Create(fs, settings))
{
CreateSerializer<T>().Serialize(writer, config);
}
}
public static T Deserialize<T>(string filePath)
{
using (FileStream fs = new FileStream(filePath, FileMode.Open))
using (XmlReader writer = XmlReader.Create(fs))
{
return (T)CreateSerializer<T>().Deserialize(writer);
}
}
private static XmlSerializer CreateSerializer<T>()
{
return new XmlSerializer(typeof(T));
}
}
<?xml version="1.0" encoding="utf-8"?>
<SampleAppConfig xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<Databases>
<DatabaseConfig>
<ConnectionString>User ID=user1; Password=password1; Host=localhost; Database=sampleDb1;</ConnectionString>
<ConnectionTimeout>30</ConnectionTimeout>
</DatabaseConfig>
<DatabaseConfig>
<ConnectionString>User ID=user2; Password=password2; Host=localhost; Database=sampleDb2;</ConnectionString>
<ConnectionTimeout>30</ConnectionTimeout>
</DatabaseConfig>
</Databases>
</SampleAppConfig>
シリアライズ可能な型の条件
- public な型である。
- 引数のない public コンストラクタが定義されている。
シリアライズ対象のメンバ
- public メンバである。
- インスタンスメンバである。
- プロパティの場合、public な setter と getter が定義されている。非 public な setter あるいは getter が定義されている場合、例外が発生します。setter あるいは getter が定義されていない場合はシリアライズ対象外になります。
- フィールドの場合、readonly でない。
- XmlIgnoreAttribute が付与されていない。
属性による制御
シリアライズ対象の型やメンバに属性を付与することによって、シリアライズ処理を制御することができます。動作設定クラスのシリアライズに用いることが多いと考えられる属性を紹介します。
XmlTypeAttribute
タグとして型名が使用されるとき、型名の代わりに任意の名称を指定することができます。
[XmlType("アプリケーション設定")]
public class SampleAppConfig
{
public SampleAppConfig() { }
public DatabaseConfig Database { get; set; }
}
[XmlType("データベース設定")]
public class DatabaseConfig
{
public DatabaseConfig() { }
public string ConnectionString { get; set; }
public int ConnectionTimeout { get; set; }
}
プロパティの場合は型名ではなくプロパティ名が使用されますので、DatabaseConfig クラスに付与した XmlTypeAttribute は適用されません。
<?xml version="1.0" encoding="utf-8"?>
<アプリケーション設定 xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<Database>
<ConnectionString>User ID=user1; Password=password1; Host=localhost; Database=sampleDb1;</ConnectionString>
<ConnectionTimeout>30</ConnectionTimeout>
</Database>
</アプリケーション設定>
XmlElementAttribute
メンバがシリアライズされるとき、メンバ名の代わりに任意の名称を指定することができます。
[XmlType("アプリケーション設定")]
public class SampleAppConfig
{
public SampleAppConfig() { }
[XmlElement("データベース設定")]
public DatabaseConfig Database { get; set; }
}
public class DatabaseConfig
{
public DatabaseConfig() { }
[XmlElement("接続文字列")]
public string ConnectionString { get; set; }
[XmlElement("接続タイムアウト")]
public int ConnectionTimeout { get; set; }
}
<?xml version="1.0" encoding="utf-8"?>
<アプリケーション設定 xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<データベース設定>
<接続文字列>User ID=user1; Password=password1; Host=localhost; Database=sampleDb1;</接続文字列>
<接続タイムアウト>30</接続タイムアウト>
</データベース設定>
</アプリケーション設定>
対象のメンバが配列やリストである場合、格納オブジェクトがネストされずに出力されます。
public class SampleAppConfig
{
public SampleAppConfig() { }
[XmlElement("データベース設定")]
public DatabaseConfig[] Databases { get; set; }
}
public class DatabaseConfig
{
public DatabaseConfig() { }
public string ConnectionString { get; set; }
public int ConnectionTimeout { get; set; }
}
<?xml version="1.0" encoding="utf-8"?>
<SampleAppConfig xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<データベース設定>
<ConnectionString>User ID=user1; Password=password1; Host=localhost; Database=sampleDb1;</ConnectionString>
<ConnectionTimeout>30</ConnectionTimeout>
</データベース設定>
<データベース設定>
<ConnectionString>User ID=user2; Password=password2; Host=localhost; Database=sampleDb2;</ConnectionString>
<ConnectionTimeout>30</ConnectionTimeout>
</データベース設定>
</SampleAppConfig>
XmlArrayAttribute
配列またはリストのメンバがシリアライズされるとき、メンバ名の代わりに任意の名称を指定することができます。
public class SampleAppConfig
{
public SampleAppConfig() { }
[XmlArray("データベース設定")]
public DatabaseConfig[] Databases { get; set; }
}
public class DatabaseConfig
{
public DatabaseConfig() { }
public string ConnectionString { get; set; }
public int ConnectionTimeout { get; set; }
}
<?xml version="1.0" encoding="utf-8"?>
<SampleAppConfig xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<データベース設定>
<DatabaseConfig>
<ConnectionString>User ID=user1; Password=password1; Host=localhost; Database=sampleDb1;</ConnectionString>
<ConnectionTimeout>30</ConnectionTimeout>
</DatabaseConfig>
<DatabaseConfig>
<ConnectionString>User ID=user2; Password=password2; Host=localhost; Database=sampleDb2;</ConnectionString>
<ConnectionTimeout>30</ConnectionTimeout>
</DatabaseConfig>
</データベース設定>
</SampleAppConfig>
XmlArrayItemAttribute
配列またはリストのメンバがシリアライズされるとき、格納オブジェクトのタグに任意の名称を指定することができます。この属性は配列またはリストに格納できる派生オブジェクトの型を指定するときにも使用します。派生オブジェクトの型については後述します。
public class SampleAppConfig
{
public SampleAppConfig() { }
[XmlArrayItem("データベース設定")]
public DatabaseConfig[] Databases { get; set; }
}
public class DatabaseConfig
{
public DatabaseConfig() { }
public string ConnectionString { get; set; }
public int ConnectionTimeout { get; set; }
}
<?xml version="1.0" encoding="utf-8"?>
<SampleAppConfig xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<Databases>
<データベース設定>
<ConnectionString>User ID=user1; Password=password1; Host=localhost; Database=sampleDb1;</ConnectionString>
<ConnectionTimeout>30</ConnectionTimeout>
</データベース設定>
<データベース設定>
<ConnectionString>User ID=user2; Password=password2; Host=localhost; Database=sampleDb2;</ConnectionString>
<ConnectionTimeout>30</ConnectionTimeout>
</データベース設定>
</Databases>
</SampleAppConfig>
XmlEnumAttribute
列挙体型のメンバがシリアライズされるとき、列挙値名の代わりに任意の名称を指定することができます。
public class SampleAppConfig
{
public SampleAppConfig() { }
public DatabaseConfig Database { get; set; }
}
public class DatabaseConfig
{
public DatabaseConfig() { }
public string ConnectionString { get; set; }
public int ConnectionTimeout { get; set; }
public DatabaseKind Kind { get; set; }
}
public enum DatabaseKind
{
[XmlEnum("unknown")]
Unknown = 0,
[XmlEnum("oracle")]
Oracle,
[XmlEnum("sqlserver")]
SqlServer,
[XmlEnum("postgresql")]
PostgreSQL,
}
<?xml version="1.0" encoding="utf-8"?>
<SampleAppConfig xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<Database>
<ConnectionString>User ID=user1; Password=password1; Host=localhost; Database=sampleDb1;</ConnectionString>
<ConnectionTimeout>30</ConnectionTimeout>
<Kind>postgresql</Kind>
</Database>
</SampleAppConfig>
XmlAttributeAttribute
メンバがシリアライズされるとき、入れ子ではなく属性として出力します。
public class SampleAppConfig
{
public SampleAppConfig() { }
public DatabaseConfig Database { get; set; }
}
public class DatabaseConfig
{
public DatabaseConfig() { }
[XmlAttribute]
public string ConnectionString { get; set; }
[XmlAttribute]
public int ConnectionTimeout { get; set; }
}
<?xml version="1.0" encoding="utf-8"?>
<SampleAppConfig xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<Database ConnectionString="User ID=user1; Password=password1; Host=localhost; Database=sampleDb1;" ConnectionTimeout="30" />
</SampleAppConfig>
コレクションのシリアライズ
List<T>
List<T> はサポートされています。格納オブジェクトのタグは型名になります。
public class SampleAppConfig
{
public SampleAppConfig() {}
public List<DatabaseConfig> Databases { get; set; }
}
public class DatabaseConfig
{
public DatabaseConfig() {}
public string ConnectionString { get; set; }
public int ConnectionTimeout { get; set; }
}
<?xml version="1.0" encoding="utf-8"?>
<SampleAppConfig xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<Databases>
<DatabaseConfig>
<ConnectionString>User ID=user1; Password=password1; Host=localhost; Database=sampleDb1;</ConnectionString>
<ConnectionTimeout>30</ConnectionTimeout>
</DatabaseConfig>
<DatabaseConfig>
<ConnectionString>User ID=user2; Password=password2; Host=localhost; Database=sampleDb2;</ConnectionString>
<ConnectionTimeout>30</ConnectionTimeout>
</DatabaseConfig>
</Databases>
</SampleAppConfig>
格納オブジェクトのタグを変更したい場合は、XmlArrayItem
属性を付与します。
public class SampleAppConfig
{
public SampleAppConfig() {}
[XmlArrayItem("Database")]
public List<DatabaseConfig> Databases { get; set; }
}
public class DatabaseConfig
{
public DatabaseConfig() {}
public string ConnectionString { get; set; }
public int ConnectionTimeout { get; set; }
}
<?xml version="1.0" encoding="utf-8"?>
<SampleAppConfig xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<Databases>
<Database>
<ConnectionString>User ID=user1; Password=password1; Host=localhost; Database=sampleDb1;</ConnectionString>
<ConnectionTimeout>30</ConnectionTimeout>
</Database>
<Database>
<ConnectionString>User ID=user2; Password=password2; Host=localhost; Database=sampleDb2;</ConnectionString>
<ConnectionTimeout>30</ConnectionTimeout>
</Database>
</Databases>
</SampleAppConfig>
Dictionary<T>
Dictionary<T> はサポートされていません。InvalidOperationException がスローされます。
IXmlSerializable インターフェースを実装した Dictionary<T> 継承クラスを定義し、Dictionary<T> の代わりに用いる方法があります。
public class XmlSerializableDictionary<TKey, TValue> : Dictionary<TKey, TValue>, IXmlSerializable
{
#region ctor
public XmlSerializableDictionary() : base()
{
}
public XmlSerializableDictionary(int capacity) : base(capacity)
{
}
public XmlSerializableDictionary(IEqualityComparer<TKey> comparer) : base(comparer)
{
}
public XmlSerializableDictionary(IDictionary<TKey, TValue> dictionary) : base(dictionary)
{
}
public XmlSerializableDictionary(int capacity, IEqualityComparer<TKey> comparer) : base(capacity, comparer)
{
}
public XmlSerializableDictionary(IDictionary<TKey, TValue> dictionary, IEqualityComparer<TKey> comparer) : base(dictionary, comparer)
{
}
#endregion
#region implements of IXmlSerializable
// これらのメンバはアプリケーションから直接利用するものではないため、明示的に実装しています。
System.Xml.Schema.XmlSchema IXmlSerializable.GetSchema()
{
return null;
}
void IXmlSerializable.ReadXml(XmlReader reader)
{
XmlSerializer serializer = new XmlSerializer(typeof(KeyValuePair));
reader.Read();
if (reader.IsEmptyElement) { return; }
while (reader.NodeType != XmlNodeType.EndElement)
{
KeyValuePair keyValue = serializer.Deserialize(reader) as KeyValuePair;
if (keyValue != null) { Add(keyValue.Key, keyValue.Value); }
}
reader.Read();
}
void IXmlSerializable.WriteXml(XmlWriter writer)
{
XmlSerializer serializer = new XmlSerializer(typeof(KeyValuePair));
KeyValuePair keyValue = new KeyValuePair();
foreach (KeyValuePair<TKey, TValue> pair in this)
{
keyValue.Key = pair.Key;
keyValue.Value = pair.Value;
serializer.Serialize(writer, keyValue);
}
}
#endregion
// シリアライズされるときのタグを XmlType 属性で指定しています。
[XmlType("Item")]
[Browsable(false)]
[EditorBrowsable(EditorBrowsableState.Never)]
public class KeyValuePair
{
public KeyValuePair() { }
public KeyValuePair(TKey key, TValue value)
{
Key = key;
Value = value;
}
public TKey Key { get; set; }
public TValue Value { get; set; }
}
}
Dictionary<TKey, TValue> の代わりに XmlSerializableDictionary<TKey, TValue> を使用します。
public class SampleAppConfig
{
public SampleAppConfig() {}
public XmlSerializableDictionary<string, DatabaseConfig> Databases { get; set; }
}
public class DatabaseConfig
{
public DatabaseConfig() {}
public string ConnectionString { get; set; }
public int ConnectionTimeout { get; set; }
}
<?xml version="1.0" encoding="utf-8"?>
<SampleAppConfig xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<Databases>
<Item>
<Key>db1</Key>
<Value>
<ConnectionString>User ID=user1; Password=password1; Host=localhost; Database=sampleDb1;</ConnectionString>
<ConnectionTimeout>30</ConnectionTimeout>
</Value>
</Item>
<Item>
<Key>db2</Key>
<Value>
<ConnectionString>User ID=user2; Password=password2; Host=localhost; Database=sampleDb2;</ConnectionString>
<ConnectionTimeout>30</ConnectionTimeout>
</Value>
</Item>
</Databases>
</SampleAppConfig>
派生クラスのシリアライズ
次の Serialize メソッドでは例外が発生します。
XmlSerializer クラスではシリアライズ対象の型の定義から必要な型情報を読み込みます。SampleAppConfig クラスの型情報の中には DatabaseConfig クラスの型情報は含まれていますが、DatabaseConfig クラスから派生した PostgreSqlConfig クラスの情報は含まれていません。Databases プロパティに格納されている PostgreSqlConfig クラスのインスタンスをシリアライズしようとして型情報不足が発生し、シリアライズに失敗します。
private void Serialize()
{
XmlSerializer s = new XmlSerializer(typeof(SampleAppConfig));
using (MemoryStream ms = new MemoryStream())
using (XmlWriter writer = XmlWriter.Create(ms))
{
s.Serialize(writer, CreateConfig());
}
}
private SampleAppConfig CreateConfig()
{
SampleAppConfig config = new SampleAppConfig();
// PostgreSqlConfig クラスのインスタンスを生成
DatabaseConfig db1 = new PostgreSqlConfig
{
ConnectionString = "User ID=user1; Password=password1; Host=localhost; Database=sampleDb1;",
ConnectionTimeout = 30,
};
DatabaseConfig db2 = new DatabaseConfig
{
ConnectionString = "User ID=user2; Password=password2; Host=localhost; Database=sampleDb2;",
ConnectionTimeout = 30,
};
config.Databases = new DatabaseConfig[] { db1, db2 };
return config;
}
public class SampleAppConfig
{
public SampleAppConfig() { }
public DatabaseConfig[] Databases { get; set; }
}
public class DatabaseConfig
{
public DatabaseConfig() { }
public string ConnectionString { get; set; }
public int ConnectionTimeout { get; set; }
}
public class PostgreSqlConfig : DatabaseConfig
{
}
シリアライザに PostgreSqlConfig クラスの型情報を与えることによって、シリアライズできるようになります。いくつかの方法があります。
- XmlSerializer インスタンスに ExtraTypes として PostgreSqlConfig クラスの型情報を与える。
- SampleAppConfig クラスに XmlIncludeAttribute を付与し、PostgreSqlConfig クラスの型情報を与える。
- DatabaseConfig クラスに XmlIncludeAttribute を付与し、PostgreSqlConfig クラスの型情報を与える。
- SampleAppConfig.Databases プロパティに XmlArrayItemAttribute を付与し、PostgreSqlConfig クラスの型情報を与える。XmlArrayItemAttribute を付与する場合、その配列やリストに格納される可能性がある全ての型を指定する必要があります。
XmlSerializer s = new XmlSerializer(typeof(SampleAppConfig), new Type[] { typeof(PostgreSqlConfig) });
[XmlInclude(typeof(PostgreSqlConfig))]
public class SampleAppConfig
{
public SampleAppConfig() { }
public DatabaseConfig[] Databases { get; set; }
}
[XmlInclude(typeof(DatabaseConfig))]
public class DatabaseConfig
{
public DatabaseConfig() { }
public string ConnectionString { get; set; }
public int ConnectionTimeout { get; set; }
}
public class SampleAppConfig
{
public SampleAppConfig() { }
[XmlArrayItem(typeof(DatabaseConfig))]
[XmlArrayItem(typeof(PostgreSqlConfig))]
public DatabaseConfig[] Databases { get; set; }
}
XmlSerializer に ExtraTypes として指定した場合、または XmlIncludeAttribute を付与した場合は、PostgreSqlConfig であることが属性で表されます。
<?xml version="1.0" encoding="utf-8"?>
<SampleAppConfig xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<Databases>
<DatabaseConfig xsi:type="PostgreSqlConfig">
<ConnectionString>User ID=user1; Password=password1; Host=localhost; Database=sampleDb1;</ConnectionString>
<ConnectionTimeout>30</ConnectionTimeout>
</DatabaseConfig>
<DatabaseConfig>
<ConnectionString>User ID=user2; Password=password2; Host=localhost; Database=sampleDb2;</ConnectionString>
<ConnectionTimeout>30</ConnectionTimeout>
</DatabaseConfig>
</Databases>
</SampleAppConfig>
XmlArrayItemAttribute を付与した場合は、PostgreSqlConfig であることがタグで表されます。XmlArrayItemAttribute で名前が指定されている場合はその名前になります。
<?xml version="1.0" encoding="utf-8"?>
<SampleAppConfig xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<Databases>
<PostgreSqlConfig>
<ConnectionString>User ID=user1; Password=password1; Host=localhost; Database=sampleDb1;</ConnectionString>
<ConnectionTimeout>30</ConnectionTimeout>
</PostgreSqlConfig>
<DatabaseConfig>
<ConnectionString>User ID=user2; Password=password2; Host=localhost; Database=sampleDb2;</ConnectionString>
<ConnectionTimeout>30</ConnectionTimeout>
</DatabaseConfig>
</Databases>
</SampleAppConfig>
他の動作設定ファイルとの比較
アプリケーション構成ファイル(app.config, web.config)
VisualStudio のデザイナによる UI サポートを受けられますが、キーと数値や文字列などの単純な値で表すことができるものがターゲットとされており、入れ子やリストなどの構造を管理するのには向いていません。
各アプリケーションが一つのアプリケーション構成ファイルを持つ仕組みであるため、複数のアプリケーションで動作設定を共有したい場合には共有部分を外部ファイルに分離する必要があります。
何れも方法がないわけではありませんが、それをするぐらいなら XmlSerializer クラスを使ったほうが簡単です。
私は次のように使い分けることが多いです。
- アセンブリのバージョンリダイレクトなどフレームワークに対する動作設定はアプリケーション構成ファイルで管理する。
- アプリケーションやライブラリに対する動作設定は独自の設定ファイルで管理する。
DataContractSerializer クラスを使ったシリアライズ
DataContract という名が表す通り、データ通信で使用することを目的としていると考えられます。private メンバをシリアライズできたり XmlSerializer クラスよりも高機能ではあるのですが、定義された順番どおりにメンバを記述しないといけません。動作設定ファイルはテキストエディタで記述することもあるため、これは扱いにくさにつながります。
JSONファイル
最近は JSON ファイルを動作設定ファイルに用いることも増えてきました。DataContractJsonSerializer クラスや Json.NET など、JSON を扱うためのライブラリも整備され扱いやすいフォーマットですが、コメントを記述しづらいのがネックです。
INIファイル
特段の理由がない限り、これから新規開発するアプリケーションで INI ファイルを用いることはないと思います。
ですが、「これまで INI ファイルを使っていた」という理由だけで用いられ続けているケースがあるもの実態です。過去のアプリケーションの思想をそのまま受け継いだアプリケーションの開発で当たり前のように INI ファイルが用いられていて、INI ファイルに纏わる不便さを感じたことがあれば、是非 ここで紹介したようなその他の方法を検討してみてください。
終わりに
XmlSerializer クラスはシリアライズ速度やシリアライズデータサイズなどの面ではその他のシリアライザに劣ります。データ通信などの用途に使われることは少なくなっていますが、今でも、動作設定ファイルのシリアライズフォーマットとしてはバランスが取れた良い手段の一つだと思います。