【.NET】Listに対する自由自在な動的ソートを実現する

# こんな動的ソートを実現します

以下のすべての点を満たすソート処理を作ります。

* **文字列**で指定されたプロパティの値でListをソートする。
* Listを構成する各インスタンスに含まれる深い階層のインスタンスのプロパティも、ソートキーとして指定できる。
* Linqの `OrderBy` 句等の使用に慣れている人にも馴染みやすいよう、**Linqそっくりな使い方のメソッドも用意**する。

例えば、以下のようなListをソート対象と仮定します。

```csharp
var targetList = new List<SomeClass>
{
    new SomeClass // This is first item.
    {
        Prop1 = 123,
        Prop2 = 45.6,
        Prop3 = "ABC",
        Prop4 = new InnerClass
        {
            InnerProp1 = 23,
            InnerProp2 = "45"
            InnerProp3 = new DeepClass
            {
                DeepProp1 = 6.7
            }
        }
    },
    // ... some other items here ...
    new SomeClass // This is last item.
    {
        Prop1 = 987,
        Prop2 = 65.4,
        Prop3 = "ZYX",
        Prop4 = new InnerClass
        {
            InnerProp1 = 54,
            InnerProp2 = "32"
            InnerProp3 = new DeepClass
            {
                DeepProp1 = 1.0
            }
        }
    },
};
```

実行したいソートに応じて、以下のようにソート処理を呼び出せるものとします。

* 使用例1 ： このリストを `Prop1` で昇順にソートする場合

```csharp
// ≪メソッド形式≫　※第3引数はソートの昇順・降順を指定するEnum
Sort(ref targetList, "Prop1", SortType.Asc);

// ≪Linq風形式≫
var sortedList = targetList
    .OrderBy("Prop1", SortType.Asc)
    .ToList();
```

* 使用例2 ： 深い階層 `Prop4.InnerProp3.DeepProp1` で降順にソートする場合

```csharp
// ≪メソッド形式≫
Sort(ref targetList, "Prop4.InnerProp3.DeepProp1", SortType.Desc);

// ≪Linq風形式≫
var sortedList = targetList
    .OrderBy("Prop4.InnerProp3.DeepProp1", SortType.Desc)
    .ToList();
```

* 使用例3<br>
さらに複雑なソートも動的に実現できるものとします。
例えば、SQLの`ORDER BY`句風に書いた場合に
`ORDER BY Prop1 ASC, Prop4.InnerProp3.DeepProp1 DESC, Prop4.InnerProp2 DESC`
となるようなソートをする場合、以下のようにメソッドを呼び出します。

```csharp
// ≪メソッド形式≫
var sortKeySortTypePairs = new Dictionary<string, SortType>
{
    { "Prop1", SortType.Asc },
    { "Prop4.InnerProp3.DeepProp1", SortType.Desc },
    { "Prop4.InnerProp2", SortType.Desc },
};
Sort(ref targetList, sortKeySortTypePairs);

// ≪Linq風形式≫
var sortedList = targetList
    .OrderBy(new Dictionary<string, SortType>
    {
        { "Prop1", SortType.Asc },
        { "Prop4.InnerProp3.DeepProp1", SortType.Desc },
        { "Prop4.InnerProp2", SortType.Desc },
    })
    .ToList();
```

汎用性を持たせたいので、（理論上）**どれだけ深い階層のプロパティでもソートキーとして指定できる**メソッドとして作成します。

# このソート処理の使い所は？

.NET Core等で「リクエストに応じて何らかのリストを生成して返すWebAPI」を作る場合、リクエストで指定されたキーでソートされたリストを返したい、という場面が生じることがあります。
.NETでソートを行う際の定番はLinqの`OrderBy`句や`ThenBy`句等々ですが、これらを素直に使う場合、ソート対象のプロパティは静的に指定することになります。
なので、想定されるソートキーの数だけif、else ifを連結して`OrderBy`等を実装する、などというゴリ押しな手段を採ることになりかねません。

そこで、この**ソートキーを動的に指定できるソート処理**の出番となります。

例えば、リクエストボディの中に

```json
"sortKey": "Prop4.InnerProp1",
"sortType": "Asc",
```
という要素が積まれるとして、これらをソート処理の呼び出しに使うようにする、という寸法です。

