Saltar al contenido

Consulta dinámica con condiciones OR en Entity Framework

Por fin después de mucho batallar ya dimos con la contestación de esta impedimento que agunos lectores de nuestra web tienen. Si tienes algo más que aportar puedes aportar tu conocimiento.

Solución:

Probablemente esté buscando algo como Predicate Builder que le permita controlar los AND y OR de la instrucción where más fácilmente.

También hay Dynamic Linq que le permite enviar la cláusula WHERE como un SQL string y lo analizará en el predicado correcto para un DÓNDE.

Si bien LINQKit y su PredicateBuilder son bastante versátiles, es posible hacer esto de manera más directa con algunas utilidades simples (cada una de las cuales puede servir como base para otras operaciones de manipulación de expresiones):

Primero, un sustituto de expresión de uso general:

public class ExpressionReplacer : ExpressionVisitor

    private readonly Func replacer;

    public ExpressionReplacer(Func replacer)
    
        this.replacer = replacer;
    

    public override Expression Visit(Expression node)
    
        return base.Visit(replacer(node));
    

A continuación, un método de utilidad simple para reemplazar el uso de un parámetro con otro parámetro en una expresión dada:

public static T ReplaceParameter(T expr, ParameterExpression toReplace, ParameterExpression replacement)
    where T : Expression

    var replacer = new ExpressionReplacer(e => e == toReplace ? replacement : e);
    return (T)replacer.Visit(expr);

Esto es necesario porque los parámetros lambda en dos expresiones diferentes son en realidad parámetros diferentes, incluso cuando tienen el mismo nombre. Por ejemplo, si quieres terminar con q => q.first.Contains(first) || q.last.Contains(last), entonces el q en q.last.Contains(last) debe ser el exactamente el mismoq que se proporciona al principio de la expresión lambda.

A continuación, necesitamos un propósito general Join método que es capaz de unirse Func-Estilo Lambda Expressions junto con un generador de expresión binaria dado.

public static Expression> Join(Func joiner, IReadOnlyCollection>> expressions)

    if (!expressions.Any())
    
        throw new ArgumentException("No expressions were provided");
    
    var firstExpression = expressions.First();
    var otherExpressions = expressions.Skip(1);
    var firstParameter = firstExpression.Parameters.Single();
    var otherExpressionsWithParameterReplaced = otherExpressions.Select(e => ReplaceParameter(e.Body, e.Parameters.Single(), firstParameter));
    var bodies = new[]  firstExpression.Body .Concat(otherExpressionsWithParameterReplaced);
    var joinedBodies = bodies.Aggregate(joiner);
    return Expression.Lambda>(joinedBodies, firstParameter);

Usaremos esto con Expression.Or, pero puede usar el mismo método para una variedad de propósitos, como combinar expresiones numéricas con Expression.Add.

Finalmente, poniendo todo junto, puede tener algo como esto:

var searchCriteria = new List>();

  if (!string.IsNullOrWhiteSpace(first))
      searchCriteria.Add(q => q.first.Contains(first));
  if (!string.IsNullOrWhiteSpace(last))
      searchCriteria.Add(q => q.last.Contains(last));
  //.. around 50 additional criteria
var query = Db.Names.AsQueryable();
if(searchCriteria.Any())

    var joinedSearchCriteria = Join(Expression.Or, searchCriteria);
    query = query.Where(joinedSearchCriteria);

  return query.ToList();

¿Existe una forma simple y limpia de agregar condiciones “O” a una consulta generada dinámicamente usando el marco de la entidad?

Sí, puede lograrlo simplemente confiando en un solo where cláusula que contiene una sola expresión booleana cuya OR las partes se “deshabilitan” o “habilitan” dinámicamente en tiempo de ejecución, evitando así tener que instalar LINQKit o escribir un generador de predicados personalizado.

En referencia a su ejemplo:

var isFirstValid = !string.IsNullOrWhiteSpace(first);
var isLastValid = !string.IsNullOrWhiteSpace(last);

var query = db.Names
  .AsQueryable()
  .Where(name =>
    (isFirstValid && name.first.Contains(first)) ||
    (isLastValid && name.last.Contains(last))
  )
  .ToList();

Como puede ver en el ejemplo anterior, estamos activando o desactivando dinámicamente las partes OR de la where-filtrar expresión basada en premisas previamente evaluadas (p. ej. isFirstValid).

Por ejemplo si isFirstValid no es true, luego name.first.Contains(first) está en cortocircuito y no se ejecutará ni afectará al conjunto de resultados. Además, EF Core DefaultQuerySqlGenerator optimizará y reducirá aún más la expresión booleana dentro where antes de ejecutarlo (p. ej. false && x || true && y || false && z puede reducirse a simplemente y a través de simple static análisis).

Tenga en cuenta: si ninguno de los locales es true, entonces el conjunto de resultados estará vacío, lo que supongo que es el comportamiento deseado en su caso. Sin embargo, si por alguna razón prefiere seleccionar todos los elementos de su IQueryable fuente, entonces puede agregar una variable final a la expresión que evalúa a true (p.ej .Where( ... || shouldReturnAll) con var shouldReturnAll = !(isFirstValid || isLastValid) o algo similar).

Un comentario final: la desventaja de esta técnica es que te obliga a construir una expresión booleana “centralizada” que reside en el mismo cuerpo del método en el que se encuentra tu consulta (más precisamente el where parte de la consulta). Si, por alguna razón, desea descentralizar el proceso de compilación de sus predicados e inyectarlos como argumentos o encadenarlos a través del generador de consultas, es mejor que se quede con un generador de predicados como se sugiere en las otras respuestas. De lo contrario, disfruta de esta sencilla técnica 🙂

Comentarios y valoraciones de la guía

No se te olvide comunicar esta reseña si lograste el éxito.

¡Haz clic para puntuar esta entrada!
(Votos: 0 Promedio: 0)



Utiliza Nuestro Buscador

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *