2011-03-25 12 views
5

Estoy construyendo un generador de consultas basado en LINQ.Métodos de Calling System.Linq.Queryable using types resolved at runtime

Una de las funciones es poder especificar una proyección arbitraria del lado del servidor como parte de la definición de la consulta. Por ejemplo:

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

Dado que el usuario debe entonces ser capaz de ordenar en las propiedades de proyección (en oposición a las propiedades de clientes), I recrear la expresión como un Func<Customer,anonymous type> en lugar de 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()); 

En para devolver la consulta proyectado, me encantaría ser capaz de hacer esto (que, de hecho, funciona en una prueba de concepto casi idéntica):

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

TRoot es el parámetro de tipo en SearchDefinition. Esto da lugar a la siguiente excepción:

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

Si se mira de cerca, es inferir los parámetros genéricos de forma incorrecta: Customer,object en lugar de Customer,anonymous type, que es el tipo real de la expresión properProjection (doble marcado)

Mi solución alternativa es usar la reflexión. Pero con argumentos genéricos, que es un verdadero desastre:

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

¿Alguien sabe de una mejor manera?


actualización: la razón por la dynamic falla es que los tipos anónimos se definen como internal. Es por eso que funcionó usando un proyecto de prueba de concepto, donde todo estaba en el mismo ensamblaje.

Estoy tranquilo con eso. Todavía me gustaría encontrar una manera más clara de encontrar la sobrecarga correcta Queryable.Select.

+0

es la llamada a 'ParameterRebinder.ReplaceParameter' realmente necesario ? El cuerpo de expresión ya tiene el tipo correcto, por lo que cuando se reconstruye la expresión, tendrá los tipos correctos. Mis propias pruebas parecen estar funcionando aquí. –

+0

@JeffM: La llamada es necesaria para reemplazar el parámetro de la expresión lambda original en el inicializador de tipo anónimo; de lo contrario, obtendría 'variable 'x' del tipo 'Cliente' al que se hace referencia desde el alcance '', pero no está definido' .Probablemente debería crear un caso de prueba completo, ya que también funcionó para mí en un proyecto de prueba de concepto. –

+0

Oh, olvidé que usaste una instancia de parámetro diferente para reconstruir tu expresión. Mis pruebas simplemente reutilizan el parámetro y el cuerpo existentes en una nueva expresión lambda (y funciona). ¿Te estaría haciendo el mismo trabajo? –

Respuesta

3

La solución es tan simple que duele:

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

Aquí está mi prueba según lo solicitado. Esto en una base de datos de Northwind y esto funciona bien para mí.

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

Además, la resolución de métodos dinámicos en realidad no hacen mucho para usted en este caso, ya que hay sólo dos sobrecargas muy distintas de Select(). En última instancia, solo debe recordar que no tendrá ninguna comprobación de tipo estático de sus resultados ya que no tiene información de tipo estático. Con eso dicho, esto también funciona para usted (utilizando el ejemplo de código anterior):

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

@Jeff Quité la cosa RebindParameter y esa pieza es más simple ahora, pero sigo teniendo el mismo error. Trataré de crear una reproducción completa. –

+0

@Jeff: He descubierto por qué funcionaba para usted y no para mí. Verifique mi última actualización –

+0

@Diego: Ok bien. Pero, en general, el despacho dinámico funcionará correctamente siempre que se puedan determinar los tipos de tiempo de ejecución. Si alguna de las variables se declara dinámica dentro de una expresión, todo se resuelve en tiempo de ejecución. –