プロパティ名版OrderByを拡張メソッド化
前回、LINQ to EntiiesのOrderByを項目名で使いたい、という不思議な相談を受けて、調べて見たらできたよ、って感じの記事を書きました。
実はこの話にはもう少し続きがあります。
まぁ、そんなにややこしい話でもないんだけど。
なにやら前回作ったコードを少し汎用化して、使い回せるようにしたいんだって。
そんなわけで、拡張メソッドにして使いやすくしてみました。
まずは前回作ったコードを再掲します。
using EfSampleApp.Models;
using System.Linq;
using System.Linq.Expressions;
static IEnumerable<User> Select(SampleDbContext context, int offset, int count, string orderName, string orderDirection)
{
var entityType = typeof(User);
var query = context.Users
.Where(user => !user.DeleteFlag);
// 「user => user.Age」という式ツリーを生成
var arg = Expression.Parameter(entityType, "user");
var property = Expression.Property(arg, orderName);
var selector = Expression.Lambda(property, new ParameterExpression[] { arg });
// System.Linq.Queryable.OrderBy または OrderByDescending メソッドを
// リフレクションで取得
var direction = orderDirection != "asc" ? "OrderByDescending" : "OrderBy";
var propertyInfo = entityType.GetProperty(orderName);
var enumarableType = typeof(Queryable);
var method = enumarableType.GetMethods()
.Where(m => m.Name == direction && m.IsGenericMethodDefinition)
.Where(m =>
{
var parameters = m.GetParameters().ToList();
return parameters.Count == 2;
}).Single();
var genericMethod = method
.MakeGenericMethod(entityType, propertyInfo.PropertyType);
// OrderBy または OrderByDescending メソッドを実行
var newQuery = (IOrderedQueryable<User>)genericMethod
.Invoke(genericMethod, new object[] { query, selector });
return newQuery.Skip(offset).Take(count);
}
var dbContext = new SampleDbContext();
var query = Select(dbContext, 3, 3, nameof(User.Age), "asc");
foreach (var user in query)
{
Console.WriteLine($"id[{user.Id}],name[{user.Name}],age[{user.Age}]");
}
Console.WriteLine("===================");
ここからOrderByを作っている部分を抜き出してジェネリクスを使って抽象化します。
さらに本家(System.Linq.dll)に倣って4つの拡張メソッドにしました。
using System.Linq;
using System.Linq.Expressions;
namespace EfSampleApp.Models;
public static class QueryableExtensions
{
public static IOrderedQueryable<T> OrderBy<T>(this IQueryable<T> query, string propertyName)
{
return OrderByCustom(query, propertyName, "OrderBy");
}
public static IOrderedQueryable<T> OrderByDescending<T>(this IQueryable<T> query, string propertyName)
{
return OrderByCustom(query, propertyName, "OrderByDescending");
}
public static IOrderedQueryable<T> ThenBy<T>(this IOrderedQueryable<T> query, string propertyName)
{
return OrderByCustom(query, propertyName, "ThenBy");
}
public static IOrderedQueryable<T> ThenByDescending<T>(this IOrderedQueryable<T> query, string propertyName)
{
return OrderByCustom(query, propertyName, "ThenByDescending");
}
private static IOrderedQueryable<T> OrderByCustom<T>(IQueryable<T> query, string propertyName, string orderByName)
{
var entityType = typeof(T);
// 「x => x.{propertyName}」という式ツリーを生成
var arg = Expression.Parameter(entityType, "x");
var property = Expression.Property(arg, propertyName);
var selector = Expression.Lambda(property, new ParameterExpression[] { arg });
// System.Linq.Queryable.OrderBy メソッドをリフレクションで取得
var propertyInfo = entityType.GetProperty(propertyName);
var enumarableType = typeof(Queryable);
var method = enumarableType.GetMethods()
.Where(m => m.Name == orderByName && m.IsGenericMethodDefinition)
.Where(m =>
{
var parameters = m.GetParameters().ToList();
return parameters.Count == 2;
}).Single();
var genericMethod = method
.MakeGenericMethod(entityType, propertyInfo.PropertyType);
// OrderBy メソッドを実行
return (IOrderedQueryable<T>)genericMethod
.Invoke(genericMethod, new object[] { query, selector });
}
}
ジェネリクスを使って抽象化したものはOrderByCustom
というメソッドになりました。User
クラス専用だったものをT
という抽象化された型定義に変更することで汎用化しています。
こうすることで、例えば匿名クラスのOrderByなんかも可能になります。
それから、作った拡張メソッドは下記の4つです。
- OrderBy
- OrderByDescending
- ThenBy
- ThenByDescending
前回のコードではThenBy
とThenByDescending
はありませんでしたが、汎用化するなら必要だろうということで作成しました。
ちなみにこのQueryableExtensions
クラスはModelsディレクトリの下に作成しています。
この拡張メソッドを使うように前回のコードを変更してみます。
ついでに複数の並び順を指定できるようにしてみました。
using EfSampleApp.Models;
static IEnumerable<User> Select(SampleDbContext context, int offset, int count, (string name, string direction)[] orders)
{
var query = context.Users
.Where(user => !user.DeleteFlag);
// order by句を作成
if (orders.Length > 0)
{
var orderedQuery = orders[0].direction == "asc" ?
query.OrderBy(orders[0].name) :
query.OrderByDescending(orders[0].name);
for (var i = 1; i < orders.Length; i++)
{
orderedQuery = orders[i].direction == "asc" ?
orderedQuery.ThenBy(orders[i].name) :
orderedQuery.ThenByDescending(orders[i].name);
}
query = orderedQuery;
}
return query.Skip(offset).Take(count);
}
var dbContext = new SampleDbContext();
var query = Select(dbContext, 3, 3, new[] { (nameof(User.Age), "desc"), (nameof(User.Name), "asc") }); // 変更
foreach (var user in query)
{
Console.WriteLine($"id[{user.Id}],name[{user.Name}],age[{user.Age}]");
}
Console.WriteLine("===================");
IEnumerable版が欲しい?
しばらくしてまた相談を受けました。
LINQ to Objectsでも使いたいんだけど、うまくいかなくて・・・。なんとかなる?
そんなわけで、こんな感じのコードを見せてくれました。
using System.Linq;
using System.Linq.Expressions;
namespace EfSampleApp.Models;
public static class EnumerableExtensions
{
public static IOrderedEnumerable<T> OrderBy<T>(this IEnumerable<T> query, string propertyName)
{
return OrderByCustom(query, propertyName, "OrderBy");
}
public static IOrderedEnumerable<T> OrderByDescending<T>(this IEnumerable<T> query, string propertyName)
{
return OrderByCustom(query, propertyName, "OrderByDescending");
}
public static IOrderedEnumerable<T> ThenBy<T>(this IOrderedEnumerable<T> query, string propertyName)
{
return OrderByCustom(query, propertyName, "ThenBy");
}
public static IOrderedEnumerable<T> ThenByDescending<T>(this IOrderedEnumerable<T> query, string propertyName)
{
return OrderByCustom(query, propertyName, "ThenByDescending");
}
private static IOrderedEnumerable<T> OrderByCustom<T>(IEnumerable<T> query, string propertyName, string orderByName)
{
var entityType = typeof(T);
// 「x => x.{propertyName}」という式ツリーを生成
var arg = Expression.Parameter(entityType, "x");
var property = Expression.Property(arg, propertyName);
var selector = Expression.Lambda(property, new ParameterExpression[] { arg });
// System.Linq.Enumerable.OrderBy メソッドをリフレクションで取得
var propertyInfo = entityType.GetProperty(propertyName);
var enumarableType = typeof(Enumerable);
var method = enumarableType.GetMethods()
.Where(m => m.Name == orderByName && m.IsGenericMethodDefinition)
.Where(m =>
{
var parameters = m.GetParameters().ToList();
return parameters.Count == 2;
}).Single();
var genericMethod = method
.MakeGenericMethod(entityType, propertyInfo.PropertyType);
// OrderBy メソッドを実行
return (IOrderedEnumerable<T>)genericMethod
.Invoke(genericMethod, new object[] { query, selector });
}
}
先の拡張メソッドの実装をコピーして、IEnumerable版を作ってみたってことらしいんですが、実行してみると例外になります。
Unhandled exception. System.ArgumentException: Object of type ‘System.Linq.Expressions.Expression1`1[System.Func2[EfSampleApp.Models.User,System.Nullable`1[System.Int32]]]’ cannot be converted to type ‘System.Func`2[EfSampleApp.Models.User,System.Nullable`1[System.Int32]]’.
実はOrderByメソッドはIQueryableとIEnumerableでは引数の型が違います。
public static System.Linq.IOrderedQueryable<TSource> OrderBy<TSource,TKey> (
this System.Linq.IQueryable<TSource> source,
System.Linq.Expressions.Expression<Func<TSource,TKey>> keySelector);
public static System.Linq.IOrderedEnumerable<TSource> OrderBy<TSource,TKey> (
this System.Collections.Generic.IEnumerable<TSource> source,
Func<TSource,TKey> keySelector);
IQueryableのOrderByは引数にExpression
を指定しますが、IEnumerableではFunc
になっています。
普通に使う時はどちらの引数もラムダ式になるので、違いを意識することはあまりないですね。
しかし、今回はIQyeryable版で直接Expressionを使っているので、IEnumerable版にはそのまま移行できません。
ExpressionからFuncへ変換する必要があるわけです。
実はExpressionにはCompile
というメソッドがあって、これでFuncへ変換できます。
IEnumerable版のソースでいえば下記の部分を編集すれば動くようになります。
// OrderBy メソッドを実行
return (IOrderedEnumerable<T>)genericMethod
.Invoke(genericMethod, new object[] { query, selector.Compile() }); // selector → selector.Compile()
これでIEnumerableでも動作するようになりました。
・・・???
あれ?
LINQ to Entities じゃなくて LINQ to Objects なはずなのに、なんでこんなことをしないといけないんだ?
考えてみれば、最初からExpressionなんて必要ないはずじゃない?
IEnumerable版拡張メソッドの最終形
ということで、最終的にこういう実装になりました。
using System.Linq;
namespace EfSampleApp.Models;
public static class EnumerableExtensions
{
public static IOrderedEnumerable<T> OrderBy<T>(this IEnumerable<T> query, string propertyName)
{
return query.OrderBy(x => typeof(T).GetProperty(propertyName).GetValue(x));
}
public static IOrderedEnumerable<T> OrderByDescending<T>(this IEnumerable<T> query, string propertyName)
{
return query.OrderByDescending(x => typeof(T).GetProperty(propertyName).GetValue(x));
}
public static IOrderedEnumerable<T> ThenBy<T>(this IOrderedEnumerable<T> query, string propertyName)
{
return query.ThenBy(x => typeof(T).GetProperty(propertyName).GetValue(x));
}
public static IOrderedEnumerable<T> ThenByDescending<T>(this IOrderedEnumerable<T> query, string propertyName)
{
return query.ThenByDescending(x => typeof(T).GetProperty(propertyName).GetValue(x));
}
}
なんだか一周回って前回の最初の実装に戻ったような感じです。
でもこれで動くならあえてExpressionにする必要はないですね。
シンプルで分かりやすいというのは正義ですww
まとめ
今回は前回のおまけって感じの軽い記事になりました。
前回作ったIQueryable.OrderByのカスタマイズ実装を扱いやすくするだけって感じの内容でしたね。
ただ、今回作ったQueryableExtensions
クラスとEnumerableExtensions
クラスはコピペするだけで、他のプロジェクトでも使えます。
EntityFrameworkを使うプロジェクトで同様の要件があったら、参考にしてみてください。
ぜひ前回の記事も覗いてみてくださいね。
コメント