# この動的ソートを実現するNuGetパッケージを作ってみた

上記のソート処理用メソッドを持つNuGetパッケージを作り、公開してみました。
https://www.nuget.org/packages/MagicSort/

対応フレームワーク・対応バージョンは以下の通りです。

* .NET Core 2.0~
* .NET Framework 4.5~
* .NET Standard 2.0~

ソースはGitHubにて公開しています。
https://github.com/microwavePC/MagicSort

詳細な使い方は、GitHubのREADMEを参照してください！

# 中でこんな処理を行っています

この処理の肝となる部分を簡潔に紹介します。処理の全体は[GitHubにupしたソース](https://github.com/microwavePC/MagicSort)をご覧ください。

このソート処理の中では、Linqの`OrderBy` `OrderByDescending` を使用しています。
複数のキーが指定される場合は、加えて`ThenBy` `ThenByDescending`も使用しています。
これらの引数となるラムダ式を、処理内部で動的に生成しています。

単一キーによるソート処理では、以下のように動的ソートを実現しています。

```csharp
// 変数「targetList」「sortKey」と「sortType」は、メソッドの引数で渡されたものを使用する。
// ジェネリックのTには、このメソッドが呼び出されるときに「targetList」の要素の型が入る。

// ここで呼び出している内部処理「AssembleOrderFunc」で、OrderBy句の引数となるラムダ式を動的に生成する。
Func<T, object> orderFunc = AssembleOrderFunc<T>(sortKey);

switch (sortType)
{
    case SortType.Asc:
        targetList = targetList
            .OrderBy(orderFunc)
            .ToList();
        break;
    case SortType.Desc:
        targetList = targetList
            .OrderByDescending(orderFunc)
            .ToList();
        break;
}
```

ラムダ式を生成するメソッド `AssembleOrderFunc` では、以下の処理を行っています。

```csharp
/// <summary>
/// Assembles the function object for sorting Linq method.
/// </summary>
/// <typeparam name="T">Type of target list class.</typeparam>
/// <param name="sortKey">Sort key.</param>
/// <returns>Function object.</returns>
private static Func<T, object> AssembleOrderFunc<T>(string sortKey)
    where T : class
{
    // ソートキーを「.」で分割する。
    List<string> sortKeyHierarchy = sortKey.Split('.').ToList();

    // リフレクションを使用し、ソート対象となるプロパティ値を引っ張り出す。
    Func<T, object> orderFunc = x =>
    {
        object val = x;
        foreach (string key in sortKeyHierarchy)
        {
            if (val == null)
            {
                return val;
            }

            val = val.GetType().GetRuntimeProperty(key).GetValue(val);
        }

        return val;
    };

    return orderFunc;
}
```

# このソート処理のパフォーマンスは？

実行する環境にも拠るとは思いますが、[この1024件のデータを持つ多階層リスト](https://github.com/microwavePC/MagicSort/blob/master/MagicSort.Test/TestData.cs)を対象に以下のような5重のソート処理を実行したところ、デバッグ実行における処理時間は5～10ミリ秒前後でした。

```csharp
// メソッド形式
var sortKeySortTypePairs = new Dictionary<string, SortType>
{
    { "Property4.PropertyW.PropertyD.PropertyK.Property4.PropertyW.PropertyD.PropertyI", SortType.Desc },
    { "Property1", SortType.Asc },
    { "Property4.PropertyW.PropertyA", SortType.Desc },
    { "Property2", SortType.Asc },
    { "Property4.PropertyW.PropertyD.PropertyJ", SortType.Asc },
}

MagicSorter.Sort(ref targetList, sortKeySortTypePairs);
```

```csharp
// Linq風形式
var sortedList = targetList
    .OrderBy(new Dictionary<string, SortType>
    {
        { "Property4.PropertyW.PropertyD.PropertyK.Property4.PropertyW.PropertyD.PropertyI", SortType.Desc },
        { "Property1", SortType.Asc },
        { "Property4.PropertyW.PropertyA", SortType.Desc },
        { "Property2", SortType.Asc },
        { "Property4.PropertyW.PropertyD.PropertyJ", SortType.Asc },
    })
    .ToList();
```