[Parte 13] EF Core: Dynamic Sort con Linq Expressions

Isaac Ojeda - Apr 29 '23 - - Dev Community

Introducción

En este post veremos de forma rápida el cómo crear un método de extensión para Entity Framework para poder crear ordenamiento de columnas de la base de datos de una forma dinámica según como el cliente de la API necesite el ordenamiento.

Nota 💡: El código de este post lo encuentras aquí

Dynamic sort con Linq Expressions

Algo que queremos evitar al dar la posibilidad de que se ordene por cualquier propiedad de nuestro modelo, es el deber tener un switch/case o ifs gigantes donde se evalúe cada posibilidad de ordenamiento, y esto repetirlo en cada endpoint donde queramos ofrecer ordenamiento.

No es bonito ni práctico, así que haremos uso de LINQ Expressions para lograr esta tarea. La idea es simplemente decir que propiedad queremos ordenar y bajo qué dirección (ascendente o descendente).

Linq expressions nos ayudan a crear expresiones lambda pero que son dinámicas en runtime, por lo que digamos, que partiendo de una expresión que parte de ella es generada con un string, se compila en runtime y genera el query de entity framework que queremos.

Para esto crearemos un método de extensión de IQueryable<TEntity> para poderlo usar como lo haríamos de forma habitual, pero en lugar de establecer una expresión lambda fuertemente tipada, le pasaremos un string que representa nuestra expresión de ordenamiento

ApplicationCore > Common > Extensions

using System.Linq.Expressions;

namespace MediatrExample.ApplicationCore.Common.Extensions;

public static class EFCoreExtensions
{
    public static IQueryable<TEntity> OrderBy<TEntity>(this IQueryable<TEntity> source, string orderByStrValues)
        where TEntity : class
    {
        var queryExpr = source.Expression;
        var command = orderByStrValues.ToUpper().EndsWith("DESC") ? "OrderByDescending" : "OrderBy";
        var propertyName = orderByStrValues.Split(' ')[0].Trim();

        var type = typeof(TEntity);
        var property = type.GetProperties()
            .Where(item => item.Name.ToLower() == propertyName.ToLower())
            .FirstOrDefault();

        if (property == null)
            return source;

        // p
        var parameter = Expression.Parameter(type, "p");
        // p.Price
        var propertyAccess = Expression.MakeMemberAccess(parameter, property);
        // p => p.Price
        var orderByExpression = Expression.Lambda(propertyAccess, parameter);

        // Ejem. final: .OrderByDescending(p => p.Price)
        queryExpr = Expression.Call(
            type: typeof(Queryable),
            methodName: command,
            typeArguments: new Type[] { type, property.PropertyType },
            queryExpr,
            Expression.Quote(orderByExpression));

        return source.Provider.CreateQuery<TEntity>(queryExpr); ;
    }

}
Enter fullscreen mode Exit fullscreen mode

Aquí utilizamos la clase Expression que nos ayuda a crear expresiones lambdas de forma dinámica. Por ejemplo, una expresión lambda la podemos conocer de esta forma:

context.Products.OrderBy(p => p.Price).Select(s => s.Description);
Enter fullscreen mode Exit fullscreen mode

Donde p => p.Price se evaluará en runtime para generar al final un query, al igual que s => s.Description. Esto si lo hacemos en un IQueryable se traduce a un Query SQL según el provider que estemos usando.

Estas expresiones lambda son fuertemente tipadas, ya están definidas, ya se sabe que es lo que harán y con que propiedades, la idea aquí es generarlas de forma dinámica en runtime según los datos de entrada de nuestro endpoint. Cada llamada de Expression crea partes de la expresión final y terminan siendo unidas con Expression.Call anexándolo a la expresión del Query original (por que previamente pudimos haber llamado .Where p .Select).

Suponiendo que orderByStrValues es "price desc" (para ordenar de forma descendente según la propiedad Price), nos estaría generando la siguiente expresión lambda en runtime:

query.OrderByDescending(p => p.Price)
Enter fullscreen mode Exit fullscreen mode

Por lo que con CreateQuery ya lo convertimos en un IQueryable para que sea traducido a un query SQL al ser ejecutado (porque después de esta expresión, podemos también llamar otras expresiones).

Para poder usarlo, necesitamos recibir en nuestro endpoint el campo a ordenar y la dirección:

public class GetProductsQuery : IRequest<List<GetProductsQueryResponse>>
{
    public string? SortDir { get; set; }
    public string? SortProperty { get; set; }
}
Enter fullscreen mode Exit fullscreen mode

Y en el handler:

using MediatrExample.ApplicationCore.Common.Extensions;

// código omitido...

    public Task<List<GetProductsQueryResponse>> Handle(GetProductsQuery request, CancellationToken cancellationToken) =>
        _context.Products
            .AsNoTracking()
            .OrderBy($"{request.SortProperty} {request.SortDir}")
            .ProjectTo<GetProductsQueryResponse>(_mapper.ConfigurationProvider)
            .ToListAsync();
Enter fullscreen mode Exit fullscreen mode

Nota 💡: Recuerden que este Query lo hicimos en las primeras partes de esta serie de posts, siempre puedes revisar el código aquí

Y también actualizamos el controller:

    /// <summary>
    /// Consulta los productos
    /// </summary>
    /// <returns></returns>
    [HttpGet]
    public Task<List<GetProductsQueryResponse>> GetProducts([FromQuery] GetProductsQuery query) =>
        _mediator.Send(query);
Enter fullscreen mode Exit fullscreen mode

Probando el Ordenamiento

La solución ya contiene Swagger, pero para evitar screenshots, con cualquier RestClient podemos probarlo:

### GET Products

GET {{host}}/api/products?sortDir=desc&sortProperty=price
Content-Type: application/json
Authorization: Bearer {{token}}
Enter fullscreen mode Exit fullscreen mode

Resultado:

[
  {
    "productId": "eQPDkwoYX31vMKGJ",
    "description": "Product 02",
    "price": 52200,
    "listDescription": "Product 02 - $52,200.00"
  },
  {
    "productId": "L1dwWxoz2omzN89g",
    "description": "Product 01",
    "price": 16000,
    "listDescription": "Product 01 - $16,000.00"
  }
]
Enter fullscreen mode Exit fullscreen mode

Este método ya lo podrías usar en cualquier Entity, ya que se hace directamente en el IQueryable y si en SQL esa columna permite ser ordenada, sin problema se podrá hacer.

Conclusión

La creación de expresiones linq dinámicas o incluso el uso de reflection suele ser un tema difícil de asimilar, pero te invito a que depures el código y veas cómo funciona paso a paso. Intenta modificarlo y hacer otro tipo de expresiones dinámicas, relamente podrías hacer cualquier cosa.

Con este método de extension ya lo podrás usar siempre en cualquier query y en cualquier entity, por lo que te quita un super dolor de cabeza para soportar ordenamiento dinámico en tus proyectos.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Terabox Video Player