C#
.NETFramework

プロパティをソートして出力する

More than 1 year has passed since last update.

先日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で扱えるエンティティクラスを作成します。

Entity.cs
class Entity {

    public string LastName { get; set; }

    public string FirstName { get; set; }

    public int Age { get; set; }

}

LastNameFirstNameの前に定義されています。
最終的には以下のように出力します。

FirstName LastName Age
Shinji Ono 37
Keisuke Honda 30
Masashi Nakayama 49

CustomAttributeを作る

今回の肝とも言える部分です。
この属性を上述したクラスのプロパティへ設定し、ソートの基準とします。

SortAttribute.cs
[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を適用する

SortAttributeEntityクラスのプロパティ達に設定します。

Entity.cs
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形式にして出力するだけです。

まず値を設定

Program.cs
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出力クラスを作ります。

Program.cs
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()で出力します。

Program.cs
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

結果

201703251538.png

うまく出力してくれました。

また、WhereSortAttributeが設定されていないプロパティは除外しているので、任意のプロパティのみ出力したい場合にも対応できます。
AgeプロパティからSortAttributeを削除します。

期待値

FirstName LastName
Shinji Ono
Keisuke Honda
Masashi Nakayama

結果

201703251545.png

こっちもOK

あとがき

プロパティを任意順にソートする機会なんてあまりないと思いますが、何かの参考になれば幸いです。
カスタム属性を今までほぼ使ったことがなかったのでいい勉強になりました。
アイディア次第では色々と応用がききそうです。

参考