2011-03-25 66 views
5

我在構建一個基於LINQ的查詢生成器。使用在運行時解析的類型調用System.Linq.Queryable方法

其中一項功能是能夠將任意的服務器端投影指定爲查詢定義的一部分。例如:

class CustomerSearch : SearchDefinition<Customer> 
{ 
    protected override Expression<Func<Customer, object>> GetProjection() 
    { 
     return x => new 
        { 
         Name = x.Name, 
         Agent = x.Agent.Code 
         Sales = x.Orders.Sum(o => o.Amount) 
        }; 
    } 
} 

由於用戶必須隨後能夠排序的投影特性(相對於客戶性能),I重新創建表達的Func<Customer,anonymous type>代替Func<Customer, object>

//This is a method on SearchDefinition 
IQueryable Transform(IQueryable source) 
{ 
    var projection = GetProjection(); 
    var properProjection = Expression.Lambda(projection.Body, 
              projection.Parameters.Single()); 

在爲了返回預計的查詢,我希望能夠做到這一點(事實上,它的概念證明幾乎相同):

return Queryable.Select((IQueryable<TRoot>)source, (dynamic)properProjection); 

TRoot是SearchDefinition中的類型參數。這將導致以下異常:

Microsoft.CSharp.RuntimeBinder.RuntimeBinderException: 
The best overloaded method match for 
'System.Linq.Queryable.Select<Customer,object>(System.Linq.IQueryable<Customer>, 
System.Linq.Expressions.Expression<System.Func<Customer,object>>)' 
has some invalid arguments 
    at CallSite.Target(Closure , CallSite , Type , IQueryable`1 , Object) 
    at System.Dynamic.UpdateDelegates.UpdateAndExecute3[T0,T1,T2,TRet] 
     (CallSite site, T0 arg0, T1 arg1, T2 arg2) 
    at SearchDefinition`1.Transform(IQueryable source) in ... 

如果你仔細觀察,它錯誤地推斷泛型參數:Customer,object代替Customer,anonymous type,這是實際類型的properProjection表達(雙重檢查)

我解決方法是使用反射。但隨着通用的參數,它是一個真正的混亂:

var genericSelectMethod = typeof(Queryable).GetMethods().Single(
    x => x.Name == "Select" && 
     x.GetParameters()[1].ParameterType.GetGenericArguments()[0] 
      .GetGenericArguments().Length == 2); 
var selectMethod = genericSelectMethod.MakeGenericMethod(source.ElementType, 
        projectionBody.Type); 
return (IQueryable)selectMethod.Invoke(null, new object[]{ source, projection }); 

有誰知道一個更好的辦法?


更新:爲什麼dynamic失敗的原因是匿名類型被定義爲internal。這就是爲什麼它使用概念驗證項目進行工作,其中所有內容都在同一個程序集中。

我很酷。我仍然想找到一個更清晰的方式來找到正確的Queryable.Select過載。

+0

是調用'ParameterRebinder.ReplaceParameter'真的有必要?表達式體已經有了正確的類型,所以當表達式被重建時,它將具有正確的類型。我自己的測試似乎在這裏工作。 – 2011-03-25 23:39:43

+0

@JeffM:在匿名類型初始化程序中調用是必要的,以取代原始lambda表達式的參數,否則您會從範圍''引用類型爲'Customer'的變量'x',但未定義' 。我應該創建一個完整的測試用例,因爲它也適用於我的概念驗證項目。 – 2011-03-25 23:44:42

+0

哦,我忘了你使用了一個不同的參數實例來重建你的表達式。我的測試只是重新使用新的lambda表達式(和工程)現有的參數和正文。會爲你做同樣的工作嗎? – 2011-03-25 23:49:17

回答

3

的修補程序太簡單了,傷害:

[assembly: InternalsVisibleTo("My.Search.Lib.Assembly")] 
1

這是我的測試要求。這在羅斯文數據庫上,這對我來說工作得很好。

static void Main(string[] args) 
{ 
    var dc = new NorthwindDataContext(); 
    var source = dc.Categories; 
    Expression<Func<Category, object>> expr = 
     c => new 
     { 
      c.CategoryID, 
      c.CategoryName, 
     }; 
    var oldParameter = expr.Parameters.Single(); 
    var parameter = Expression.Parameter(oldParameter.Type, oldParameter.Name); 
    var body = expr.Body; 
    body = RebindParameter(body, oldParameter, parameter); 

    Console.WriteLine("Parameter Type: {0}", parameter.Type); 
    Console.WriteLine("Body Type: {0}", body.Type); 

    var newExpr = Expression.Lambda(body, parameter); 
    Console.WriteLine("Old Expression Type: {0}", expr.Type); 
    Console.WriteLine("New Expression Type: {0}", newExpr.Type); 

    var query = Queryable.Select(source, (dynamic)newExpr); 
    Console.WriteLine(query); 

    foreach (var item in query) 
    { 
     Console.WriteLine(item); 
     Console.WriteLine("\t{0}", item.CategoryID.GetType()); 
     Console.WriteLine("\t{0}", item.CategoryName.GetType()); 
    } 

    Console.Write("Press any key to continue . . . "); 
    Console.ReadKey(true); 
    Console.WriteLine(); 
} 

static Expression RebindParameter(Expression expr, ParameterExpression oldParam, ParameterExpression newParam) 
{ 
    switch (expr.NodeType) 
    { 
    case ExpressionType.Parameter: 
     var parameterExpression = expr as ParameterExpression; 
     return (parameterExpression.Name == oldParam.Name) 
      ? newParam 
      : parameterExpression; 
    case ExpressionType.MemberAccess: 
     var memberExpression = expr as MemberExpression; 
     return memberExpression.Update(
      RebindParameter(memberExpression.Expression, oldParam, newParam)); 
    case ExpressionType.AndAlso: 
    case ExpressionType.OrElse: 
    case ExpressionType.Equal: 
    case ExpressionType.NotEqual: 
    case ExpressionType.LessThan: 
    case ExpressionType.LessThanOrEqual: 
    case ExpressionType.GreaterThan: 
    case ExpressionType.GreaterThanOrEqual: 
     var binaryExpression = expr as BinaryExpression; 
     return binaryExpression.Update(
      RebindParameter(binaryExpression.Left, oldParam, newParam), 
      binaryExpression.Conversion, 
      RebindParameter(binaryExpression.Right, oldParam, newParam)); 
    case ExpressionType.New: 
     var newExpression = expr as NewExpression; 
     return newExpression.Update(
      newExpression.Arguments 
         .Select(arg => RebindParameter(arg, oldParam, newParam))); 
    case ExpressionType.Call: 
     var methodCallExpression = expr as MethodCallExpression; 
     return methodCallExpression.Update(
      RebindParameter(methodCallExpression.Object, oldParam, newParam), 
      methodCallExpression.Arguments 
           .Select(arg => RebindParameter(arg, oldParam, newParam))); 
    default: 
     return expr; 
    } 
} 

此外,動態方法解析並沒有真正做多的,你在這種情況下,因爲只有兩個Select()非常明顯的過載。最終,您只需要記住,由於您沒有任何靜態類型信息,因此您不會對結果進行任何靜態類型檢查。隨着中說,這也會爲你工作(使用上面的代碼示例):

var query = Queryable.Select(source, expr).Cast<dynamic>(); 
Console.WriteLine(query); 

foreach (var item in query) 
{ 
    Console.WriteLine(item); 
    Console.WriteLine("\t{0}", item.CategoryID.GetType()); 
    Console.WriteLine("\t{0}", item.CategoryName.GetType()); 
} 
+0

@Jeff我刪除了RebindParameter的東西,現在這件作品更簡單了,但我仍然得到相同的錯誤。我會嘗試創建一個完整的repro。 – 2011-03-26 01:01:36

+0

@Jeff:我發現它爲什麼會爲你工作,而不是爲我工作。檢查我的最新更新。 – 2011-03-26 01:57:29

+0

@Diego:好吧。但通常情況下,只要能夠確定運行時類型,動態調度就能正常工作。如果任何變量在表達式中聲明爲動態,則在運行時解決所有問題。 – 2011-03-26 02:02:28

相關問題