Agregar un Endpoint Get por ID en .NET Aspire en Linux
[[academy-video-youtube({"vid": "hMhA5sLHUpY", "start_time": "0", "title": "Adding a Get By ID Endpoint in .NET Aspire on Linux", "creator": "Tim Corey", "length": "15m 20s"})]]
Devolver cada registro de una tabla de base de datos es útil para páginas de listado, pero la mayoría de los consumidores de API también necesitan obtener un solo registro por su identificador. Ese segundo punto final introduce decisiones que la ruta 'obtener todos' no requirió: qué tipo de retorno tiene sentido para un solo objeto frente a una colección, qué sucede cuando el ID no coincide con ningún registro, y cómo comunicar ese fallo al llamador con el código de estado HTTP correcto.
In his video "Adding a Get By ID Endpoint in .NET Aspire on Linux," Tim Corey continues the Tiny Ticket API by adding a GET /api/tickets/{id} endpoint. Lo que comienza como un copia y pega de la ruta existente se convierte en un recorrido en vivo del manejo de casos especiales: devolver un solo objeto en lugar de un array, verificar resultados nulos y usar TypedResults para devolver ya sea un 200 OK con el ticket o un 404 Not Found cuando no existe el ID. Si sigues la serie de C# en Linux o estás construyendo APIs mínimas que necesitan respuestas de código de estado adecuadas, este episodio cubre todo el proceso de pensamiento.
Copiando la Ruta Get All como Punto de Partida
[0:35 - 1:45] Tim abre el servicio API Program.cs y duplica el endpoint GET /api/tickets existente. La nueva ruta requiere un parámetro de ruta para el ID del ticket, así que el patrón de URL cambia para incluir {id:int} y el manejador recibe un parámetro int id. La referencia al procedimiento almacenado cambia de spTickets_GetAll a spTickets_Get, que espera un parámetro ID.
app.MapGet("/api/tickets/{id:int}", async (int id, IDbConnection db) =>
{
var tickets = await db.LoadSqlAsync<TicketModel>("spTickets_Get", new { id });
// Initial version: returns a list, which we'll fix next
return tickets;
});app.MapGet("/api/tickets/{id:int}", async (int id, IDbConnection db) =>
{
var tickets = await db.LoadSqlAsync<TicketModel>("spTickets_Get", new { id });
// Initial version: returns a list, which we'll fix next
return tickets;
});Un detalle de conveniencia en los nombres: el parámetro del procedimiento almacenado es id en minúsculas, que coincide exactamente con el nombre del parámetro en C#. Eso significa que Dapper puede mapear el objeto anónimo new { id } directamente sin especificar un nombre de propiedad. Si el parámetro SQL utilizara un formato diferente, el objeto anónimo necesitaría una asignación explícita de propiedad como new { Id = id }.
Devolver un Solo Objeto en Lugar de una Lista
[2:39 - 4:16] La primera prueba a través de Swagger revela un problema: pasar el ID 2 devuelve un 200 con el ticket correcto, pero el cuerpo de respuesta está envuelto en un array JSON. Cuando un llamador solicita un recurso único por ID, espera un solo objeto, no una colección que contenga un elemento.
Añadir .FirstOrDefault() al resultado de la consulta corrige el encapsulamiento. FirstOrDefault devuelve el primer elemento si la lista tiene elementos, o null si la lista está vacía. Eso soluciona el problema del array, pero introduce una nueva pregunta: ¿qué debería devolver la API cuando el ID no coincide con ningún registro?
var output = tickets.FirstOrDefault();var output = tickets.FirstOrDefault();El cambio de una línea produce la forma de respuesta correcta para los registros existentes. Sin embargo, probar con el ID 4, que no existe en la base de datos, revela un vacío más profundo. La respuesta regresa como null con un código de estado 200. Eso es técnicamente HTTP válido, pero engaña al llamador: un 200 significa que la solicitud fue exitosa y el recurso fue encontrado, cuando en realidad no coincidió nada.
Manejo de No Encontrado con TypedResults
[4:41 - 11:44] Esta sección es donde Tim trabaja a través de las decisiones de diseño en tiempo real, lo que la hace valiosa para ver en lugar de solo leer el código final. Su proceso de pensamiento pasa por varias iteraciones:
Primero, considera usar .First() en lugar de .FirstOrDefault(), lo cual lanza una excepción cuando la lista está vacía. Eso produce un error 500, lo cual es peor que un 200 nulo porque 500 implica un error del servidor en lugar de un recurso faltante.
Luego retrocede y construye una verificación de nulo. Almacena el resultado en una variable, verifica si es nula, y devuelve diferentes respuestas para cada caso. El desafío es que un manejador de API mínimo necesita declarar su tipo de retorno explícitamente cuando puede devolver más de una forma de respuesta.
La solución es TypedResults, lo que te permite especificar los posibles tipos de respuesta en la firma del método:
app.MapGet("/api/tickets/{id:int}", async Task<Results<Ok<TicketModel>, NotFound>> (int id, IDbConnection db) =>
{
var tickets = await db.LoadSqlAsync<TicketModel>("spTickets_Get", new { id });
var output = tickets?.FirstOrDefault();
if (output is null)
{
return TypedResults.NotFound();
}
return TypedResults.Ok(output);
});app.MapGet("/api/tickets/{id:int}", async Task<Results<Ok<TicketModel>, NotFound>> (int id, IDbConnection db) =>
{
var tickets = await db.LoadSqlAsync<TicketModel>("spTickets_Get", new { id });
var output = tickets?.FirstOrDefault();
if (output is null)
{
return TypedResults.NotFound();
}
return TypedResults.Ok(output);
});Ese tipo de retorno, Task<Results<Ok<TicketModel>, NotFound>>, indica al marco de trabajo (y a Swagger) que este endpoint produce ya sea un 200 con un cuerpo TicketModel o un 404 sin cuerpo. La coincidencia de corchetes de colores en VS Code ayuda a navegar los ángulos anidados, que se acumulan rápidamente con tipos de resultado genéricos.
Un pequeño error sale a la luz durante las pruebas: la primera versión llama a TypedResults.NotFound() pero no return. El endpoint compila porque NotFound() es una expresión válida, pero sin la palabra clave return, la ejecución pasa al camino Ok. Tim detecta esto cuando Swagger aún muestra un 200 para un ID faltante, añade el return, y el 404 aparece correctamente en la siguiente ejecución.
Probando Ambos Caminos en Swagger
[11:44 - 14:09] With the final code in place, Tim runs through both scenarios in the Swagger UI. Pasar el ID 3 devuelve un 200 con el objeto del ticket. Pasar el ID 4 devuelve un 404 con un cuerpo de respuesta vacío.
También señala un detalle en la interfaz de Swagger que puede confundir a los usuarios por primera vez: la sección "Respuestas" debajo del botón de ejecución muestra los códigos de respuesta posibles (200 y 404), no el resultado actual. La respuesta real del servidor aparece en un panel separado encima de esa sección. Confundir los dos paneles es una fuente común de "¿por qué estoy obteniendo un 200?" confusión.
El enfoque TypedResults también mejora automáticamente la documentación de Swagger. Dado que el tipo de retorno declara tanto Ok<TicketModel> como NotFound, Swagger muestra ambos como posibles resultados con sus respectivos esquemas. Los que llaman al leer la documentación de la API saben que necesitan manejar un caso 404 sin que el desarrollador escriba anotaciones OpenAPI por separado.
Conclusión: Casos límite antes que características
[14:09 - 15:09] Lo que comenzó como un simple copiar-pegar del punto final get-all se convirtió en un ejercicio más profundo en el diseño de API. La versión final maneja el camino feliz (registro encontrado), la falla esperada (registro no encontrado) y una verificación nula defensiva para escenarios inesperados (consulta que devuelve nulo). El enfoque de Tim de trabajar estos casos en vivo, en lugar de presentar código pulido, demuestra el tipo de pensamiento iterativo que requieren los puntos finales en producción.
Conclusión
[15:09 - 15:20] Añadir un endpoint por ID a una API mínima implica tres decisiones más allá de la definición de la ruta: usar FirstOrDefault para desenvolver la colección, verificar si es nulo para distinguir "no encontrado" de "encontrado," y declarar TypedResults en el tipo de retorno para que el marco de trabajo devuelva el código de estado HTTP correcto. El patrón Results<Ok<t>, NotFound> es reutilizable en cualquier endpoint que necesite comunicar éxito o ausencia.
Navegación de la serie: Este artículo es parte de la serie C# en Linux construyendo la aplicación Tiny Ticket. Anterior: Agregar Swagger UI. Siguiente: Agregar un Punto Final de Inserción POST.
Consejo de ejemplo: cuando tu manejador de API mínima devuelve múltiples posibles códigos de estado, siempre decláralos en el genérico Results<>. Esto genera documentación precisa de Swagger automáticamente y obliga al compilador a verificar que cada ruta de código devuelva un tipo de resultado válido.
Mira el video completo en su Canal de YouTube y obtén más información sobre cómo construir puntos finales de API robustos en la serie C# en Linux.

