はじめに
C#のSystem.Xml.Serialization.XmlSerializerを使って列挙型のシリアライズを行うと,識別子の名前がそのまま出力されてしまっていい感じ(?)ではありません。
そこで,列挙型をシリアライズしたときに出力される文字列をカスタマイズできるようにしてみました。
コード
using System;
using System.Reflection;
using System.Xml.Serialization;
namespace Qiita
{
// 列挙体に任意の文字列を割り当てるための属性
public class CustomTextAttribute : Attrubute
{
public string StringValue { get; protected set; }
public CustomTextAttribute(string value)
{
this.StringValue = value;
}
}
// 列挙体
public enum Wochentage
{
[CustomText("mo")]
Montag,
[CustomText("di")]
Dienstag,
[CustomText("mi")]
Mittwoch,
[CustomText("do")]
Donnerstag,
[CustomText("fr")]
Freitag,
[CustomText("sa")]
Samstag,
[CustomText("so")]
Sonntag
}
// シリアライズのためのラッパー
[Serializable]
public class EnumWrapper<T> where T : Enum
{
protected T value;
[XmlText]
public string Value
{
get
{
// リフレクションで属性を取得
var type = this.value.GetType();
var fieldInfo = type.GetField(this.value.ToString());
if (fieldInfo == null) return this.value.ToString();
var attrs = fieldInfo.GetCustomAttributes<CustomTextAttribute>() as CustomTextAttribute[];
return attrs.Length > 0 ? attrs[0].StringValue : this.value.ToString();
} // get
set
{
// 効率的な方法がわからなかったので強引に頑張る
foreach (T val in Enum.GetValues(typeof(T)))
{
if (val.ToString<CustomTextAttribute>() == value)
this.value = val;
return;
}
} // set
}
public EnumWrapper() { }
public EnumWrapper(T val)
{
this.value = val;
}
// 列挙体との暗黙の型変換を定義
public static implicit operator EnumWrapper<T>(T val)
=> new EnumWrapper<T>(val);
public static implicit operator T(EnumWrapper<T> val)
=> val.value;
} // public class EnumWrapper<T> where T : Enum
} // namespace Qiita
使い方
シリアライズする型の中の列挙体(Wochentageとします)の型をEnumWrapper<Wochentage>に変更するだけで,CustomText属性で指定した値を使用してシリアライズされるようになります。
変更前の型(Wochentage)と暗黙的に変換できるため,その他のコードに変更を加える必要はありません。
その他
今回はプロパティの中でゴリゴリ書きましたが,CustomTextAttributeに相当するようなクラスを複数作り,拡張メソッドToString<T>を定義してあげて文字列に変換する,と言ったこともできます(UIに表示する用とか)。
2025-11-06追記: リフレクションは重い
上記の例では,値を取得するためにリフレクションを用いていますが,これはなかなか重い処理です。
そんなに速度を求めない場面であれば問題にはならないかもしれませんが,それでも改善できるならしたいところです。
というわけで,ソースジェネレータでコンパイル時に属性の値を取得してしまうというアプローチを考えました。
詳細な実装についてはGitHubをご参照ください。
簡単な概要
上記の例の場合には[EnumSerializable(typeof(CustomTextAttribute))]
public enum Wochentage
{
[CustomText("mo")]
Montag,
[CustomText("di")]
Dienstag,
[CustomText("mi")]
Mittwoch,
[CustomText("do")]
Donnerstag,
[CustomText("fr")]
Freitag,
[CustomText("sa")]
Samstag,
[CustomText("so")]
Sonntag
}
のように書くと,ソースジェネレータによって
internal static class WochentageSerializationExtensions
{
internal static string ToCustomText(this Wochentage value)
{
return value switch
{
Wochentage.Montag => "mo",
Wochentage.Dienstag => "di",
Wochentage.Mittwoch => "mi",
Wochentage.Donnerstag => "do",
Wochentage.Freitag => "fr",
Wochentage.Samstag => "sa",
Wochentage.Sonntag => "so",
_ => value.ToString(),
};
}
internal static string ToString<TAttr>(this Wochentage value)
{
if (typeof(TAttr) == typeof(CustomTextAttribute))
return ToCustomText(value);
return value.ToString()
}
}
といった拡張メソッドが自動的に生成されます。
これを利用して,例えば
var foo = Wochentage.Montag;
var customText = foo.ToCustomText(); // あるいは foo.ToString<CustomTextAttribute>();
のように書くことができます。
これは実行時にはただのswitch式になるのでリフレクションよりも高速です。
ToString<TAttr>の方もTAttrに具体的な型が入ったメソッドのJITコンパイル時に分岐の条件が確定するため,実行時には分岐は消滅するはずです。
すべての列挙体を調査し,属性もすべて見るのではあまりにもコストが高いため,対象となる列挙体に対して属性を付けるようにしています。
自分が管理するプロジェクトで使用する前提で作成したため細かい部分は粗いですが,こういう方法もいいんじゃないかと思ったのでご紹介しました。