文字列パラメータによって式を生成したいのですが、次のようなコードがあります。
private Expression<Func<Task, T>> Generate(string orderby)
{
switch (orderby)
{
case "Time":
return t => t.Time;
case "Money":
return t => t.RewardMoney;
default:
return t => t.Id;
}
}
それを呼び出す:
_context.Items.OrderBy(Generate("Money"));
しかし、コンパイルできません! Tをオブジェクトに変更します。
private Expression<Func<Task, object>> Generate(string orderby)
その後、コンパイルできますが、機能しません。
System.NotSupportedException:タイプ 'System.Int32'をタイプ 'System.Object'にキャストできません。 LINQ to Entitiesは、EDMプリミティブまたは列挙型のキャストのみをサポートしています。
reflection と expression-trees を使用すると、Expression<Func<Task, T>>
を返してOrderBy
を呼び出す代わりに、パラメーターを指定してOrderBy
関数を呼び出すことができます。
OrderBy
は拡張メソッドであり、System.Linq.Enumarable
クラスとSystem.Linq.Queryable
クラスの両方に実装されていることに注意してください。最初のものは linq-to-objects 用であり、後者は linq-to-entities 用です。 entity-framework をSQLコマンドに変換するには、クエリの式ツリーが必要です。したがって、Queryable
実装を使用します。
これは、拡張メソッド(コメントとして追加された説明)によって実行できます。
public static IOrderedQueryable<TSource> OrderBy<TSource>(
this IQueryable<TSource> query, string propertyName)
{
var entityType = typeof(TSource);
//Create x=>x.PropName
var propertyInfo = entityType.GetProperty(propertyName);
ParameterExpression arg = Expression.Parameter(entityType, "x");
MemberExpression property = Expression.Property(arg, propertyName);
var selector = Expression.Lambda(property, new ParameterExpression[] { arg });
//Get System.Linq.Queryable.OrderBy() method.
var enumarableType = typeof(System.Linq.Queryable);
var method = enumarableType.GetMethods()
.Where(m => m.Name == "OrderBy" && m.IsGenericMethodDefinition)
.Where(m =>
{
var parameters = m.GetParameters().ToList();
//Put more restriction here to ensure selecting the right overload
return parameters.Count == 2;//overload that has 2 parameters
}).Single();
//The linq's OrderBy<TSource, TKey> has two generic types, which provided here
MethodInfo genericMethod = method
.MakeGenericMethod(entityType, propertyInfo.PropertyType);
/*Call query.OrderBy(selector), with query and selector: x=> x.PropName
Note that we pass the selector as Expression to the method and we don't compile it.
By doing so EF can extract "order by" columns and generate SQL for it.*/
var newQuery = (IOrderedQueryable<TSource>)genericMethod
.Invoke(genericMethod, new object[] { query, selector });
return newQuery;
}
これで、OrderBy
のこのオーバーロードを他のオーバーロードと同じように呼び出すことができます。
例えば:
var cheapestItems = _context.Items.OrderBy("Money").Take(10).ToList();
これは次のように変換されます:
SELECT TOP (10) {coulmn names} FROM [dbo].[Items] AS [Extent1]
ORDER BY [Extent1].[Money] ASC
このアプローチを使用して、OrderBy
およびOrderByDescending
メソッドのすべてのオーバーロードを定義し、string
プロパティセレクターを使用できます。
ジェネリックメソッドでGenerate
メソッドを変換してみることができます。
private Expression<Func<Task, TResult>> Generate<TResult>(string orderby)
{
switch (orderby)
{
case "Time":
return t => t.Time;
case "Money":
return t => t.RewardMoney;
default:
return t => t.Id;
}
}
したがって、このメソッドを呼び出す場合は、順序付けするプロパティのタイプを指定する必要があります。
_context.Items.OrderBy(Generate<decimal>("Money"));
ここで、TResult
はプリミティブ型または列挙型にしかできないことに注意してください。
ジェネリックメソッドを使用します。ラムダ式は厳密に型指定されたデリゲートまたは式にのみ割り当てることができるため、対応するtempを使用する必要があります。次に、このtempをobject
として型指定された変数に割り当てることができます。最後に、結果タイプにキャストすることで結果を返すことができます。
public Expression<Func<Task, TResult>> Generate<TResult>(string orderby)
{
object result;
switch (orderby) {
case "Time":
Expression<Func<Task, DateTime>> temp1 = t => t.Time;
result = temp1;
break;
case "Money":
Expression<Func<Task, decimal>> temp2 = t => t.RewardMoney;
result = temp2;
break;
default:
Expression<Func<Task, int>> temp3 = t => t.Id;
result = temp3;
break;
}
return (Expression<Func<Task, TResult>>)result;
}