2012-06-22 14 views
10

Estoy escribiendo una capa de datos para una parte de nuestro sistema que registra información sobre trabajos automatizados que se ejecutan todos los días: nombre del trabajo , cuánto tiempo funcionó, cuál fue el resultado, etc.Uso de LINQ ExpressionVisitor para reemplazar parámetros primitivos con referencias de propiedad en una expresión lambda

Estoy hablando con la base de datos utilizando Entity Framework, pero trato de mantener esos detalles ocultos de los módulos de nivel superior y no quiero que la entidad se objeta a sí misma para estar expuesta.

Sin embargo, me gustaría que mi interfaz sea muy flexible en los criterios que utiliza para buscar información de trabajo. Por ejemplo, una interfaz de usuario debería permitir al usuario ejecutar consultas complejas como "darme todos los trabajos llamados 'hola' que se ejecutaron entre las 10:00 a.m. y las 11:00 a.m. que fallaron". Obviamente, esto parece un trabajo para árboles Expression construidos dinámicamente.

Así que lo que me gustaría que mi capa de datos (repositorio) para ser capaz de hacer es aceptar expresiones LINQ de tipo Expression<Func<string, DateTime, ResultCode, long, bool>> (expresión lambda) y luego detrás de las escenas que se convierten en una expresión lambda que mi marco de la entidad ObjectContext puede utilizar como un filtro dentro de una cláusula Where().

En pocas palabras, estoy tratando de convertir una expresión lambda del tipo Expression<Func<string, DateTime, ResultCode, long, bool>> a Expression<Func<svc_JobAudit, bool>>, donde svc_JobAudit es el objeto de datos de Entity Framework que corresponde a la mesa donde se almacena la información del trabajo. (Los cuatro parámetros en el primer delegado corresponden al nombre del trabajo, cuándo se ejecutó, el resultado y cuánto tiempo llevó en MS, respectivamente)

Estaba haciendo un buen progreso utilizando la clase ExpressionVisitor hasta que toco una pared de ladrillo y recibió una InvalidOperationException con este mensaje de error:

cuando se llama desde 'VisitLambda', la reescritura de un nodo de tipo 'System.Linq.Expressions.ParameterExpression' debe devolver un valor no nulo de la el mismo tipo. Alternativamente, anule 'VisitLambda' y cámbielo para no visitar los niños de este tipo.

Estoy completamente desconcertado. ¿Por qué diablos no me permitirá convertir los nodos de expresión que los parámetros de referencia a los nodos que hacen referencia a las propiedades? ¿Hay alguna otra manera de hacerlo?

Aquí hay un código de ejemplo:

namespace ExpressionTest 
{ 
    class Program 
    { 
     static void Main(string[] args) 
     { 
      Expression<Func<string, DateTime, ResultCode, long, bool>> expression = (myString, myDateTime, myResultCode, myTimeSpan) => myResultCode == ResultCode.Failed && myString == "hello"; 
      var result = ConvertExpression(expression); 
     } 

     private static Expression<Func<svc_JobAudit, bool>> ConvertExpression(Expression<Func<string, DateTime, ResultCode, long, bool>> expression) 
     { 
      var newExpression = Expression.Lambda<Func<svc_JobAudit, bool>>(new ReplaceVisitor().Modify(expression), Expression.Parameter(typeof(svc_JobAudit))); 
      return newExpression; 
     } 
    } 

    class ReplaceVisitor : ExpressionVisitor 
    { 
     public Expression Modify(Expression expression) 
     { 
      return Visit(expression); 
     } 

     protected override Expression VisitParameter(ParameterExpression node) 
     { 
      if (node.Type == typeof(string)) 
      { 
       return Expression.Property(Expression.Parameter(typeof(svc_JobAudit)), "JobName"); 
      } 
      return node; 
     } 
    } 
} 
+1

