Ir para o conteúdo do rodapé
Iron Academy Logo
Aprenda C#
Aprenda C#

Outras categorias

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.

Hero Worlddot related to Adicionando um Endpoint Get By ID em .NET Aspire no Linux
Hero Affiliate related to Adicionando um Endpoint Get By ID em .NET Aspire no Linux

Ganhe mais compartilhando o que você ama.

Você cria conteúdo para desenvolvedores que trabalham com .NET, C#, Java, Python ou Node.js? Transforme sua expertise em renda extra!

Equipe de Suporte Iron

Estamos online 24 horas por dia, 5 dias por semana.
Bater papo
E-mail
Liga para mim