Ir al contenido principal

Predicados Dinámicos Linq Usando LinqKit

El escenario es el siguiente:

Generar predicados linq dinámicos de acuerdo a un conjunto de parámetros variables.

Ejemplo:

Si tengo un listado de elementos, por ejemplo, los libros disponibles en una biblioteca y quiero hacer el filtrado de la misma usando uno o más parámetros de búsqueda. Se puede resolver usando predicados linq dinámicos.

Supongamos que nuestra base de datos de libros está formada por una tabla llamada Libros con los siguientes campos:
  • ISBN o identificado único
  • Título del libro
  • Precio
Obviamente nuestra tabla puede estar conformada por más campos, dependiendo de las necesidades del negocio, para nuestro ejemplo, estos datos serán suficientes.

El tipo de búsquedas que me gustaría realizar en el listado serían las siguientes:
  1. El título contenga alguna frase ingresada por el usuario
  2. Que el precio sea mayor al especificado por el usuario
  3. Que el precio sea menor al especificado por el usuario
  4. Que el precio se encuentre entre un rango mínimo y máximo
Consideremos la siguiente UX:


Ahora veamos un video en el que se implementará dicha solución usando LinqKit


Clase ExpressionExtensions

Esta clase nos permitirá generar la unión de expresiones linq del tipo: AND y OR 

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
/// <summary>
/// Clase usada para la generación de expresiones lambda
/// </summary>
public static class ExpressionExtensions
{
    #region Métodos

    /// <summary>
    /// Método para unión de predicados usando el operador Or
    /// </summary>
    /// <typeparam name="T">Tipo al que aplica la expresión</typeparam>
    /// <param name="expr1">Expresión 1</param>
    /// <param name="expr2">Expresión 2</param>
    /// <returns>Predicado del tipo: Expresión 1 OR Expresión 2</returns>
    public static Expression<Func<T, bool>> Or<T>(this Expression<Func<T, bool>> expr1,
        Expression<Func<T, bool>> expr2)
    {
        var invokedExpr = Expression.Invoke(expr2, expr1.Parameters.Cast<Expression>());
        return Expression.Lambda<Func<T, bool>>(Expression.Or(expr1.Body, invokedExpr),
            expr1.Parameters);
    }

    /// <summary>
    /// Método para unión de predicados usando el operador AND
    /// </summary>
    /// <typeparam name="T">Tipo al que aplica la expresión</typeparam>
    /// <param name="expr1">Expresión 1</param>
    /// <param name="expr2">Expresión 2</param>
    /// <returns>Predicado del tipo: Expresión 1 AND Expresión 2</returns>
    public static Expression<Func<T, bool>> And<T>(this Expression<Func<T, bool>> expr1,
        Expression<Func<T, bool>> expr2)
    {
        var invokedExpr = Expression.Invoke(expr2, expr1.Parameters.Cast<Expression>());
        return Expression.Lambda<Func<T, bool>>(Expression.And(expr1.Body, invokedExpr),
            expr1.Parameters);
    }

    /// <summary>
    /// Método para unión de predicados usando el operador OR
    /// </summary>
    /// <typeparam name="T">Tipo al que aplica la expresión</typeparam>
    /// <param name="exprs">Conjunto de expresiones a unir</param>
    /// <returns>Predicado del tipo: Expresión 1 OR Expresión 2 OR ... Expresión N</returns>
    public static Expression<Func<T, bool>> WhereOr<T>
        (this IEnumerable<Expression<Func<T, bool>>> exprs)
    {
        Expression<Func<T, bool>> FinalQuery = t => false;
        foreach (var expr in exprs)
        {
            FinalQuery = FinalQuery.Or(expr);
        }
        return FinalQuery;
    }

