Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Calling System.Linq.Queryable methods using types resolved at runtime

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.

like image 256
Diego Mijelshon Avatar asked Oct 12 '22 10:10

Diego Mijelshon


2 Answers

The fix is so simple it hurts:

[assembly: InternalsVisibleTo("My.Search.Lib.Assembly")]
like image 194
Diego Mijelshon Avatar answered Oct 18 '22 00:10

Diego Mijelshon


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());
}
like image 45
7 revs Avatar answered Oct 18 '22 02:10

7 revs