2011-03-25 108 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

相关问题