    /// <summary>
    /// Método para unión de predicados usando el operador AND
    /// </summary>
    /// <typeparam name="T">Tipo al que aplica la expresión</typeparam>
    /// <param name="exprs">Conjunto de expresiones a unir</param>
    /// <returns>Predicado del tipo: Expresión 1 AND Expresión 2 AND ... Expresión N</returns>
    public static Expression<Func<T, bool>> WhereAnd<T>
        (this IEnumerable<Expression<Func<T, bool>>> exprs)
    {
        Expression<Func<T, bool>> FinalQuery = t => true;
        foreach (var expr in exprs)
        {
            FinalQuery = FinalQuery.And(expr);
        }
        return FinalQuery;
    }

    #endregion
}

Formulario de Prueba

btnBuscar_Click

Este evento validará los criterios ingresados por el usuario, para posteriormente filtrar el listado.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
/// <summary>
/// Realiza el filtrado del listado de acuerdo a los parámetros ingresados por el usuario
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void btnBuscar_Click(object sender, EventArgs e)
{
    // Valida que se haya ingresado al menos un parámetro de búsqueda
    if (tbxNombre.Text.Trim().Length > 0 || tbxPrecioMínimo.Text.Trim().Length > 0 ||
        tbxPrecioMáximo.Text.Trim().Length > 0)
    {
        // Valida que los precios estén correctamente formateados
        if (ValidarPrecio(tbxPrecioMínimo) && ValidarPrecio(tbxPrecioMáximo))
        {
            decimal precioMínimo;
            decimal.TryParse(tbxPrecioMínimo.Text, NumberStyles.Currency,
                CultureInfo.CurrentCulture, out precioMínimo);
            decimal precioMáximo;
            decimal.TryParse(tbxPrecioMáximo.Text, NumberStyles.Currency,
                CultureInfo.CurrentCulture, out precioMáximo);
            // Ejecuta el filtrado del listado
            FiltrarListado(tbxNombre.Text.Trim(), precioMínimo, precioMáximo);
        }
        // Muestra un mensaje indicando que los parámetros de búsqueda 
        // no tienen el formato adecuado
        else
        {
            MessageBox.Show("Revise los criterios de búsqueda proporcionados", 
                "Búsqueda de Libros", MessageBoxButtons.OK, MessageBoxIcon.Exclamation);
        }
    }
}

FiltrarListado

Este método genera los predicados linq, los une utilizando ExpressionExtenssions y los aplica al listado general, usando para ello el método AsExpandable(), el cuál está definido como parte de LinqKit, aplicando el predicado final.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
/// <summary>
/// Filtra el listado de acuerdo a los parámetros recibidos
/// </summary>
/// <param name="título">Título del libro</param>
/// <param name="precioMínimo">Precio mínimo</param>
/// <param name="precioMáximo">Precio máximo</param>
private void FiltrarListado(string título, decimal precioMínimo, decimal precioMáximo)
{
    // Expresión base ISBN no es nulo ni cadena vacia
    Expression<Func<Libro, bool>> expresiónFinal = l => !string.IsNullOrEmpty(l.ISBN);

    // Búsqueda por título del libro de tipo Contains
    if (!string.IsNullOrEmpty(título))
    {
        Expression<Func<Libro, bool>> expresión = 
            l => l.Título.ToUpper().Contains(título.ToUpper());
        expresiónFinal = ExpressionExtensions.And(expresiónFinal, expresión);
    }

    // Búsqueda por precio mínimo
    if (precioMínimo > 0 && precioMáximo.Equals(0))
    {
        Expression<Func<Libro, bool>> expresión = l => l.Precio >= precioMínimo;
        expresiónFinal = ExpressionExtensions.And(expresiónFinal, expresión);
    }

    // Búsqueda por precio máximo
    if (precioMínimo.Equals(0) && precioMáximo > 0)
    {
        Expression<Func<Libro, bool>> expresión = l => l.Precio <= precioMáximo;
        expresiónFinal = ExpressionExtensions.And(expresiónFinal, expresión);
    }

    // Búsqueda por precio mínimo y máximo
    if (precioMínimo > 0 && precioMáximo > 0)
    {
        if (precioMínimo <= precioMáximo)
        {
            Expression<Func<Libro, bool>> expresión = 
                l => l.Precio >= precioMínimo && l.Precio <= precioMáximo;
            expresiónFinal = ExpressionExtensions.And(expresiónFinal, expresión);
        }
    }

    // Aplicación del predicado final
    dgvLibros.DataSource = datos.Elements("bookstore").Elements().Select(b => new Libro()
    {
        ISBN = b.Attribute("ISBN").Value,
        Título = b.Attribute("title").Value,
        Precio = decimal.Parse(b.Attribute("price").Value)
    }).AsQueryable().AsExpandable().Where(expresiónFinal).ToList();
}

