先日Dapper
でDBから取得したList<T>
型オブジェクトをExcelに出力するクラスを作りました。
そのときに問題になったのは、単純にGetProperties()
で取得してforeach
してもプロパティの列挙順は毎回同じにはならないという点です。
何回かテストしてみたところ、全て定義順で出力されましたが、公式に保証しないと明言されている以上無視できません。
DataTable
を使えば何も考えずグルグル回せばいいのですが、それではDapper
の旨味が無くなってしまいす。
っていうかDataTable
とか使いたくないし
プロパティをソートするお手軽な手段は.Net Framework
に(たぶん)存在しないので自前で実装することにしました。
環境
- Windows 7 64bit
- Visual Studio 2015
実装
C#
でソートといえばみんな大好きLINQ
ただし、OrderBy
する基準を定義しなければいけません。
名前順でいいのであればPropertyInfo.Name
でソートすればOK
ただし今回はソート順を任意に指定したいので独自属性を付けてOrderBy
します。
また、この記事ではExcelに出力するのは面倒なのでCSV形式で出力するサンプルを提示します。
エンティティクラスを作る
まずDapper
で扱えるエンティティクラスを作成します。
class Entity {
public string LastName { get; set; }
public string FirstName { get; set; }
public int Age { get; set; }
}
LastName
がFirstName
の前に定義されています。
最終的には以下のように出力します。
FirstName | LastName | Age |
---|---|---|
Shinji | Ono | 37 |
Keisuke | Honda | 30 |
Masashi | Nakayama | 49 |
CustomAttributeを作る
今回の肝とも言える部分です。
この属性を上述したクラスのプロパティへ設定し、ソートの基準とします。
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = false)]
public class SortAttribute : Attribute {
private readonly int Index;
public SortAttribute(int index) {
this.Index = index;
}
public int SortIndex {
get { return this.Index; }
}
}
Attribute
クラスを継承し、AttributeUsage
属性を設定することで独自属性を定義できます。
コンストラクタにインデックスを渡してreadonly
なプロパティでその値を取得できるようにします。
属性パラメーターについて
AttributeTargets
適用可能範囲です。
デフォルトでAll
になっていますが、今回はプロパティにしか使わないのでProperty
を設定します。
AllowMultiple
複数設定の可、不可を指定します。
デフォルトでtrue
です。
1プロパティに複数設定できると困るのでfalse
を指定します。
Inherited
継承の可、不可を指定します。
デフォルトでtrue
です。
継承する必要がないのでfalse
を指定します。
実際に使う場合は用途によって切り替えてください。
エンティティクラスへCustomAttributeを適用する
SortAttribute
をEntity
クラスのプロパティ達に設定します。
class Entity {
[SortAttribute(1)]
public string LastName { get; set; }
//Attributeは省略できる
[Sort(0)]
public string FirstName { get; set; }
[Sort(2)]
public int Age { get; set; }
}
出力
後は値をソートしてCSV形式にして出力するだけです。
まず値を設定
class Program {
static void Main(string[] args) {
//出力する内容
var entitys = new List<Entity>() {
new Entity() { LastName = "Ono", FirstName = "Shinji", Age = 37 },
new Entity() { LastName = "Honda", FirstName = "Keisuke", Age = 30 },
new Entity() { LastName = "Nakayama", FirstName = "Masashi", Age = 49 },
};
}
}
次にCSV出力クラスを作ります。
public class ConvertCsv<T> {
private List<T> Entitys;
/// <summary>
/// 変換対象ソース
/// </summary>
public List<T> Source {
set {
this.Entitys = value;
}
}
/// <summary>
/// CSV形式で出力
/// </summary>
/// <returns></returns>
public string ToCsv() {
var propertyNames = typeof(T).GetProperties()
//SortAttribute属性が設定されているプロパティのみ対象
.Where(e => Attribute.IsDefined(e, typeof(SortAttribute)))
//SortAttribute属性のSortIndexプロパティでソート
.OrderBy(e => ((SortAttribute)Attribute.GetCustomAttribute(e, typeof(SortAttribute))).SortIndex)
//プロパティ名を取得
.Select(e => e.Name);
var result = new StringBuilder();
//ヘッダー出力
result.AppendLine(string.Join(",", propertyNames));
foreach (var entity in this.Entitys) {
//要素出力
var element = propertyNames.Select(n => typeof(T).GetProperty(n).GetValue(entity));
result.AppendLine(string.Join(",", element));
}
return result.ToString().TrimEnd('\r', '\n');
}
}
GetCustomAttribute()
でSortAttribute
を取得してキャスト、SortIndex
プロパティで設定値を取得してその値を基準にソートしています。
後はforeach
で回してpropertyNames
順に値を取り出して出力しているだけです。
ConvertCsv
クラスに値を渡してCSV形式の文字列を取得、Console.WriteLine()
で出力します。
class Program {
static void Main(string[] args) {
//出力する内容
var entitys = new List<Entity>() {
new Entity() { LastName = "Ono", FirstName = "Shinji", Age = 37 },
new Entity() { LastName = "Honda", FirstName = "Keisuke", Age = 30 },
new Entity() { LastName = "Nakayama", FirstName = "Masashi", Age = 49 },
};
var csv = new ConvertCsv<Entity>() { Source = entitys };
Console.WriteLine(csv.ToCsv());
}
}
出力結果
期待値
FirstName | LastName | Age |
---|---|---|
Shinji | Ono | 37 |
Keisuke | Honda | 30 |
Masashi | Nakayama | 49 |
結果
うまく出力してくれました。
また、Where
でSortAttribute
が設定されていないプロパティは除外しているので、任意のプロパティのみ出力したい場合にも対応できます。
Age
プロパティからSortAttribute
を削除します。
期待値
FirstName | LastName |
---|---|
Shinji | Ono |
Keisuke | Honda |
Masashi | Nakayama |
結果
こっちもOK
あとがき
プロパティを任意順にソートする機会なんてあまりないと思いますが、何かの参考になれば幸いです。
カスタム属性を今までほぼ使ったことがなかったのでいい勉強になりました。
アイディア次第では色々と応用がききそうです。