web-dev-qa-db-ja.com

文字列でEF orderby式を生成する

文字列パラメータによって式を生成したいのですが、次のようなコードがあります。

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プリミティブまたは列挙型のキャストのみをサポートしています。

17
yubaolee

reflectionexpression-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プロパティセレクターを使用できます。

40
Taher Rahgooy

ジェネリックメソッドで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はプリミティブ型または列挙型にしかできないことに注意してください。

4
octavioccl

ジェネリックメソッドを使用します。ラムダ式は厳密に型指定されたデリゲートまたは式にのみ割り当てることができるため、対応する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;
}