- `をソートする完全な例です。
using System.Linq.Expressions;
// エンティティクラスの定義
public class Item
{
public int Id { get; set; }
public string ItemName { get; set; } = "";
public decimal Cost { get; set; }
}
public static class ExpressionBuilder
{
// 動的ソートラムダの生成: x => x.PropertyName
public static Expression> CreateSortExpression<T>(string propertyName)
{
// パラメータ: x
ParameterExpression param = Expression.Parameter(typeof(T), "x");
// プロパティアクセス: x.PropertyName
MemberExpression property = Expression.Property(param, propertyName);
// プロパティ値をobjectに変換(OrderByはobjectを返す必要があるが、実際の型はstring/intなど)
UnaryExpression convert = Expression.Convert(property, typeof(object));
// ラムダ: x => (object)x.PropertyName
return Expression.Lambda>(convert, param);
}
}
class Program
{
static void Main()
{
// モデルデータ
var items = new List<Item>
{
new Item { Id = 3, ItemName = "Zebra", Cost = 100 },
new Item { Id = 1, ItemName = "Apple", Cost = 50 },
new Item { Id = 2, ItemName = "Banana", Cost = 75 }
};
// ユーザーによるソートフィールドの選択をシミュレート(例:コンソール入力から)
Console.Write("ソートフィールドを入力してください (ItemName/Cost/Id): ");
string sortField = Console.ReadLine() ?? "ItemName";
// 式木を動的に構築
var sortLambda = ExpressionBuilder.CreateSortExpression<Item>(sortField);
// ソートの実行(メモリコレクションでは.Compile()でデリゲートに変換)
var sorted = items.OrderBy(sortLambda.Compile()).ToList();
// 結果の出力
Console.WriteLine($"\n{sortField}でソートした結果:");
foreach (var item in sorted)
{
Console.WriteLine($"Id:{item.Id}, ItemName:{item.ItemName}, Cost:{item.Cost}");
}
}
}
**実行例**:
- `ItemName`を入力 → 名前順にソート:Apple, Banana, Zebra。
- `Cost`を入力 → 価格昇順にソート:50, 75, 100。
> 注意点:`Enumerable.OrderBy`(メモリコレクション)では、式木をデリゲートに変換するために`.Compile()`の呼び出しが必須です。`IQueryable`(EF Coreの`DbSet`など)では、式木を直接渡すだけでよく、`Compile`は不要です。
5. 発展:複数フィールドソートと昇順・降順のサポート
5.1 昇順・降順のサポート
ヘルパーメソッドを拡張し、昇順(OrderBy)または降順(OrderByDescending)を指定できるようにします。
public static IOrderedQueryable<T> OrderByDynamic<T>(
this IQueryable<T> source,
string propertyName,
bool descending = false)
{
var param = Expression.Parameter(typeof(T), "x");
var property = Expression.Property(param, propertyName);
var lambda = Expression.Lambda(property, param);
string methodName = descending ? "OrderByDescending" : "OrderBy";
var result = typeof(Queryable).GetMethods()
.First(m => m.Name == methodName && m.GetParameters().Length == 2)
.MakeGenericMethod(typeof(T), property.Type)
.Invoke(null, new object[] { source, lambda });
return (IOrderedQueryable<T>)result!;
}
**使用例**(EF Core):
var query = dbContext.Items.OrderByDynamic("Cost", descending: true);
5.2 複数フィールドソートのサポート(ThenBy)
`IOrderedQueryable`を返し、その後`ThenByDynamic`メソッドを呼び出し続けることができます。ロジックは似ていますが、`methodName`を`"ThenBy"`または`"ThenByDescending"`に変更するだけです。
6. 注意点と一般的な落とし穴
6.1 プロパティ名の大文字・小文字と存在確認
- `Expression.Property(param, propertyName)`は大文字・小文字を区別し、プロパティが型に存在しない場合は`ArgumentException`をスローします。
- 呼び出し前にリフレクションを使用してプロパティの存在を確認するか、例外をキャッチしてユーザーフレンドリーなメッセージを提供することをお勧めします。
6.2 null参照とnull許容型
- ソートフィールドが参照型(`string`など)の場合、その値は`null`になる可能性があります。ソート時、`null`は昇順では先頭に、降順では末尾に配置されます。これは通常のLINQと同じ動作です。
- null許容値型(`int?`)の場合も同様で、式木が自動的に処理します。
6.3 パフォーマンスに関する考慮事項
- **メモリソート**:`Compile()`の呼び出しには毎回オーバーヘッドがあるため、コンパイル済みのデリゲートをキャッシュする(例:`Dictionary>`を使用)ことをお勧めします。
- **データベースソート**:EF Coreに式木を直接渡すと、SQLの`ORDER BY`句に変換され、パフォーマンスは良好です。
6.4 型変換の問題
元のコードが返す`Expression>`は、`string`型のプロパティにのみ使用できます。`int`や`decimal`などのプロパティでソートする場合は、ラムダの戻り型を動的に指定する必要があります。より一般的な方法はラムダが`object`を返すようにすること(上記の例の`CreateSortExpression`参照)ですが、これによりボクシングが発生します。`IQueryable`の場合、非ジェネリックの`LambdaExpression`を使用し、リフレクション経由で`OrderBy`メソッドを呼び出す(5.1節参照)方が良い方法です。
7. まとめ
式木を使用して動的にソートラムダを構築することは、C#で柔軟なソートを実現するための古典的な技術です:
- **主要な手順**:パラメータ → プロパティアクセス → ラムダ → ソートメソッドへの渡し
- **適用シーン**:Web APIの動的ソート、レポートツール、汎用クエリコンポーネント
- **2つの環境**:
- `IEnumerable`:`.Compile()`でデリゲートを取得する必要あり
- `IQueryable`:式木を直接使用し、LINQプロバイダー(EF Coreなど)に変換させる
この技術を習得すれば、動的ソートだけでなく、`Where`や`Select`などの複雑なクエリを動的に構築でき、コードの再利用性と柔軟性を大幅に向上させることができます。