Adicionando um Endpoint Get By ID em .NET Aspire no 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"})]]
Retornar todos os registros de uma tabela de banco de dados é útil para páginas de listagem, mas a maioria dos consumidores de API também precisa buscar um único registro por seu identificador. Esse segundo endpoint introduz decisões que a rota "get all" não exigia: qual tipo de retorno faz sentido para um único objeto versus uma coleção, o que acontece quando o ID não corresponde a nenhum registro, e como comunicar essa falha para o chamador com o código de status HTTP correto.
Em seu vídeo "Adding a Get By ID Endpoint in .NET Aspire on Linux", Tim Corey continua a Tiny Ticket API adicionando um ponto de extremidade GET /api/tickets/{id}. O que começa como uma cópia e colagem da rota existente se transforma em uma explicação ao vivo sobre o tratamento de casos extremos: retornando um único objeto em vez de um array, verificando resultados nulos e usando TypedResults para retornar um 200 OK com o ticket ou um 404 Not Found quando o ID não existe. Se você está acompanhando a série C# on Linux ou criando APIs mínimas que precisam de respostas adequadas de código de status, este episódio cobre todo o processo de pensamento.
Copiando a Rota Get All como Ponto de Partida
[0:35 - 1:45] Tim abre o Program.cs do serviço de API e duplica o ponto de extremidade existente GET /api/tickets. A nova rota precisa de um parâmetro de caminho para o ID do ticket, então o padrão da URL muda para incluir {id:int} e o manipulador recebe um parâmetro int id. A referência ao procedimento armazenado muda de spTickets_GetAll para spTickets_Get, que espera um 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;
});
Uma conveniência de nomenclatura que vale a pena notar: o parâmetro do procedimento armazenado é em minúsculas id, que corresponde exatamente ao nome do parâmetro em C#. Isso significa que o Dapper pode mapear o objeto anônimo new { id } diretamente sem especificar um nome de propriedade. Se o parâmetro SQL usasse uma grafia diferente, o objeto anônimo precisaria de uma atribuição explícita de propriedade como new { Id = id }.
Retornando um Único Objeto ao invés de uma Lista
[2:39 - 4:16] O primeiro teste através do Swagger revela um problema: passar o ID 2 retorna um 200 com o ticket correto, mas o corpo da resposta está envolvido em um array JSON. Quando um chamador solicita um recurso único por ID, espera-se um único objeto, não uma coleção contendo um elemento.
Anexar .FirstOrDefault() ao resultado da consulta corrige o encapsulamento. FirstOrDefault retorna o primeiro elemento se a lista tiver itens, ou null se a lista estiver vazia. Isso resolve o problema do array, mas introduz uma nova questão: o que a API deve retornar quando o ID não corresponde a nenhum registro?
var output = tickets.FirstOrDefault();
var output = tickets.FirstOrDefault();
A alteração de uma única linha produz o formato de resposta correto para registros existentes. No entanto, testar com o ID 4, que não existe no banco de dados, revela uma lacuna mais profunda. A resposta retorna como null com um código de status 200. Isso é tecnicamente válido no HTTP, mas engana quem chama: um 200 significa que a solicitação foi bem-sucedida e o recurso foi encontrado, quando na realidade nada correspondeu.
Tratando Not Found com TypedResults
[4:41 - 11:44] Esta seção é onde Tim trabalha as decisões de design em tempo real, o que a torna valiosa para assistir em vez de apenas ler o código final. Seu processo de pensamento passa por várias iterações:
Primeiramente, ele considera usar .First() em vez de .FirstOrDefault(), que lança uma exceção quando a lista está vazia. Isso produz um erro 500, o que é pior do que um nulo 200, porque 500 implica um bug no servidor, em vez de um recurso ausente.
Então ele recua e constrói uma verificação nula. Ele armazena o resultado em uma variável, verifica se está nula e retorna respostas diferentes para cada caso. O desafio é que um manipulador de API mínima precisa declarar explicitamente seu tipo de retorno quando pode retornar mais de um formato de resposta.
A solução é TypedResults, que permite especificar os possíveis tipos de resposta na assinatura do 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);
});
Esse tipo de retorno, Task<Results<Ok<TicketModel>, NotFound>>, informa ao framework (e ao Swagger) que este ponto de extremidade produz um 200 com um corpo TicketModel ou um 404 sem corpo. A correspondência de colchetes coloridos no VS Code ajuda a navegar pelos colchetes angulares aninhados, que se acumulam rapidamente com tipos de resultado genéricos.
Um bug sutil surge durante o teste: a primeira versão chama TypedResults.NotFound(), mas não return. O ponto de extremidade compila porque NotFound() é uma expressão válida, mas sem a palavra-chave return, a execução passa para o caminho Ok. Tim percebe isso quando o Swagger ainda mostra um 200 para um ID ausente, adiciona o return e o 404 aparece corretamente na próxima execução.
Análise de Ambos os Caminhos no Swagger
[11:44 - 14:09] Com o código final em vigor, Tim percorre ambos os cenários na Swagger UI. Passar o ID 3 retorna um 200 com o objeto do ticket. Passar o ID 4 retorna um 404 com um corpo de resposta vazio.
Ele também aponta um detalhe na interface do Swagger que pode confundir usuários de primeira viagem: a seção "Responses" abaixo do botão de execução mostra os códigos de resposta possíveis (200 e 404), não o resultado real. A resposta real do servidor aparece em um painel separado acima dessa seção. Confundir os dois painéis é uma fonte comum de confusão "por que estou recebendo um 200?".
A abordagem TypedResults também melhora a documentação do Swagger automaticamente. Porque o tipo de retorno declara ambos Ok<TicketModel> e NotFound, o Swagger exibe ambos como resultados potenciais com seus respectivos esquemas. Chamadores que leem a documentação da API sabem que precisam lidar com um caso 404 sem que o desenvolvedor escreva anotações OpenAPI separadas.
Conclusão: Casos de Borda Antes de Funcionalidades
[14:09 - 15:09] O que começou como um simples copiar-colar do endpoint get-all transformou-se em um exercício mais profundo de design de API. A versão final lida com o caminho feliz (registro encontrado), a falha esperada (registro não encontrado) e uma verificação nula defensiva para cenários inesperados (consulta retornando nulo). A abordagem de Tim de trabalhar através desses casos ao vivo, em vez de apresentar código polido, demonstra o tipo de pensamento iterativo que endpoints de produção requerem.
Conclusão
[15:09 - 15:20] Adicionar um ponto de extremidade get-by-ID a uma API mínima envolve três decisões além da definição da rota: usar FirstOrDefault para desembrulhar a coleção, verificar nulo para distinguir "não encontrado" de "encontrado" e declarar TypedResults no tipo de retorno para que o framework retorne o código de status HTTP correto. O padrão Results<Ok<t>, NotFound> é reutilizável em qualquer ponto de extremidade que precise comunicar sucesso ou ausência.
Navegação na série: Este artigo faz parte da série C# no Linux construindo o app Tiny Ticket. Anterior: Adicionando Swagger UI. Próximo: Adicionando um Endpoint POST Insert.
Dica de Exemplo: Quando seu manipulador de API mínima retorna múltiplos códigos de status possíveis, sempre os declare no Results<> genérico. Isso gera documentação Swagger precisa automaticamente e força o compilador a verificar se cada caminho de código retorna um tipo de resultado válido.
Assista o vídeo completo no canal do YouTube dele e obtenha mais insights sobre como construir endpoints de API robustos na série C# no Linux.
