El estándar OData, definido por la organización OASIS, establece directrices estrictas sobre el formato de las peticiones y respuestas HTTP. Al trabajar con OData JSON Versión 4.01, la estructura de los datos devueltos varía significativamente dependiendo de si estamos entregando un único recurso o una colección de ellos.
En el desarrollo cotidiano con ASP.NET Core, es común exponer datos mediante IQueryable. Cuando estas consultas son procesadas por el middleware de OData, el resultado suele ser una colección envuelta en un objeto JSON específico. Una respuesta típica de una colección OData tiene este aspecto:
{
"@odata.context": "http://localhost:9000/api/v2/$metadata#Coleccion(Metricas_Dto)",
"@odata.count": 2,
"value": [
{
"id": 1001,
"lectura": 180.5,
"estado": "Normal"
},
{
"id": 1002,
"lectura": 210.2,
"estado": "Critico"
}
]
}
Como se observa, el array de datos no se devuelve directamente en la raíz, sino que se encapsula dentro de una propiedad llamada value, acompañada de metadatos como @odata.context. Sin embargo, si un endpoint de nuestra WebAPI no está mapeado a través de las rutas de OData, el comportamiento por defecto de ASP.NET Core es devolver un array plano:
[
{
"id": 1001,
"lectura": 180.5,
"estado": "Normal"
},
{
"id": 1002,
"lectura": 210.2,
"estado": "Critico"
}
]
Esta discrepancia genera inconsistencias en el consumo de la API desde el frontend, obligando a los desarrolladores cliente a implementar lógica condicional para extraer los datos dependiendo de la ruta accedida.
Estandarización de respuestas fuera del mapeo OData
Para garantizar la uniformidad en la arquitectura de la API, podemos ajustar manualmente los endpoints estándar para que sigan el esquema de OData, especialmente en lo que respecta a la propiedad value.
Gestión de Entidades Únicas
Cuando un endpoint devuelve un único objeto, la estructura base de OData suele coincidir con la de una WebAPI convencional, exceptuando la omisión de los metadatos de contexto. En este caso, no suele ser necesaria una transformación estructural compleja.
[HttpGet("/api/v1/sensores/{codigo}/ultimo")]
public async Task<IActionResult> ObtenerUltimoRegistro(string codigo)
{
var registro = await _context.Historial
.Where(x => x.SensorId == codigo)
.OrderByDescending(x => x.FechaRegistro)
.FirstOrDefaultAsync();
if (registro == null) return NotFound();
return Ok(_mapper.Map<MedicionDto>(registro));
}
En el ejemplo anterior, la respuesta JSON será un objeto directo, lo cual es compatible con la forma en que OData representa registros individuales.
Encapsulación de Colecciones de Objetos
El desafío surge con los listados. Para alinear un endpoint convencional con el formato OData, debemos envolver la lista de resultados en un objeto anónimo que contenga la propiedad Value.
[HttpGet("/api/v1/sensores/{codigo}/historial")]
public async Task<IActionResult> ListarHistorial(string codigo)
{
var resultados = await _context.Historial
.Where(x => x.SensorId == codigo)
.ToListAsync();
// Envolvemos el resultado para mantener consistencia con OData
var respuestaEstandarizada = new
{
Value = _mapper.Map<List<MedicionDto>>(resultados)
};
return Ok(respuestaEstandarizada);
}
Es importante notar que, aunque en el código C# se define la propiedad como Value (con mayúscula inicial), si la API tiene configurada la política de nomenclatura CamelCase (común en ASP.NET Core moderno), el JSON resultante expondrá la clave como value. De este modo, el frontend podrá acceder a los datos siempre a través de la misma propiedad, independientemente de si el endpoint es un OData Controller o un ApiController tradicional.