[Reemplazar parámetro en la expresión lambda] (http://stackoverflow.com/questions/11159697/replace-parameter-in-lambda-expression) –

Respuesta

6

El problema era doble:

  • que fue malentendido cómo visitar el tipo de expresión Lambda. Todavía estaba devolviendo un lambda que coincidía con el antiguo delegado en lugar de devolver un nuevo lambda para que coincida con el nuevo delegado.

  • Necesitaba mantener una referencia a la nueva instancia ParameterExpression, que no estaba haciendo.

El nuevo código es el siguiente (observe como el visitante acepta ahora una referencia a un ParameterExpression que coincida con el objeto de datos de Entity Framework):

class Program 
{ 
    const string conString = @"myDB"; 

    static void Main(string[] args) 
    { 
     Expression<Func<string, DateTime, byte, long, bool>> expression = (jobName, ranAt, resultCode, elapsed) => jobName == "Email Notifications" && resultCode == (byte)ResultCode.Failed; 
     var criteria = ConvertExpression(expression); 

     using (MyDataContext dataContext = new MyDataContext(conString)) 
     { 
      List<svc_JobAudit> jobs = dataContext.svc_JobAudit.Where(criteria).ToList(); 
     } 
    } 

    private static Expression<Func<svc_JobAudit, bool>> ConvertExpression(Expression<Func<string, DateTime, byte, long, bool>> expression) 
    { 
     var jobAuditParameter = Expression.Parameter(typeof(svc_JobAudit), "jobAudit"); 
     var newExpression = Expression.Lambda<Func<svc_JobAudit, bool>>(new ReplaceVisitor().Modify(expression.Body, jobAuditParameter), jobAuditParameter); 
     return newExpression; 
    } 
} 

class ReplaceVisitor : ExpressionVisitor 
{ 
    private ParameterExpression parameter; 

    public Expression Modify(Expression expression, ParameterExpression parameter) 
    { 
     this.parameter = parameter; 
     return Visit(expression); 
    } 

    protected override Expression VisitLambda<T>(Expression<T> node) 
    { 
     return Expression.Lambda<Func<svc_JobAudit, bool>>(Visit(node.Body), Expression.Parameter(typeof(svc_JobAudit))); 
    } 

    protected override Expression VisitParameter(ParameterExpression node) 
    { 
     if (node.Type == typeof(string)) 
     { 
      return Expression.Property(parameter, "JobName"); 
     } 
     else if (node.Type == typeof(DateTime)) 
     { 
      return Expression.Property(parameter, "RanAt"); 
     } 
     else if (node.Type == typeof(byte)) 
     { 
      return Expression.Property(parameter, "Result"); 
     } 
     else if (node.Type == typeof(long)) 
     { 
      return Expression.Property(parameter, "Elapsed"); 
     } 
     throw new InvalidOperationException(); 
    } 
} 
+0

Esto era útil, gracias. –

+0

La anulación de VisitLambda en ExpressionVisitor tiene un problema con las expresiones lambda anidadas. Supongamos (un ejemplo inventado), la expresión que se convertía era: (jobName, ranAt, resultCode, transcurrido) => AuditLogTable.Any (a => a.JobName == jobName && a.Date> = ranAt) ; El sub-lambda "a => a.JobName ..." logrará procesada por VisitLambda y convertida en: svc_JobAudit => a.JobName == jobname && a.Date> = Ranat ... que fallará. En mi caso, acabo de eliminar la anulación (que solo se llama sub-lambdas, y no se llama para la lambda principal de nivel superior) –

1

La respuesta aceptada es 'codificado' para algunos específicos tipos. Aquí hay un reescritor de expresión más general que puede sustituir un parámetro por cualquier otra expresión (lambda, constante, ...). En el caso de una expresión lambda, la firma de la expresión debe cambiar para incorporar los parámetros necesarios para el valor sustituido.

public class ExpressionParameterSubstitute : System.Linq.Expressions.ExpressionVisitor 
{ 
    private readonly ParameterExpression from; 
    private readonly Expression to; 
    public ExpressionParameterSubstitute(ParameterExpression from, Expression to) 
    { 
     this.from = from; 
     this.to = to; 
    } 

    protected override Expression VisitLambda<T>(Expression<T> node) 
    { 
     if (node.Parameters.All(p => p != this.from)) 
      return node; 

     // We need to replace the `from` parameter, but in its place we need the `to` parameter(s) 
     // e.g. F<DateTime,Bool> subst F<Source,DateTime> => F<Source,bool> 
     // e.g. F<DateTime,Bool> subst F<Source1,Source2,DateTime> => F<Source1,Source2,bool> 

     var toLambda = to as LambdaExpression; 
     var substituteParameters = toLambda?.Parameters ?? Enumerable.Empty<ParameterExpression>(); 

     ReadOnlyCollection<ParameterExpression> substitutedParameters 
      = new ReadOnlyCollection<ParameterExpression>(node.Parameters 
       .SelectMany(p => p == this.from ? substituteParameters : Enumerable.Repeat(p, 1)) 
       .ToList()); 

     var updatedBody = this.Visit(node.Body);  // which will convert parameters to 'to' 
     return Expression.Lambda(updatedBody, substitutedParameters); 
    } 

    protected override Expression VisitParameter(ParameterExpression node) 
    { 
     var toLambda = to as LambdaExpression; 
     if (node == from) return toLambda?.Body ?? to; 
     return base.VisitParameter(node); 
    } 
} 
Cuestiones relacionadas