【问题标题】:Calling System.Linq.Queryable methods using types resolved at runtime使用在运行时解析的类型调用 System.Linq.Queryable 方法
【发布时间】:2011-07-23 06:35:46
【问题描述】:

我正在构建一个基于 LINQ 的查询生成器。

其中一项功能是能够将任意服务器端投影指定为查询定义的一部分。例如:

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)
                    };
    }
}

由于用户必须能够对投影属性(而不是客户属性)进行排序,因此我将表达式重新创建为 Func&lt;Customer,anonymous type&gt; 而不是 Func&lt;Customer, object&gt;:

//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 重载。

【问题讨论】:

  • 真的有必要拨打ParameterRebinder.ReplaceParameter吗?表达式主体已经具有正确的类型,因此在重建表达式时,它将具有正确的类型。我自己的测试似乎在这里工作。
  • @JeffM:调用是替换匿名类型初始值设定项中原始 lambda 表达式的参数所必需的,否则您会得到 variable 'x' of type 'Customer' referenced from scope '', but it is not defined。我可能应该创建一个完整的测试用例,因为它在概念验证项目中也适用于我。
  • 哦,我忘记了您使用不同的参数实例来重建您的表达式。我的测试只是在新的 lambda 表达式中重用现有参数和主体(并且有效)。会为你做同样的工作吗?
  • @JeffM:你能发布你的测试吗?请记住,投影是在子类中定义的。

标签: linq generics reflection dynamic c#-4.0


【解决方案1】:

解决方法很简单,让人心痛:

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

【讨论】:

    【解决方案2】:

    这是我要求的测试。这在罗斯文数据库上,这对我来说很好。

    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());
    }
    

    【讨论】:

    • @Jeff 我删除了 RebindParameter 东西,现在这块更简单了,但我仍然遇到同样的错误。我会尝试创建一个完整的重现。
    • @Jeff:我找到了为什么它对你有用,而不是对我有用。检查我的最新更新。
    • @Diego:好的。但总的来说,只要可以确定运行时类型,动态调度就会正常工作。如果任何变量在表达式中被声明为动态的,那么一切都会在运行时解析。
    • @JeffM:不幸的是,后期绑定上下文中的Cast&lt;dynamic&gt;() 就像Cast&lt;object&gt;:它不允许我在投影类型的属性上添加OrderBy(这是我做整个 Expression.Lambda hack 的原因)
    • @JeffM:看看我的解决方案是什么 :-) 如果您没有将此标记为社区 wiki,我仍然会为您的努力投票,并让我走上正确的道路。谢谢!
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-11-08
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多