Potenciando tu API Web con las Consultas Simplificadas de OData

Este artículo es parte de una serie sobre OData.

Introducción

Tras una breve introducción a OData, profundizaremos en sus potentes y convenientes capacidades de consulta, basándonos en la versión más reciente, OData V4.

Consideremos las siguientes clases de entidad y su configuración OData correspondiente:


public class DeviceInfo
{
   [Key]
   [MaxLength(200)]
   public string DeviceId { get; set; }
   // ... otros campos ...
   public float Longitude { get; set; }
   public Config[] Layout { get; set; }
}

public class AlarmInfo
{
   [Key]
   [MaxLength(200)]
   public string Id { get; set; }
   public string DeviceId { get; set; }
   public string Type { get; set; }
   [ForeignKey("DeviceId")]
   public virtual DeviceInfo DeviceInfo { get; set; }
   public bool Handled { get; set; }
   public long Timestamp { get; set; }
}
 

La configuración del controlador es sencilla, con la lógica principal concentrada en unas pocas líneas:


[ODataRoute]
[EnableQuery]
[ProducesResponseType(typeof(ODataValue<IEnumerable<DeviceInfo>>), Status200OK)]
public IActionResult Get()
{
   return Ok(_context.DeviceInfoes.AsQueryable());
}

[ODataRoute("({key})")]
[EnableQuery]
[ProducesResponseType(typeof(ODataValue<IDeviceInfo>), Status200OK)]
public IActionResult GetSingle([FromODataUri] string key)
{
   var result = _context.DeviceInfoes.Find(key);
   if (result == null) return NotFound(new ODataError() { ErrorCode = "404", Message = "Cannot find key." });
   return Ok(result);
}
 

Acceso a Colecciones y Objetos Individuales

OData facilita el acceso tanto a colecciones de entidades como a objetos individuales.


// Acceder a una colección
GET http://localhost:9000/api/DeviceInfoes

// Acceder a un objeto individual. Nota: los strings deben ir entre comillas simples.
GET http://localhost:9000/api/DeviceInfoes('someDeviceId')
 

$Filter: Filtrado de Datos

Cuando se trabaja con grandes coleccionse, el filtrado de datos en el servidor es esencial. Los parámetros $filter permiten realizar esta selección de manera eficiente.


// Filtrar alarmas para un dispositivo específico dentro de un rango de tiempo.
// Nota: El DeviceId 'device123' debe ser manejado adecuadamente en la URL o por el cliente.
GET http://localhost:9000/api/AlarmInfoes?$filter=(DeviceId eq 'device123') and (Timestamp ge 12421552) and (Timestamp le 31562346785561)
 

El parámetro $filter soporta una amplia gama de sintaxis, permitiendo a los clientes construir consultas complejas y flexibles según sus necesidades, mejorando significativamente la agilidad de la API.

$Expand: Recuperación de Datos Relacionados

Al consultar AlarmInfo, a menudo es deseable obtener también la información completa del DeviceInfo asociado. Sin $expand, la respuesta predeterminada solo incluiría el DeviceId:


{
   "@odata.context": "http://localhost:9000/api/$metadata#AlarmInfoes",
   "value": [
       {
           "Id": "235314",
           "DeviceId": "123",
           "Type": "LowTemperature",
           "Handled": true,
           "Timestamp": 1589235890047
       },
       // ... otros elementos
   ]
}
 

Esto podría llevar al probleam N+1, donde se requiere una consulta adicional por cada elemento de la lista para obtener los detalles del DeviceInfo. OData resuelve esto elegantemente con el parámetro $expand:


GET http://localhost:9000/api/alarminfoes?$expand=deviceInfo
 

Al especificar $expand=deviceInfo, la respuesta incluirá los datos de la propiedad de navegación deviceInfo:


{
   "@odata.context": "http://localhost:9000/api/$metadata#AlarmInfoes(deviceInfo())",
   "value": [
       {
           "Id": "235314",
           "DeviceId": "123",
           "Type": "LowTemperature",
           "Handled": true,
           "Timestamp": 1589235890047,
           "deviceInfo": {
               "DeviceId": "123",
               "Name": null,
               "DeviceType": null,
               "Location": "plex",
               "Description": "99",
               "Longitude": 99,
               "Layout": []
           }
       }
       // ... otros elementos
   ]
}
 

Consultas Jerárquicas

OData permite realizar consultas a través de relaciones anidadas. Por ejemplo, para encontrar todas las AlarmInfo asociadas a un DeviceInfo con una Longitude mayor que 90:


GET http://localhost:9000/api/alarminfoes?$expand=deviceinfo&$filter=deviceinfo/longitude gt 90
 

Aquí, la barra diagonal (/) se utiliza para navegar por la jerarquía de la relación (deviceinfo/longitude), diferenciándose del punto (.) que podría tener interpretaciones diferentes en ciertos contextos como los navegadores.

Operadores $any y $all

Los operadores $any y $all son útiles para realizar evaluaciones sobre colecciones dentro de una entidad. Por ejemplo, para encontrar DeviceInfo cuyo Layout contenga algún elemento con Description igual a '0':


GET http://localhost:9000/api/DeviceInfoes?$filter=layout/any(e: e/description eq '0')
 

Nota Importante: En versiones de EF Core 3.0 y posteriores, las consultas $any/$all sobre colecciones anidadas (no en el nivel superior) pueden no ser traducibles directamente a LINQ. Esto se debe a precauciones de seguridad para evitar desbordamientos de memoria en el servidor. Si se requiere esta funcionalidad, se puede recurrir a la evaluación del lado del cliente usando métodos como AsEnumerable(), aunque esto debe hacerse con precaución.

Manejo de Fechas y Tiempos

Para consultas basadas en fechas y horas, OData permite el uso de literales de fecha/hora en formato ISO 8601. Por ejemplo, para consultar alarmas dentro de una fecha específica:


GET http://localhost:9000/api/AlarmInfoes?$filter=Timestamp ge datetime'2016-10-01T00:00:00'
 

Esto permite realizar filtros precisos sobre campos de tipo fecha/hora.

Orden de Procesamiento de Consultas

OData procesa los parámetros de consulta en un orden específico para garantizar la coherencia. El orden general es:

  1. $filter
  2. $count (en V4, reemplaza a $inlinecount de versiones anteriores)
  3. $orderby
  4. $skiptoken
  5. $skip
  6. $top
  7. $expand
  8. $select
  9. $format

El $filter se aplica primero, asegurando que los recuentos ($count) y las ordenaciones se basen en los datos ya filtrados.

Guía para el Frontend

La construcción manual de cadenas de consulta puede ser tediosa y propensa a errores. Afortunadamente, existen bibliotecas que simplifican este proceso. El ecosistema OData ofrece diversas librerías cliente para diferentes lenguajes. Una opción popular para JavaScript es o.js:


const response = await o('http://my.url')
 .get('User')
 .query({$filter: `UserName eq 'foobar'`});

console.log(response); // Devuelve un solo usuario si solo hay uno, o un array de usuarios.
 

Estas librerías abstraen la complejidad de la construcción de las URLs de consulta, permitiendo a los desarrolladores frontend centrarse en la lógica de la aplicación.

Etiquetas: odata WebApi REST C# efcore

Publicado el 7-25 10:30