在运行时解析类型并调用System.Linq.Queryable方法

5

我正在构建一个基于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<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,匿名类型,后者才是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 真的有必要吗?表达式主体已经具有正确的类型,因此当重建表达式时,它将具有正确的类型。我的测试似乎在这里起作用。 - Jeff Mercado
@JeffM:调用是必要的,以替换匿名类型初始化程序中原始lambda表达式中的参数,否则您将得到“变量'x'的类型为'Customer',从范围''引用,但未定义”的错误。我应该创建一个完整的测试用例,因为它在我的概念验证项目中也起作用。 - Diego Mijelshon
哦,我忘记了你在重新构建表达式时使用了一个不同的参数实例。我的测试只是在新的 lambda 表达式中重复使用现有的参数和主体(而且有效)。对你来说,这样做是否也可以? - Jeff Mercado
@JeffM:你能发一下你的测试吗?记得投影是在子类中定义的。 - Diego Mijelshon
2个回答

3
修复非常简单:
[assembly: InternalsVisibleTo("My.Search.Lib.Assembly")]

1

这是您要求的测试。这是在Northwind数据库上进行的,对我来说运行良好。

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这个东西,现在那一部分更简单了,但我仍然遇到了相同的错误。我会尝试创建一个完整的复现。 - Diego Mijelshon
@Jeff:我找到了为什么它对你有效而对我无效的原因。请查看我的最新更新。 - Diego Mijelshon
@Diego:好的,不错。但是一般来说,只要运行时类型可以确定,动态分派就能正常工作。如果任何变量在表达式中被声明为动态类型,那么所有内容都将在运行时解析。 - Jeff Mercado
@JeffM:不幸的是,在晚期绑定上下文中使用Cast<dynamic>()就像Cast<object>一样:它不允许我在投影类型的属性上添加OrderBy(这就是为什么我要做整个Expression.Lambda hack的原因)。 - Diego Mijelshon
@JeffM: 看看我的解决方案是什么 :-) 如果你没有将这个标记为社区共享,我还会投票支持你的努力,并且让我走上了正确的路径。谢谢! - Diego Mijelshon
@Diego:虽然我不会因此获得声望,但你仍然可以为它投赞成票。这没关系,我并不是想把它作为答案,因为我不知道错误指的是什么。;) - Jeff Mercado

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接