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

先日`Dapper`でDBから取得した`List<T>`型オブジェクトをExcelに出力するクラスを作りました｡
そのときに問題になったのは､単純に`GetProperties()`で取得して`foreach`してもプロパティの[列挙順は毎回同じにはならない](https://msdn.microsoft.com/ja-jp/library/kyaxdd3x(v=vs.110).aspx)という点です｡
何回かテストしてみたところ､全て定義順で出力されましたが､公式に保証しないと明言されている以上無視できません｡
`DataTable`を使えば何も考えずグルグル回せばいいのですが､それでは`Dapper`の旨味が無くなってしまいす｡
~~っていうか`DataTable`とか使いたくないし~~
プロパティをソートするお手軽な手段は`.Net Framework`に(たぶん)存在しないので自前で実装することにしました｡

[ソースはこちら](https://github.com/KeisukeKudo/PropertySort)

# 環境
- 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; }

}
```

`LastName`が`FirstName`の前に定義されています｡
最終的には以下のように出力します｡

| 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を適用する
`SortAttribute`を`Entity`クラスのプロパティ達に設定します｡

```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](https://qiita-image-store.s3.amazonaws.com/0/74711/2792c9da-e2c5-cbe2-6218-43d1f2039721.png)

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

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

#### 期待値
| FirstName | LastName |
|:----------|:---------|
| Shinji    | Ono      |
| Keisuke   | Honda    |
| Masashi   | Nakayama |

#### 結果
![201703251545.png](https://qiita-image-store.s3.amazonaws.com/0/74711/78e1ed76-668a-9dbc-bc3a-e3754b5cebfc.png)

こっちもOK

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

# 参考
- [C# によるプログラミング入門 | ++C++; // 未確認飛行 C](http://ufcpp.net/study/csharp/sp_attribute.html)
- [WisdomSoft(旧)](http://wisdom.sakura.ne.jp/programming/cs/cs64.html)