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:
$filter$count(en V4, reemplaza a$inlinecountde versiones anteriores)$orderby$skiptoken$skip$top$expand$select$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.