Linux에서 .NET Aspire에 ID로 가져오기 엔드포인트 추가하기
[[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"})]]
데이터베이스 테이블에서 모든 레코드를 반환하는 것은 목록 페이지에 유용하지만, 대부분의 API 소비자는 식별자별로 단일 레코드도 가져와야 합니다. 두 번째 엔드포인트는 "모든 가져오기" 라우트가 필요로 하지 않았던 결정을 소개합니다: 단일 객체에 대해 어떤 반환 유형이 적절한가, ID가 어떤 레코드와도 일치하지 않을 때 어떡하는가, 올바른 HTTP 상태 코드로 응답 실패를 호출자에게 어떻게 전달하는가.
Tim Corey는 그의 동영상 "Adding a Get By ID Endpoint in .NET Aspire on Linux"에서 Tiny Ticket API에 GET /api/tickets/{id} 엔드포인트를 추가합니다. 기존 경로의 복사-붙여넣기로 시작한 작업이, 배열 대신 단일 객체 반환, null 결과 확인, ID가 존재하지 않을 때 티켓과 함께 200 OK 또는 404 Not Found을 반환하기 위한 TypedResults를 사용하는 에지 케이스 처리의 라이브 워크스루로 바뀝니다. Linux에서 C# 시리즈를 따르거나 적절한 상태 코드 응답이 필요한 최소 API를 구축하고 있다면 이 에피소드는 전체 사고 과정을 다룹니다.
시작점으로 모든 가져오기 라우트 복사
[0:35 - 1:45] Tim이 API 서비스의 Program.cs를 열고 기존의 GET /api/tickets 엔드포인트를 복제합니다. 새 경로에는 티켓 ID를 위한 경로 매개변수가 필요하므로 URL 패턴이 {id:int}를 포함하도록 변경되고 처리기가 int id 매개변수를 받습니다. 저장 프로시저 참조가 spTickets_GetAll에서 ID 매개변수를 기대하는 spTickets_Get로 전환됩니다.
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;
});주의할 만한 명명 편의: 저장 프로시저 매개변수가 소문자 id로, C# 매개변수 이름과 정확히 일치합니다. 즉, Dapper는 속성 이름을 지정하지 않고 익명 객체 new { id }를 직접 매핑할 수 있습니다. SQL 매개변수가 다른 대소문자를 사용했다면 익명 객체에 new { Id = id }와 같은 명시적인 속성 할당이 필요했을 것입니다.
목록이 아닌 단일 객체 반환
[2:39 - 4:16] Swagger를 통한 첫 번째 테스트는 문제를 드러냅니다: ID 2를 전달하면 올바른 티켓이 포함된 200을 반환하지만 응답 본문은 JSON 배열로 감싸져 있습니다. 호출자가 ID로 단일 리소스를 요청할 때, 하나의 객체를 기대하며요, 단일 요소를 포함하는 배열이 아니라
쿼리 결과에 .FirstOrDefault()을 추가하여 래핑을 수정합니다. 목록에 항목이 있으면 FirstOrDefault가 첫 번째 요소를 반환하고, 목록이 비어 있으면 null를 반환합니다. 배열 문제는 해결되었지만 새로운 질문이 생깁니다: ID가 어떤 레코드와도 일치하지 않을 때 API는 무엇을 반환해야 합니까?
var output = tickets.FirstOrDefault();var output = tickets.FirstOrDefault();단일 줄 변경으로 기존 레코드에 대한 올바른 응답 형태를 생성합니다. 그러나 데이터베이스에 존재하지 않는 ID 4로 테스트할 때 더 깊은 간극이 드러납니다. 응답은 상태 코드 200과 함께 null으로 돌아옵니다. 이는 기술적으로 유효한 HTTP이지만, 호출자를 오도할 수 있습니다: 200은 요청이 성공했으며 리소스가 발견되었음을 의미하는데, 실제로는 아무 것도 일치하지 않았습니다.
TypedResults로 Not Found 처리
[4:41 - 11:44] 이 섹션은 Tim이 실시간으로 디자인 결정을 작업하는 곳으로, 최종 코드만 읽는 것이 아니라 시청하는 것이 가치가 있습니다. 그의 사고 과정은 여러 반복을 거칩니다:
먼저 그는 목록이 비어 있을 때 예외를 발생시키는 .FirstOrDefault() 대신 .First() 사용을 고려합니다. 이는 서버 버그보다는 자원이 없음을 암시하는 500 오류를 발생시켜 null 200보다 나쁩니다.
그런 다음 그는 물러서서 null 검사를 만듭니다. 결과를 변수에 저장하고 null인지 여부를 확인하여 각 경우에 대해 다른 응답을 반환합니다. 문제는 Minimal API 핸들러가 응답 형태가 여러 가지일 수 있을 때 명시적으로 반환 유형을 선언해야 한다는 것입니다.
솔루션은 메서드 서명에서 가능한 응답 유형들을 지정할 수 있는 TypedResults입니다:
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);
});그 반환 유형 Task<Results<Ok<TicketModel>, NotFound>>은 프레임워크(와 Swagger)에게 이 엔드포인트가 TicketModel 본문과 함께 200 또는 본문이 없는 404를 생성한다고 알려줍니다. VS Code에서 괄호 매칭 색상화는 일반적 결과 유형이 빨리 쌓이면서 중첩된 꺽쇠 괄호를 탐색하는 데 도움이 됩니다.
테스트 중에 발견된 미묘한 버그 하나: 첫 번째 버전은 TypedResults.NotFound()를 호출하지만 return하지 않습니다. 엔드포인트는 NotFound()이 유효한 표현식이기 때문에 컴파일되지만 return 키워드가 없으면 실행이 Ok 경로로 넘어갑니다. Tim은 Swagger가 여전히 누락된 ID에 대해 200을 보여줄 때 이 문제를 발견하고 return를 추가하며, 다음 실행에서는 404가 올바르게 나타납니다.
Swagger에서 두 경로를 테스트하기
[11:44 - 14:09] 최종 코드가 완료된 상태에서 Tim은 Swagger UI에서 두 시나리오 모두 실행합니다. ID 3을 전달하면 티켓 객체가 포함된 200이 반환됩니다. ID 4를 전달하면 본문이 비어 있는 404가 반환됩니다.
그는 또한 첫 사용자에게 혼란스러운 Swagger 인터페이스의 세부 사항을 지적합니다: 실행 버튼 아래의 "응답" 섹션은 가능한 응답 코드(200 및 404)를 보여주며 실제 결과는 아닙니다. 실제 서버 응답은 해당 섹션 위의 별도 패널에 나타납니다. 이 두 패널을 혼동하는 것은 "왜 200을 받고 있지?"라는 혼란의 일반적인 원인입니다.
TypedResults 접근 방식은 또한 자동으로 Swagger 문서를 개선합니다. 반환 유형이 Ok<TicketModel>와 NotFound를 모두 선언했기 때문에 Swagger는 해당 스키마와 함께 두 가지를 잠재적인 결과로 표시합니다. API 문서를 읽는 호출자는 개발자가 별도로 OpenAPI 주석을 작성하지 않아도 404 케이스를 처리해야 함을 알고 있습니다.
정리: 기능 전에 엣지 케이스
[14:09 - 15:09] 모든 가져오기 엔드포인트의 간단한 복사-붙여넣기로 시작한 것이 API 디자인에 대한 깊은 연습으로 발전하였습니다. 최종 버전은 행복 경로(레코드 발견), 예상된 실패(레코드 미발견), 예상치 않게 null이 반환되는 경우의 방어적인 null 검사를 처리합니다. Tim의 이러한 케이스를 실시간으로 작업하는 접근법은, 생산 엔드포인트가 요구하는 반복적 사고를 보여줍니다.
결론
[15:09 - 15:20] 최소 API에 ID로 검색 엔드포인트를 추가하는 것은 경로 정의 외에도 세 가지 결정을 포함합니다: 컬렉션을 해체하기 위해 FirstOrDefault를 사용하고, "못 찾음"과 "찾음"을 구분하기 위해 null을 확인하고, 프레임워크가 올바른 HTTP 상태 코드를 반환하도록 TypedResults를 반환 유형에 선언합니다. 성공 또는 부재를 알려야 하는 모든 엔드포인트에 대해 Results<Ok<t>, NotFound> 패턴을 재사용할 수 있습니다.
시리즈 내비게이션: 이 기사는 Linux에서 Tiny Ticket 앱을 구축하는 C# 시리즈의 일부입니다. 이전: Adding Swagger UI. 다음: POST 삽입 엔드포인트 추가하기.
예제 팁: 최소 API 핸들러가 여러 가능한 상태 코드를 반환할 때, 항상 Results<> 제네릭에서 이를 선언하세요. 이는 정확한 Swagger 문서를 자동으로 생성하고 컴파일러가 모든 코드 경로가 유효한 결과 유형을 반환하는지 확인하게 합니다.
전체 비디오를 그의 YouTube 채널에서 시청하고 Linux 시리즈의 C#에서 견고한 API 엔드포인트를 구축하는 방법에 대한 더 많은 통찰을 얻으세요.