Código Fuente

Recuerda que puedes descargar el código fuente usado en el ejemplo del video para que puedas llevar a cabo las pruebas y el entendimiento del código de mejor manera,



Si tienes dudas o comentarios, déjanos un comentario.

Comentarios

Entradas más populares de este blog

Publicación de WCF en Net.TCP

Una de las grandes características de WCF es la forma en como éstos pueden ser expuestos a nuestros clientes, por ejemplo los siguientes protocolos, de manera muy recurrente: Http TCP Net Pipe ¿Sabes en que casos usar cada uno? Http cuando el servicio y el cliente se encuentran en redes diferentes TCP cuando el servicio y el cliente se encuentran en la misma red, pero en equipos diferentes Net Pipe cuando el servicio y el cliente se encuentran alojados en el mismo equipo ¿Por qué razón? Por cuestiones de redimiento y seguridad de la información Si una petición http toma un tiempo de 200 ms, por poner un ejemplo Una petición tcp tomará aproximadamente el 20% menos que la petición http (40 ms aproximadamente) Y una petición Net Pipe , tomará aproximadamente el 80% del tiempo que toma el hacer una petición tcp (32 ms aproximadamente) Para mayor entendimiento, chequen este post que alguna vez consultamos para entender un poco más acerca de éstos conceptos: https://j...

Validación de Fechas con jQuery Validator

Al validar fechas con jQuery Validate nos encontramos con el problema de que dicha validación se hace en formato: MM/DD/YYYY, esto es correcto, pero no tanto para formato de fechas de México al menos que es más común para nosotros hacerlo de manera que dicho formato sea: DD/MM/YYYY. Para poder solucionar este problema haremos algunas modificaciones en las librerías: jquery.validate.js jquery.validate.min.js Veamos el siguiente video para la implementación de dicha solución: jquery.validate.js Se deja comentado el cómo se hace la validación original y se adiciona el código personalizado que permite validar el formato de fecha del tipo: DD/MM/YYYY 1 2 3 4 5 6 7 8 9 10 11 // http://jqueryvalidation.org/date-method/ // Nivisix Soluciones: personalización para formato de fechas DD/MM/YYYY date : function ( value, element ) { //return this.optional( element ) || !/Invalid|NaN/.test( new Date( value ).toString() ); var com...

SQLServer Stored Procedure Para Ordenar Columnas Ascendente / Descendente

Pues este es nuestro primer post referente a SQLServer ... En primera instancia les queremos platicar qué es lo que buscamos:  Mediante un stored procedure obtener un listado ordenado por alguna de las columnas devueltas de manera ascendente o descendente, dependiendo de los parámetros de entrada del mismo. Muchas de las ocasiones cuando nos ha tocado buscar información acerca de cómo hacer este proceso, solemos encontrar recomendaciones de hacerlo usando un query que se contruye en tiempo de ejecución usando una variable de tipo VARCHAR(MAX) y después hacer uso de la función EXEC para ejecutarlo. Al final, se resuelve el problema, pero la intención es mejorar el procedimiento sin afectar el rendimiento y evitando problemas de seguridad como el SQL Injection . Veamos un video con una propuesta diferente de resolución de este requerimiento: Creación de la Base de Datos Usando el siguiente query que pueden ejecutar en el cliente base de datos SQL Server creare...