IQueryable: Crear dinámicamente un filtro OR

c# expression-trees iqueryable linq linq-to-entities

Pregunta

Tengo un conjunto de criterios de búsqueda en esta forma:

 member  |  value  |  operator
 --------+---------+---------
 height  |   10    |    >
 height  |    2    |    < 
 name    |  Carl   |   ==

Y quiero consultar todos los objetos que coincidan con cualquiera de estos criterios.

En este momento, lo estoy haciendo por:

  • Construyendo una expresión para cada uno de los criterios.
  • concatenando cada expresión usando una expresión 'OR'
  • construyendo una expresión lambda que contiene la expresión concatenada
  • pasando la expresión lambda al método IQueryable <>. Where ()

¿Conoces una forma más fácil de filtrar dinámicamente una colección IQueryable usando OR consecutivo?


BONIFICACIÓN Nuestra solución:

Basado en la solución de IlyaBuiluk @ CodeProject

// The structure used by the new extension method
public struct SearchCriteria
{
    public string Column;
    public object Value;
    public WhereOperation Operation;
}

// How to convert the rules structure to the search criteria structure
var searchCriterias = grid.Where.rules.Select(Rule => new SearchCriteria
  {
      Column = Rule.field,
      Operation =
          (WhereOperation)
          StringEnum.Parse(
              typeof (WhereOperation),
              Rule.op),
      Value = Rule.data
  }).ToArray();


// Usage:
query = query.WhereOr(searchCriterias);


// Implementation
public static IQueryable<T> WhereOr<T>( this IQueryable<T> Query, SearchCriteria [ ] Criterias )
{
    if( Criterias.Count( ) == 0 )
        return Query;

    LambdaExpression lambda;
    Expression resultCondition = null;

    // Create a member expression pointing to given column
    ParameterExpression parameter = Expression.Parameter( Query.ElementType, "p" );

    foreach( var searchCriteria in Criterias )
    {
        if( string.IsNullOrEmpty( searchCriteria.Column ) )
            continue;

        MemberExpression memberAccess = null;
        foreach( var property in searchCriteria.Column.Split( '.' ) )
            memberAccess = MemberExpression.Property
                ( memberAccess ?? ( parameter as Expression ), property );

        // Change the type of the parameter 'value'. it is necessary for comparisons (specially for booleans)
        ConstantExpression filter = Expression.Constant
            (
                Convert.ChangeType( searchCriteria.Value, memberAccess.Type )
            );

        //switch operation
        Expression condition = null;
        switch( searchCriteria.Operation )
        {
            //equal ==
            case WhereOperation.Equal:
                condition = Expression.Equal( memberAccess, filter );
                break;
            //not equal !=
            case WhereOperation.NotEqual:
                condition = Expression.NotEqual( memberAccess, filter );
                break;
            // Greater
            case WhereOperation.Greater:
                condition = Expression.GreaterThan( memberAccess, filter );
                break;
            // Greater or equal
            case WhereOperation.GreaterOrEqual:
                condition = Expression.GreaterThanOrEqual( memberAccess, filter );
                break;
            // Less
            case WhereOperation.Less:
                condition = Expression.LessThan( memberAccess, filter );
                break;
            // Less or equal
            case WhereOperation.LessEqual:
                condition = Expression.LessThanOrEqual( memberAccess, filter );
                break;
            //string.Contains()
            case WhereOperation.Contains:
                condition = Expression.Call( memberAccess,
                                            typeof( string ).GetMethod( "Contains" ),
                                            Expression.Constant( searchCriteria.Value ) );
                break;

            default:
                continue;
        }

        resultCondition = resultCondition != null ? Expression.Or( resultCondition, condition ): condition;
    }

    lambda = Expression.Lambda( resultCondition, parameter );

    MethodCallExpression result = Expression.Call(
               typeof( Queryable ), "Where",
               new [ ] { Query.ElementType },
               Query.Expression,
               lambda );

    return Query.Provider.CreateQuery&lt;T&gt;( result );

}

Respuesta aceptada

En términos de rendimiento y facilidad de implementación, ¿cómo es este un mejor enfoque que usar la Biblioteca de consultas dinámicas? Creo que de esta manera usted tiene un mejor control sobre la salida SQL de sus árboles de expresión.

Por Raúl Roa


Respuesta popular

Si tiene un conjunto fijo de operadores y un conjunto fijo de miembros, puede escribir esto casi sin tratar con árboles de expresión directamente. La idea es crear expresiones lambda simples para varias piezas de código (por ejemplo, Expression<Func<Entity, string>> para leer la propiedad de un miembro y similares para los operadores) y luego componerlas para construir un árbol de expresiones. Describí la solución aquí . El único problema es que la composición de expresiones no se admite directamente en C #, por lo que necesita un poco de preprocesamiento (consulte la sección sobre "Utilites expandibles").

Luego, puede almacenar funciones básicas en un diccionario y seleccionar la correcta (o una combinación de ellas) según lo que el usuario seleccione. Por ejemplo algo como:

NorthwindDataContext db = new NorthwindDataContext();

// A query that tests whether a property 
// (specified by 'selector' matches a string value
var queryBuilder = Linq.Func
  ((Expression<Func<Customer, string>> selector, string val) =>
      from c in db.Customers.ToExpandable()
      where selector.Expand(c).IndexOf(val) != -1
      select c);

// Dictionary with supported members...
var dict = new Dictionary<string, Expression<Func<Customer, string>>> 
  { { "CompanyName", c => c.CompanyName },
    { "Country",     c => c.Country },
    { "ContactName", c => c.ContactName } };

// Ask user for a property name & value and Build the query
string field = Console.ReadLine();
string value = Console.ReadLine();
var q = queryBuilder(dict[field], value);

El artículo también contiene un ejemplo con la composición de condiciones OR o AND dinámicamente. No actualicé el código por un tiempo, así que necesita algo de trabajo, pero creo que el proyecto LINQ KIT también contiene una versión de esta idea.



Licencia bajo: CC-BY-SA with attribution
No afiliado con Stack Overflow
¿Es esto KB legal? Sí, aprende por qué
Licencia bajo: CC-BY-SA with attribution
No afiliado con Stack Overflow
¿Es esto KB legal? Sí, aprende por qué