5

I'm building a LINQ-based query generator.

One of the features is being able to specify an arbitrary server-side projection as part of the query definition. For example:

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

Since the user must then be able to sort on the projection properties (as opposed to Customer properties), I recreate the expression as a Func<Customer,anonymous type> instead of 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());

In order to return the projected query, I'd love to be able to do this (which, in fact, works in an almost identical proof of concept):

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

TRoot is the type parameter in SearchDefinition. This results in the following exception:

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 ...

If you look closely, it's inferring the generic parameters incorrectly: Customer,object instead of Customer,anonymous type, which is the actual type of the properProjection expression (double-checked)

My workaround is using reflection. But with generic arguments, it's a real mess:

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

Does anyone know of a better way?


Update: the reason why dynamic fails is that anonymous types are defined as internal. That's why it worked using a proof-of-concept project, where everything was in the same assembly.

I'm cool with that. I'd still like to find a cleaner way to find the right Queryable.Select overload.

4
  • Is the call to ParameterRebinder.ReplaceParameter really necessary? The expression body already has the right type so when the expression is rebuilt, it will have the correct types. My own tests seems to be working here. Commented Mar 25, 2011 at 23:39
  • @JeffM: The call is necessary to replace the parameter from the original lambda expression in the anonymous type initializer, otherwise you'd get variable 'x' of type 'Customer' referenced from scope '', but it is not defined. I should probably create a complete test case, as it also worked for me in a proof of concept project. Commented Mar 25, 2011 at 23:44
  • Oh I forgot that you used a different parameter instance to rebuild your expression. My tests just reuses the existing parameter and body in a new lambda expression (and works). Would doing the same work for you? Commented Mar 25, 2011 at 23:49
  • @JeffM: can you post your test? Remember the projection is defined in a subclass. Commented Mar 25, 2011 at 23:53

2 Answers 2

3

The fix is so simple it hurts:

[assembly: InternalsVisibleTo("My.Search.Lib.Assembly")]
Sign up to request clarification or add additional context in comments.

Comments

1

Here's my test as requested. This on a Northwind database and this works fine for me.

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

Also, dynamic method resolution doesn't really do much for you in this case as there are only two very distinct overloads of Select(). Ultimately you just need to remember that you won't have any static type checking on your results since you don't have any static type information. With that said, this will also work for you (using the above code example):

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

6 Comments

@Jeff I removed the RebindParameter thing and that piece is simpler now, but I still get the same error. I'll try to create a complete repro.
@Jeff: I found why it was working for you and not for me. Check my last update.
@Diego: Ok good. But in general, the dynamic dispatch will work correctly as long as the runtime types can be determined. If any of the variables are declared dynamic within an expression, everything is resolved at runtime.
@JeffM: unfortunately, Cast<dynamic>() in a late-bound context is just like Cast<object>: it does not allow me to add an OrderBy on properties of the projected type (which is the reason why I did the whole Expression.Lambda hack)
@JeffM: See what my solution was :-) If you hadn't marked this as community wiki, I'd still upvote you for the effort, plus getting me in the right path. Thanks!
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.