Вызов методов System.Linq.Queryable с использованием типов, разрешенных во время выполнения - PullRequest
5 голосов
/ 26 марта 2011

Я создаю генератор запросов на основе 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,anonymous type, что является фактическим типом выражения properProjection (double-checked)

Мой обходной путь использует отражение.Но с общими аргументами, это настоящий беспорядок:

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 перегрузку.

Ответы [ 2 ]

3 голосов
/ 26 марта 2011

Исправление настолько простое, что болит:

[assembly: InternalsVisibleTo("My.Search.Lib.Assembly")]
1 голос
/ 26 марта 2011

Вот мой тест по запросу.Это относится к базе данных 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());
}
...