Note
이 문서의 최신 버전은 아닙니다. 현재 릴리스는 이 문서의 .NET 10 버전을 참조하세요.
Warning
이 버전의 ASP.NET Core는 더 이상 지원되지 않습니다. 자세한 내용은 .NET 및 .NET Core 지원 정책을 참조 하세요. 현재 릴리스는 이 문서의 .NET 10 버전을 참조하세요.
최소 API는 최소한의 종속성을 사용하여 HTTP API를 만들도록 설계되었습니다. ASP.NET Core에 최소 파일, 기능 및 종속성만 포함하려는 마이크로 서비스 및 앱에 이상적입니다.
이 자습서에서는 ASP.NET Core를 사용하여 최소 API를 빌드하는 기본 사항을 설명합니다. ASP.NET Core에서 API를 만드는 또 다른 방법은 컨트롤러를 사용하는 것입니다. 최소 API와 컨트롤러 기반 API 중에서 선택하는 방법에 대한 도움말은 API 개요를 참조하세요. 더 많은 기능이 포함된 컨트롤러를 기반으로 API 프로젝트를 만드는 방법에 대한 자습서는 웹 API 만들기를 참조하세요.
Overview
이 자습서에서는 다음 API를 만듭니다.
| API | Description | 요청 메시지 본문 | 응답 본문 |
|---|---|---|---|
GET /todoitems |
할 일 항목 모두 가져오기 | None | 할 일 항목의 배열 |
GET /todoitems/complete |
완성된 할 일 항목 가져오기 | None | 할 일 항목의 배열 |
GET /todoitems/{id} |
ID로 항목 가져오기 | None | 할 일 항목 |
POST /todoitems |
새 항목 추가 | 할 일 항목 | 할 일 항목 |
PUT /todoitems/{id} |
기존 항목 업데이트 | 할 일 항목 | None |
PATCH /todoitems/{id} |
항목 부분 업데이트 | 부분 할 일 항목 | None |
DELETE /todoitems/{id} |
항목 삭제 | None | None |
Prerequisites
ASP.NET 및 웹 개발 워크로드가 있는 최신 버전의 Visual Studio입니다.
API 프로젝트 만들기
2026년 Visual Studio 시작하고 새 프로젝트 만들기 선택합니다.
새 프로젝트 만들기 대화 상자에서 다음을 수행합니다.
- ASP.NET Core Web API 프로젝트 형식을 선택하고 다음을 선택합니다.
- 프로젝트 이름을 TodoApi로 지정하고 다음을 선택합니다.
추가 정보 대화 상자에서 다음을 수행합니다.
- Framework가 .NET 10.0(장기 지원)인지 확인합니다.
- OpenAPI 지원 사용 확인란이 선택되어 있는지 확인합니다.
- 컨트롤러 사용 확인란이 선택되어 있지 않은지 확인합니다. 컨트롤러 기반 프로젝트가 아닌 이 자습서에 필요한 최소 API 프로젝트를 만들려면 이 설정을 선택 취소합니다.
- Create를 선택합니다.
코드 검사
템플릿에서 생성된 파일에는 Program.cs 다음 코드가 포함되어 있습니다.
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi
builder.Services.AddOpenApi();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.MapOpenApi();
}
app.UseHttpsRedirection();
var summaries = new[]
{
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};
app.MapGet("/weatherforecast", () =>
{
var forecast = Enumerable.Range(1, 5).Select(index =>
new WeatherForecast
(
DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
Random.Shared.Next(-20, 55),
summaries[Random.Shared.Next(summaries.Length)]
))
.ToArray();
return forecast;
})
.WithName("GetWeatherForecast");
app.Run();
internal record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary)
{
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
}
앞의 코드가 하는 역할은 다음과 같습니다.
- 미리 구성된 기본값을 사용하여 WebApplicationBuilder 및 WebApplication을 만듭니다.
-
builder.Services.AddOpenApi()를 사용하여 기본 제공 OpenAPI 문서 생성기를 등록합니다. - 개발 환경에서만
/openapi/v1.json를 사용하여 생성된 OpenAPI 문서를app.MapOpenApi()에 매핑합니다. - 임의로 생성된 5개의 레코드를 반환하는 샘플
GET /weatherforecast엔드포인트를WeatherForecast정의합니다.
이 자습서에서는 WeatherForecast를 항목을 만들고, 읽고, 업데이트하고, 삭제하는 엔드포인트와 모델 및 데이터베이스를 기반으로 하는 새 Todo 샘플로 바꿉니다.
NuGet 패키지 추가
이 자습서에서 사용되는 데이터베이스를 지원하는 NuGet 패키지를 추가합니다.
- 도구 메뉴에서 NuGet 패키지 관리자 > 솔루션용 NuGet 패키지 관리를 선택합니다.
- 찾아보기 탭을 선택합니다.
- 검색 상자에 Microsoft.EntityFrameworkCore.InMemory를 입력한 다음
Microsoft.EntityFrameworkCore.InMemory를 선택합니다. - 오른쪽 창에서 프로젝트 확인란을 선택하고 설치를 선택합니다.
모델 및 데이터베이스 컨텍스트 클래스
- 프로젝트 폴더에서 다음 코드가 포함된
Todo.cs라는 파일을 만듭니다.
public class Todo
{
public int Id { get; set; }
public string? Name { get; set; }
public bool IsComplete { get; set; }
public string? Secret { get; set; }
}
앞의 코드는 이 앱에 대한 모델을 만듭니다. 모델은 앱에서 관리하는 데이터를 나타내는 일련의 클래스입니다.
이 Secret 속성은 일반적인 실제 필요성을 보여주기 위해 포함됩니다. 앱이 내부적으로 저장하고 사용하는 데이터(예: 항목을 소유한 사용자의 ID)는 클라이언트가 보거나 설정하지 않도록 합니다. 이 자습서 뒷부분의 초과 게시 방지 단계에서는 DTO(데이터 전송 객체)를 사용하여 Secret와 같은 필드가 API의 입력 및 응답에 포함되지 않도록 합니다.
- 다음 코드를 사용하여
TodoDb.cs라는 파일을 만듭니다.
using Microsoft.EntityFrameworkCore;
class TodoDb : DbContext
{
public TodoDb(DbContextOptions<TodoDb> options)
: base(options) { }
public DbSet<Todo> Todos => Set<Todo>();
}
앞의 코드는 데이터 모델에 대한 Entity Framework 기능을 조정하는 기본 클래스인 데이터베이스 컨텍스트를 정의합니다. 이 클래스는 Microsoft.EntityFrameworkCore.DbContext 클래스에서 파생됩니다.
앱은 라는 TodoList메모리 내 데이터베이스를 사용합니다. 데이터베이스는 나중에 Program.cs에서 builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));을 사용하여 등록되며, 여기서 문자열 "TodoList"는 데이터베이스 이름입니다. 데이터는 메모리에 저장되므로 앱이 다시 시작될 때마다 다시 설정됩니다.
WeatherForecast 샘플을 Todo API로 바꾸기
템플릿은 webapi에 샘플 GET /weatherforecast 엔드포인트를 추가하고 파일 맨 아래에 Program.cs 레코드를 추가합니다. 샘플은 템플릿이 제대로 작동함을 보여 주기 위한 자리 표시자일 뿐이므로, 둘 다 다음 섹션에 설명된 Todo 엔드포인트로 교체하세요.
- 모든 코드를
Program.cs다음으로 바꿉다.WeatherForecast샘플 엔드포인트와 레코드가 제거되고 Todo 엔드포인트가 해당 위치를 차지합니다.
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
builder.Services.AddOpenApi();
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.MapOpenApi();
}
app.MapGet("/todoitems", async (TodoDb db) =>
await db.Todos.ToListAsync());
app.MapGet("/todoitems/complete", async (TodoDb db) =>
await db.Todos.Where(t => t.IsComplete).ToListAsync());
app.MapGet("/todoitems/{id}", async (int id, TodoDb db) =>
await db.Todos.FindAsync(id)
is Todo todo
? Results.Ok(todo)
: Results.NotFound());
app.MapPost("/todoitems", async (Todo todo, TodoDb db) =>
{
db.Todos.Add(todo);
await db.SaveChangesAsync();
return Results.Created($"/todoitems/{todo.Id}", todo);
});
app.MapPut("/todoitems/{id}", async (int id, Todo inputTodo, TodoDb db) =>
{
var todo = await db.Todos.FindAsync(id);
if (todo is null) return Results.NotFound();
todo.Name = inputTodo.Name;
todo.IsComplete = inputTodo.IsComplete;
await db.SaveChangesAsync();
return Results.NoContent();
});
app.MapDelete("/todoitems/{id}", async (int id, TodoDb db) =>
{
if (await db.Todos.FindAsync(id) is Todo todo)
{
db.Todos.Remove(todo);
await db.SaveChangesAsync();
return Results.NoContent();
}
return Results.NotFound();
});
app.Run();
템플릿이 추가하는 OpenAPI 줄(builder.Services.AddOpenApi() 및 app.MapOpenApi())은 그대로 유지됩니다. 이제 샘플 날씨 엔드포인트 대신 Todo 엔드포인트를 설명합니다.
강조 표시된 다음 코드는 DI(종속성 주입) 컨테이너에 앱의 서비스를 등록합니다. 데이터베이스 컨텍스트(AddDbContext) 및 OpenAPI 문서 생성기(AddOpenApi).
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
builder.Services.AddOpenApi();
var app = builder.Build();
DI 컨테이너는 데이터베이스 컨텍스트 및 기타 서비스에 대한 액세스를 제공합니다.
이 자습서에서는 엔드포인트 탐색기 및 .http 파일을 사용하여 API를 테스트합니다.
데이터 게시 테스트
Program.cs에서 다음 코드는 HTTP POST 엔드포인트 /todoitems를 만들어 메모리 내 데이터베이스에 데이터를 추가합니다.
app.MapPost("/todoitems", async (Todo todo, TodoDb db) =>
{
db.Todos.Add(todo);
await db.SaveChangesAsync();
return Results.Created($"/todoitems/{todo.Id}", todo);
});
- Ctrl+F5 키를 눌러 디버깅하지 않고 앱을 실행합니다. Visual Studio 웹 서버를 시작하고Kestrel 필요한 경우 개발 인증서를 신뢰합니다.
Visual Studio는 다음 대화 상자를 표시합니다.
IIS Express SSL 인증서를 신뢰하는 경우 예를 선택합니다.
다음 대화 상자가 표시됩니다.
개발 인증서를 신뢰하는 데 동의하는 경우 예를 선택합니다.
Firefox 브라우저를 신뢰하는 방법에 대한 자세한 내용은 Firefox SEC_ERROR_INADEQUATE_KEY_USAGE 인증서 오류를 참조하세요.
POST 엔드포인트를 사용하여 앱에 데이터를 추가합니다.
선택 보기>다른 Windows>Endpoints 탐색기.
POST 엔드포인트를 마우스 오른쪽 단추로 클릭하고 요청 생성을 선택합니다.
다음 예제와 유사한 내용이 포함된 새 파일이 프로젝트 폴더
TodoApi.http에 만들어집니다.
@TodoApi_HostAddress = https://localhost:7031
POST {{TodoApi_HostAddress}}/todoitems
###
첫 번째 줄은 모든 엔드포인트에 사용되는 변수를 만듭니다.
다음 줄은 POST 요청을 정의합니다.
삼중 해시 태그(
###) 줄은 요청 구분 기호입니다. 이는 다른 요청에 대한 것입니다.POST 요청에는 헤더와 본문이 필요합니다. 요청의 해당 부분을 정의하려면 POST 요청 줄 바로 다음에 다음 줄을 추가합니다.
Content-Type: application/json
{
"name":"walk dog",
"isComplete":true
}
앞의 코드는 Content-Type 헤더와 JSON 요청 본문을 추가합니다. 이제 TodoApi.http 파일은 다음 예제와 비슷하게 보이지만, 사용자 본인의 포트 번호를 사용해야 합니다.
@TodoApi_HostAddress = https://localhost:7057
POST {{TodoApi_HostAddress}}/todoitems
Content-Type: application/json
{
"name":"walk dog",
"isComplete":true
}
###
요청 줄 위에
POST있는 요청 보내기 링크를 선택합니다.
POST 요청이 앱으로 전송되고 응답 창에 응답이 표시됩니다.
GET 엔드포인트 검사
사용자의 Program.cs에는 MapGet로 정의된 여러 GET 엔드포인트가 포함되어 있습니다.
| API | Description | 요청 메시지 본문 | 응답 본문 |
|---|---|---|---|
GET /todoitems |
할 일 항목 모두 가져오기 | None | 할 일 항목의 배열 |
GET /todoitems/complete |
완성된 모든 할 일 항목 가져오기 | None | 할 일 항목의 배열 |
GET /todoitems/{id} |
ID로 항목 가져오기 | None | 할 일 항목 |
app.MapGet("/todoitems", async (TodoDb db) =>
await db.Todos.ToListAsync());
app.MapGet("/todoitems/complete", async (TodoDb db) =>
await db.Todos.Where(t => t.IsComplete).ToListAsync());
app.MapGet("/todoitems/{id}", async (int id, TodoDb db) =>
await db.Todos.FindAsync(id)
is Todo todo
? Results.Ok(todo)
: Results.NotFound());
GET 엔드포인트 테스트
브라우저에서 엔드포인트를 GET 호출하거나 엔드포인트 탐색기를 사용하여 앱을 테스트합니다. 다음 단계는 엔드포인트 탐색기를 위한 것입니다.
엔드포인트 탐색기에서 첫 번째 GET 엔드포인트를 마우스 오른쪽 단추로 클릭하고 요청 생성을 선택합니다.
다음 콘텐츠가
TodoApi.http파일에 추가됩니다.
GET {{TodoApi_HostAddress}}/todoitems
###
새로운 요청 줄 위에 있는 요청 보내기 링크를 선택합니다.
GET 요청이 앱으로 전송되고 응답 창에 응답이 표시됩니다.
응답 본문은 다음 JSON과 유사합니다.
[
{
"id": 1,
"name": "walk dog",
"isComplete": true,
"secret": null
}
]
- 엔드포인트 탐색기에서 GET 엔드포인트를 마우스 오른쪽 버튼으로 클릭하고
/todoitems/{id}을 선택하세요. 다음 콘텐츠가TodoApi.http파일에 추가됩니다.
GET {{TodoApi_HostAddress}}/todoitems/{id}
###
{id}를1로 교체합니다.새 GET 요청 줄 위에 있는 요청 보내기 링크를 선택합니다.
GET 요청이 앱으로 전송되고 응답 창에 응답이 표시됩니다.
응답 본문은 다음 JSON과 유사합니다.
{
"id": 1,
"name": "walk dog",
"isComplete": true,
"secret": null
}
이 앱은 메모리 내 데이터베이스를 사용합니다. 앱을 다시 시작하면 데이터가 유실됩니다. GET /todoitems는 빈 배열([])을 반환하고, GET /todoitems/{id}는 404 Not Found를 반환합니다. 앱을 다시 채워보려면 데이터를 앱에 게시 하고 GET 요청을 다시 시도합니다.
반환 값
ASP.NET Core는 자동으로 JSON에 개체를 직렬화하고 JSON을 응답 메시지의 본문에 기록합니다. 이 반환 형식의 응답 코드는 200 OK이며 처리되지 않은 예외가 없다고 가정합니다. 처리되지 않은 예외는 5xx 오류로 변환됩니다.
반환 형식은 다양한 HTTP 상태 코드를 나타낼 수 있습니다. 예를 들어 GET /todoitems/{id}은 두 가지 상태 값을 반환할 수 있습니다.
- 요청된 ID와 일치하는 항목이 없는 경우 메서드에서 404 상태NotFound 오류 코드를 반환합니다.
- 그렇지 않으면 메서드가 JSON 응답 본문에서 200을 반환합니다.
item을 반환하면 HTTP 200 응답이 발생합니다.
PUT 엔드포인트 검사
샘플 앱은 MapPut을 사용하여 단일 PUT 엔드포인트를 구현합니다.
app.MapPut("/todoitems/{id}", async (int id, Todo inputTodo, TodoDb db) =>
{
var todo = await db.Todos.FindAsync(id);
if (todo is null) return Results.NotFound();
todo.Name = inputTodo.Name;
todo.IsComplete = inputTodo.IsComplete;
await db.SaveChangesAsync();
return Results.NoContent();
});
이 메서드는 HTTP PUT을 사용한다는 점을 제외하고 MapPost 메서드와 비슷합니다. 성공적인 응답은 204(콘텐츠 없음)를 반환합니다. HTTP 사양에 따라 PUT 요청의 경우 클라이언트는 변경 내용만이 아니라 전체 업데이트된 엔터티를 보내야 합니다. 부분 업데이트를 지원하려면 HTTP PATCH를 사용합니다.
PUT 엔드포인트 테스트
이 샘플에서는 앱을 시작할 때마다 초기화해야 하는 메모리 내 데이터베이스를 사용합니다. PUT 호출을 하기 전에 데이터베이스에 항목이 필요합니다. PUT을 호출하기 전에 POST를 호출하여 데이터베이스에 항목이 있는지 확인합니다.
Id = 1이 있는 Todo 항목을 업데이트하고 이름을 "feed fish"로 설정합니다.
엔드포인트 탐색기에서 PUT 엔드포인트를 마우스 오른쪽 단추로 클릭하고 요청 생성을 선택합니다.
다음 콘텐츠가
TodoApi.http파일에 추가됩니다.
PUT {{TodoApi_HostAddress}}/todoitems/{id}
###
PUT 요청 줄에서 `
{id}`를 `1`로 교체하세요.PUT 요청 줄 바로 다음에 다음 줄을 추가합니다.
Content-Type: application/json
{
"id": 1,
"name": "feed fish",
"isComplete": false
}
앞의 코드는 Content-Type 헤더와 JSON 요청 본문을 추가합니다.
- 새 PUT 요청 줄 위에 있는 요청 보내기 링크를 선택합니다.
PUT 요청이 앱으로 전송되고 응답 창에 응답이 표시됩니다. 응답 본문이 비어 있고 상태 코드는 204입니다.
PATCH 엔드포인트 만들기 및 검사
PATCH 엔드포인트를 사용하면 클라이언트가 완료 상태를 다시 보내지 않고 Todo 항목의 이름을 바꾸는 등 업데이트하려는 필드만 보낼 수 있습니다. 이 방법은 전체 항목을 대체하는 PUT 요청과 다르므로 클라이언트는 하나만 변경되더라도 모든 필드를 보내야 합니다.
다음 단계에서는 새 파일을 추가하고 파일을 수정합니다 Program.cs . 이러한 변경을 수행하기 전에 TodoApi 앱을 중지하세요. 브라우저에서 스칼라 페이지를 실행 상태로 둡니다.
- 앱을 중지하려면 Visual Studio 도구 모음에서 중지 단추(빨간색 사각형)를 선택합니다.
이 샘플에서는 앱이 시작될 때마다 초기화해야 하는 메모리 내 데이터베이스를 사용합니다. PATCH를 호출하기 전에 데이터베이스에 항목이 포함되어야 합니다. POST를 호출하여 PATCH를 호출하기 전에 데이터베이스에 항목이 있는지 확인합니다.
PATCH 엔드포인트는 nullable 속성이 있는 TodoPatchDto 클래스를 사용하여 부분 업데이트를 제대로 처리합니다. nullable 속성을 사용하면 엔드포인트가 제공되지 않은 필드(null)와 명시적으로 값으로 설정된 필드(부울 필드의 경우 false 포함)를 구분할 수 있습니다. nullable 속성이 없으면 nullable이 아닌 bool은 기본적으로 false로 설정되며, 해당 필드가 요청에 포함되지 않은 경우 기존 true 값을 덮어쓸 수 있습니다.
- 다음 코드를 사용하여
TodoPatchDto.cs라는 파일을 만듭니다.
public class TodoPatchDto
{
public string? Name { get; set; }
public bool? IsComplete { get; set; }
}
클래스는 TodoPatchDto nullable 속성(string? 및)을 사용하여 요청에 제공되지 않은 필드와 bool?명시적으로 값으로 설정된 필드를 구분합니다.
-
Program.cs에서MapPut엔드포인트 바로 다음에 다음 PATCH 엔드포인트를 추가합니다.
app.MapPatch("/todoitems/{id}", async (int id, TodoPatchDto inputTodo, TodoDb db) =>
{
var todo = await db.Todos.FindAsync(id);
if (todo is null) return Results.NotFound();
if (inputTodo.Name is not null) todo.Name = inputTodo.Name;
if (inputTodo.IsComplete is not null) todo.IsComplete = inputTodo.IsComplete.Value;
await db.SaveChangesAsync();
return Results.NoContent();
});
이 메서드는 메서드와 비슷하 MapPut 지만 HTTP PATCH를 사용하고 요청에 제공된 필드만 업데이트합니다. 성공적인 응답은 204(콘텐츠 없음)를 반환합니다.
Note
PATCH 작업을 통해 리소스를 부분적으로 업데이트할 수 있습니다. JSON 패치 문서를 사용하는 고급 부분 업데이트는 ASP.NET Core 웹 API의 JsonPatch를 참조하세요.
PATCH 엔드포인트를 테스트하십시오
- Ctrl+F5 키를 눌러 새 PATCH 엔드포인트를 사용하여 앱을 다시 빌드하고 실행합니다.
이 샘플에서는 앱이 시작될 때마다 초기화해야 하는 메모리 내 데이터베이스를 사용합니다. PATCH를 호출하기 전에 데이터베이스에 항목이 포함되어야 합니다. POST를 호출하여 PATCH를 호출하기 전에 데이터베이스에 항목이 있는지 확인합니다.
Todo 항목 name 의 Id = 1 속성만 업데이트하고 이름을 "run errands".로 설정합니다.
엔드포인트 탐색기에서 새로 고침 단추를 선택합니다. 그런 다음 PATCH 엔드포인트를 마우스 오른쪽 단추로 클릭하고 요청 생성을 선택합니다.
다음 콘텐츠가
TodoApi.http파일에 추가됩니다.
PATCH {{TodoApi_HostAddress}}/todoitems/{id}
###
PATCH 요청 줄에서 `
{id}`를 `1`로 교체합니다.PATCH 요청 줄 바로 다음에 다음 줄을 추가합니다.
Content-Type: application/json
{
"name": "run errands"
}
앞의 코드는 업데이트할 필드만 있는 Content-Type 헤더 및 JSON 요청 본문을 추가합니다.
- 새 PATCH 요청 줄 위에 있는 요청 보내기 링크를 선택합니다.
PATCH 요청이 앱으로 전송되고 응답 창에 응답 이 표시됩니다. 응답 본문이 비어 있고 상태 코드는 204입니다.
DELETE 엔드포인트 살펴보기
Program.cs 파일에는 MapDelete로 정의된 단일 DELETE 엔드포인트가 포함되어 있습니다:
app.MapDelete("/todoitems/{id}", async (int id, TodoDb db) =>
{
if (await db.Todos.FindAsync(id) is Todo todo)
{
db.Todos.Remove(todo);
await db.SaveChangesAsync();
return Results.NoContent();
}
return Results.NotFound();
});
DELETE 엔드포인트를 테스트하세요
엔드포인트 탐색기에서 DELETE 엔드포인트를 마우스 오른쪽 단추로 클릭하고 요청 생성을 선택합니다.
DELETE 요청이
TodoApi.http에 추가됩니다.DELETE 요청 줄에서
{id}을1으로 바꾸세요. DELETE 요청은 다음 예제와 같습니다.
DELETE {{TodoApi_HostAddress}}/todoitems/1
###
- DELETE 요청에 대한 요청 보내기 링크를 선택합니다.
DELETE 요청이 앱으로 전송되고 응답 창에 응답이 표시됩니다. 응답 본문이 비어 있고 상태 코드는 204입니다.
MapGroup API 사용
- 다음 단계에서는
Program.cs파일을 수정하므로 이러한 변경을 수행하기 전에 앱을 중지하세요. 브라우저에서 스칼라 페이지를 실행 상태로 둡니다.
- 앱을 중지하려면 Visual Studio 도구 모음에서 중지 단추(빨간색 사각형)를 선택합니다.
작성한 Program.cs 파일은 엔드포인트를 설정할 때마다 todoitems URL 접두사를 반복해서 사용합니다. API에는 공통 URL 접두사를 사용하는 엔드포인트 그룹이 있는 경우가 많으며, 이 메서드는 MapGroup 이러한 그룹을 구성하는 데 도움이 됩니다. 반복 코드를 줄이고, 메서드와 같은 RequireAuthorizationWithMetadata메서드를 한 번의 호출로 전체 엔드포인트 그룹을 사용자 지정할 수 있습니다.
-
Program.cs의 내용을 다음 코드로 바꿉니다.
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
builder.Services.AddOpenApi();
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.MapOpenApi();
}
var todoItems = app.MapGroup("/todoitems");
todoItems.MapGet("/", async (TodoDb db) =>
await db.Todos.ToListAsync());
todoItems.MapGet("/complete", async (TodoDb db) =>
await db.Todos.Where(t => t.IsComplete).ToListAsync());
todoItems.MapGet("/{id}", async (int id, TodoDb db) =>
await db.Todos.FindAsync(id)
is Todo todo
? Results.Ok(todo)
: Results.NotFound());
todoItems.MapPost("/", async (Todo todo, TodoDb db) =>
{
db.Todos.Add(todo);
await db.SaveChangesAsync();
return Results.Created($"/todoitems/{todo.Id}", todo);
});
todoItems.MapPut("/{id}", async (int id, Todo inputTodo, TodoDb db) =>
{
var todo = await db.Todos.FindAsync(id);
if (todo is null) return Results.NotFound();
todo.Name = inputTodo.Name;
todo.IsComplete = inputTodo.IsComplete;
await db.SaveChangesAsync();
return Results.NoContent();
});
todoItems.MapPatch("/{id}", async (int id, TodoPatchDto inputTodo, TodoDb db) =>
{
var todo = await db.Todos.FindAsync(id);
if (todo is null) return Results.NotFound();
if (inputTodo.Name is not null) todo.Name = inputTodo.Name;
if (inputTodo.IsComplete is not null) todo.IsComplete = inputTodo.IsComplete.Value;
await db.SaveChangesAsync();
return Results.NoContent();
});
todoItems.MapDelete("/{id}", async (int id, TodoDb db) =>
{
if (await db.Todos.FindAsync(id) is Todo todo)
{
db.Todos.Remove(todo);
await db.SaveChangesAsync();
return Results.NoContent();
}
return Results.NotFound();
});
app.Run();
위의 코드에는 다음과 같은 변경 내용이 있습니다.
URL 접두사
var todoItems = app.MapGroup("/todoitems");을 사용하여 그룹을 설정하도록/todoitems을 추가합니다.모든
app.Map<HttpVerb>메서드를todoItems.Map<HttpVerb>로 변경합니다./todoitems메서드 호출에서 URL 접두사Map<HttpVerb>를 제거합니다.앱을 실행하고 엔드포인트를 테스트하여 동일하게 작동하는지 확인합니다.
TypedResults API 사용
다음 단계에서는 Program.cs 파일을 수정하므로 이러한 변경을 수행하기 전에 앱을 중지하세요. 브라우저에서 스칼라 페이지를 실행 상태로 둡니다.
TypedResults을 반환하는 것은 Results을 반환하는 것보다 테스트 용이성, 그리고 OpenAPI가 엔드포인트를 설명하기 위한 응답 형식 메타데이터를 자동으로 반환하는 등 여러 가지 이점이 있습니다. 자세한 내용은 TypedResults vs Results를 참조하세요.
-
Map<HttpVerb>메서드는 람다를 사용하는 대신 경로 처리기 메서드를 호출할 수 있습니다. 예제를 보려면 다음 코드로 Program.cs를 업데이트합니다.
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
builder.Services.AddOpenApi();
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.MapOpenApi();
}
var todoItems = app.MapGroup("/todoitems");
todoItems.MapGet("/", GetAllTodos);
todoItems.MapGet("/complete", GetCompleteTodos);
todoItems.MapGet("/{id}", GetTodo);
todoItems.MapPost("/", CreateTodo);
todoItems.MapPut("/{id}", UpdateTodo);
todoItems.MapPatch("/{id}", PatchTodo);
todoItems.MapDelete("/{id}", DeleteTodo);
app.Run();
static async Task<IResult> GetAllTodos(TodoDb db)
{
return TypedResults.Ok(await db.Todos.ToArrayAsync());
}
static async Task<IResult> GetCompleteTodos(TodoDb db)
{
return TypedResults.Ok(await db.Todos.Where(t => t.IsComplete).ToListAsync());
}
static async Task<IResult> GetTodo(int id, TodoDb db)
{
return await db.Todos.FindAsync(id)
is Todo todo
? TypedResults.Ok(todo)
: TypedResults.NotFound();
}
static async Task<IResult> CreateTodo(Todo todo, TodoDb db)
{
db.Todos.Add(todo);
await db.SaveChangesAsync();
return TypedResults.Created($"/todoitems/{todo.Id}", todo);
}
static async Task<IResult> UpdateTodo(int id, Todo inputTodo, TodoDb db)
{
var todo = await db.Todos.FindAsync(id);
if (todo is null) return TypedResults.NotFound();
todo.Name = inputTodo.Name;
todo.IsComplete = inputTodo.IsComplete;
await db.SaveChangesAsync();
return TypedResults.NoContent();
}
static async Task<IResult> PatchTodo(int id, TodoPatchDto inputTodo, TodoDb db)
{
var todo = await db.Todos.FindAsync(id);
if (todo is null) return TypedResults.NotFound();
if (inputTodo.Name is not null) todo.Name = inputTodo.Name;
if (inputTodo.IsComplete is not null) todo.IsComplete = inputTodo.IsComplete.Value;
await db.SaveChangesAsync();
return TypedResults.NoContent();
}
static async Task<IResult> DeleteTodo(int id, TodoDb db)
{
if (await db.Todos.FindAsync(id) is Todo todo)
{
db.Todos.Remove(todo);
await db.SaveChangesAsync();
return TypedResults.NoContent();
}
return TypedResults.NotFound();
}
이제 Map<HttpVerb> 코드는 람다 대신 메서드를 호출합니다.
var todoItems = app.MapGroup("/todoitems");
todoItems.MapGet("/", GetAllTodos);
todoItems.MapGet("/complete", GetCompleteTodos);
todoItems.MapGet("/{id}", GetTodo);
todoItems.MapPost("/", CreateTodo);
todoItems.MapPut("/{id}", UpdateTodo);
todoItems.MapPatch("/{id}", PatchTodo);
todoItems.MapDelete("/{id}", DeleteTodo);
다음 메서드는 IResult를 구현하고 TypedResults에 의해 정의된 개체를 반환합니다.
static async Task<IResult> GetAllTodos(TodoDb db)
{
return TypedResults.Ok(await db.Todos.ToArrayAsync());
}
static async Task<IResult> GetCompleteTodos(TodoDb db)
{
return TypedResults.Ok(await db.Todos.Where(t => t.IsComplete).ToListAsync());
}
static async Task<IResult> GetTodo(int id, TodoDb db)
{
return await db.Todos.FindAsync(id)
is Todo todo
? TypedResults.Ok(todo)
: TypedResults.NotFound();
}
static async Task<IResult> CreateTodo(Todo todo, TodoDb db)
{
db.Todos.Add(todo);
await db.SaveChangesAsync();
return TypedResults.Created($"/todoitems/{todo.Id}", todo);
}
static async Task<IResult> UpdateTodo(int id, Todo inputTodo, TodoDb db)
{
var todo = await db.Todos.FindAsync(id);
if (todo is null) return TypedResults.NotFound();
todo.Name = inputTodo.Name;
todo.IsComplete = inputTodo.IsComplete;
await db.SaveChangesAsync();
return TypedResults.NoContent();
}
static async Task<IResult> PatchTodo(int id, TodoPatchDto inputTodo, TodoDb db)
{
var todo = await db.Todos.FindAsync(id);
if (todo is null) return TypedResults.NotFound();
if (inputTodo.Name is not null) todo.Name = inputTodo.Name;
if (inputTodo.IsComplete is not null) todo.IsComplete = inputTodo.IsComplete.Value;
await db.SaveChangesAsync();
return TypedResults.NoContent();
}
static async Task<IResult> DeleteTodo(int id, TodoDb db)
{
if (await db.Todos.FindAsync(id) is Todo todo)
{
db.Todos.Remove(todo);
await db.SaveChangesAsync();
return TypedResults.NoContent();
}
return TypedResults.NotFound();
}
단위 테스트는 이러한 메서드를 호출하고 올바른 형식을 반환하는지 테스트할 수 있습니다. 예를 들어 메서드가 GetAllTodos인 경우:
static async Task<IResult> GetAllTodos(TodoDb db)
{
return TypedResults.Ok(await db.Todos.ToArrayAsync());
}
단위 테스트 코드는 Ok<Todo[]> 형식의 개체가 처리기 메서드에서 반환되는지 확인할 수 있습니다. 다음은 그 예입니다.
public async Task GetAllTodos_ReturnsOkOfTodosResult()
{
// Arrange
var db = CreateDbContext();
// Act
var result = await TodosApi.GetAllTodos(db);
// Assert: Check for the correct returned type
Assert.IsType<Ok<Todo[]>>(result);
}
초과 게시 방지
현재 API는 Todo에 추가한 Secret 속성을 포함하여 전체 개체를 노출합니다. 프로덕션 애플리케이션에서 모델의 하위 집합을 사용하여 클라이언트가 입력하고 받을 수 있는 데이터를 제한합니다. 보안이 이러한 제한의 주요 원인입니다. 모델의 이 하위 집합을 일반적으로 DTO(데이터 전송 개체), 입력 모델 또는 뷰 모델이라고 합니다. 이 문서에서는 DTO를 사용합니다.
DTO를 사용하여 다음을 수행합니다.
- 초과 게시를 방지합니다.
- 클라이언트에서 볼 수 없는 속성을 숨깁니다.
- 페이로드 크기를 줄이기 위해 일부 속성을 생략합니다.
- 중첩된 개체를 포함하는 개체 그래프를 평면화합니다. 클라이언트에는 평면화된 개체 그래프가 더 편리할 수 있습니다.
이 앱은 Secret 필드를 숨겨야 하지만 관리 앱이 필드를 노출하도록 선택할 수 있습니다.
다음 단계에서는 파일을 추가하므로 변경하기 전에 앱을 중지합니다. 브라우저에서 스칼라 페이지를 실행 상태로 둡니다.
다음 코드를 사용하여
TodoItemDTO.cs라는 파일을 만듭니다.
public class TodoItemDTO
{
public int Id { get; set; }
public string? Name { get; set; }
public bool IsComplete { get; set; }
public TodoItemDTO() { }
public TodoItemDTO(Todo todoItem) =>
(Id, Name, IsComplete) = (todoItem.Id, todoItem.Name, todoItem.IsComplete);
}
- 이 DTO 모델을 사용하려면 파일의
Program.cs내용을 다음 코드로 바꿉니다.
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
builder.Services.AddOpenApi();
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.MapOpenApi();
}
var todoItems = app.MapGroup("/todoitems");
todoItems.MapGet("/", GetAllTodos);
todoItems.MapGet("/complete", GetCompleteTodos);
todoItems.MapGet("/{id}", GetTodo);
todoItems.MapPost("/", CreateTodo);
todoItems.MapPut("/{id}", UpdateTodo);
todoItems.MapPatch("/{id}", PatchTodo);
todoItems.MapDelete("/{id}", DeleteTodo);
app.Run();
static async Task<IResult> GetAllTodos(TodoDb db)
{
return TypedResults.Ok(await db.Todos.Select(x => new TodoItemDTO(x)).ToArrayAsync());
}
static async Task<IResult> GetCompleteTodos(TodoDb db)
{
return TypedResults.Ok(await db.Todos.Where(t => t.IsComplete).Select(x => new TodoItemDTO(x)).ToListAsync());
}
static async Task<IResult> GetTodo(int id, TodoDb db)
{
return await db.Todos.FindAsync(id)
is Todo todo
? TypedResults.Ok(new TodoItemDTO(todo))
: TypedResults.NotFound();
}
static async Task<IResult> CreateTodo(TodoItemDTO todoItemDTO, TodoDb db)
{
var todoItem = new Todo
{
IsComplete = todoItemDTO.IsComplete,
Name = todoItemDTO.Name
};
db.Todos.Add(todoItem);
await db.SaveChangesAsync();
todoItemDTO = new TodoItemDTO(todoItem);
return TypedResults.Created($"/todoitems/{todoItem.Id}", todoItemDTO);
}
static async Task<IResult> UpdateTodo(int id, TodoItemDTO todoItemDTO, TodoDb db)
{
var todo = await db.Todos.FindAsync(id);
if (todo is null) return TypedResults.NotFound();
todo.Name = todoItemDTO.Name;
todo.IsComplete = todoItemDTO.IsComplete;
await db.SaveChangesAsync();
return TypedResults.NoContent();
}
static async Task<IResult> PatchTodo(int id, TodoPatchDto inputTodo, TodoDb db)
{
var todo = await db.Todos.FindAsync(id);
if (todo is null) return TypedResults.NotFound();
if (inputTodo.Name is not null) todo.Name = inputTodo.Name;
if (inputTodo.IsComplete is not null) todo.IsComplete = inputTodo.IsComplete.Value;
await db.SaveChangesAsync();
return TypedResults.NoContent();
}
static async Task<IResult> DeleteTodo(int id, TodoDb db)
{
if (await db.Todos.FindAsync(id) is Todo todo)
{
db.Todos.Remove(todo);
await db.SaveChangesAsync();
return TypedResults.NoContent();
}
return TypedResults.NotFound();
}
- 앱을 실행하고 필드를 제외한
Secret모든 필드를 게시하고 가져올 수 있는지 확인합니다.
완료된 샘플을 사용한 문제 해결
해결할 수 없는 문제가 발생한 경우 완료된 프로젝트와 코드를 비교합니다. 완료된 프로젝트를 보거나 다운로드합니다(다운로드 방법).
다음 단계
- JSON serialization 옵션을 구성합니다.
- 오류 및 예외 처리: 최소 API 앱에 대한 환경에서는 개발자 예외 페이지 가
Development기본적으로 사용하도록 설정됩니다. 오류 및 예외를 처리하는 방법에 대한 자세한 내용은 ASP.NET Core API에서 오류 처리를 참조하세요. - 최소 API 앱을 테스트하는 예제는 이 GitHub 샘플을 참조하세요.
- 최소 API에서 OpenAPI를 지원합니다.
- 빠른 시작: Azure에 게시합니다.
- ASP.NET Core 최소 API 구성
Learn more
최소 API 빠른 참조 참조하세요.
최소 API는 최소한의 종속성으로 HTTP API를 만들도록 설계되었습니다. ASP.NET Core에 최소 파일, 기능, 종속성만 포함하려는 앱과 마이크로 서비스에 적합합니다.
이 자습서에서는 ASP.NET Core를 사용하여 최소 API를 빌드하는 기본 사항을 설명합니다. ASP.NET Core에서 API를 만드는 또 다른 방법은 컨트롤러를 사용하는 것입니다. 최소 API와 컨트롤러 기반 API 중에서 선택하는 방법에 대한 도움말은 API 개요를 참조하세요. 더 많은 기능이 포함된 컨트롤러를 기반으로 API 프로젝트를 만드는 방법에 대한 자습서는 웹 API 만들기를 참조하세요.
Overview
이 자습서에서는 다음 API를 만듭니다.
| API | Description | 요청 메시지 본문 | 응답 본문 |
|---|---|---|---|
GET /todoitems |
할 일 항목 모두 가져오기 | None | 할 일 항목의 배열 |
GET /todoitems/complete |
완성된 할 일 항목 가져오기 | None | 할 일 항목의 배열 |
GET /todoitems/{id} |
ID로 항목 가져오기 | None | 할 일 항목 |
POST /todoitems |
새 항목 추가 | 할 일 항목 | 할 일 항목 |
PUT /todoitems/{id} |
기존 항목 업데이트 | 할 일 항목 | None |
PATCH /todoitems/{id} |
항목 부분 업데이트 | 부분 할 일 항목 | None |
DELETE /todoitems/{id} |
항목 삭제 | None | None |
Prerequisites
ASP.NET 및 웹 개발 워크로드가 있는 Visual Studio 2022
API 프로젝트 만들기
Visual Studio 2022를 시작하고 새 프로젝트 만들기를 선택합니다.
새 프로젝트 만들기 대화 상자에서 다음을 수행합니다.
-
Empty검색 상자에 를 입력합니다. - ASP.NET Core Empty 템플릿을 선택하고 다음을 선택합니다.
-
프로젝트 이름을 TodoApi로 지정하고 다음을 선택합니다.
추가 정보 대화 상자에서 다음을 수행합니다.
- .NET 7.0 선택
- 최상위 문 사용 안 함 옵션의 체크를 해제
- 만들기를 선택하세요
코드 검사
Program.cs 파일에는 다음 코드가 포함되어 있습니다.
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/", () => "Hello World!");
app.Run();
앞의 코드가 하는 역할은 다음과 같습니다.
- 미리 구성된 기본값을 사용하여 WebApplicationBuilder 및 WebApplication을 만듭니다.
-
/를 반환하는 HTTP GET 엔드포인트헬로 월드!를 만듭니다.
앱 실행
Ctrl+F5를 눌러 디버거 없이 실행합니다.
Visual Studio는 다음 대화 상자를 표시합니다.
IIS Express SSL 인증서를 신뢰하는 경우 예를 선택합니다.
다음 대화 상자가 표시됩니다.
개발 인증서를 신뢰하는 데 동의하는 경우 예를 선택합니다.
Firefox 브라우저를 신뢰하는 방법에 대한 자세한 내용은 Firefox SEC_ERROR_INADEQUATE_KEY_USAGE 인증서 오류를 참조하세요.
Visual Studio에서 Kestrel 웹 서버를 시작하고 브라우저 창을 엽니다.
헬로 월드!가 브라우저에 표시됩니다.
Program.cs 파일에는 최소이지만 완전한 앱이 포함되어 있습니다.
NuGet 패키지 추가
이 자습서에서 사용되는 데이터베이스 및 진단을 지원하려면 NuGet 패키지를 추가해야 합니다.
- 도구 메뉴에서 NuGet 패키지 관리자 > 솔루션용 NuGet 패키지 관리를 선택합니다.
- 찾아보기 탭을 선택합니다.
- 검색 상자에 Microsoft.EntityFrameworkCore.InMemory를 입력한 다음
Microsoft.EntityFrameworkCore.InMemory를 선택합니다. - 오른쪽 창에서 프로젝트 확인란을 선택합니다.
-
버전 드롭다운에서 사용 가능한 최신 버전 7을
7.0.17선택한 다음 설치를 선택합니다. - 위의 지침에 따라 최신 버전 7이 포함된 패키지
Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore를 추가합니다.
모델 및 데이터베이스 컨텍스트 클래스
프로젝트 폴더에서 다음 코드가 포함된 Todo.cs라는 파일을 만듭니다.
public class Todo
{
public int Id { get; set; }
public string? Name { get; set; }
public bool IsComplete { get; set; }
}
앞의 코드는 이 앱에 대한 모델을 만듭니다. 모델은 앱에서 관리하는 데이터를 나타내는 일련의 클래스입니다.
다음 코드를 사용하여 TodoDb.cs라는 파일을 만듭니다.
using Microsoft.EntityFrameworkCore;
class TodoDb : DbContext
{
public TodoDb(DbContextOptions<TodoDb> options)
: base(options) { }
public DbSet<Todo> Todos => Set<Todo>();
}
앞의 코드는 데이터 모델에 대한 Entity Framework 기능을 조정하는 기본 클래스인 데이터베이스 컨텍스트를 정의합니다. 이 클래스는 Microsoft.EntityFrameworkCore.DbContext 클래스에서 파생됩니다.
API 코드 추가
Program.cs 파일의 내용을 다음 코드로 바꿉니다.
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
var app = builder.Build();
app.MapGet("/todoitems", async (TodoDb db) =>
await db.Todos.ToListAsync());
app.MapGet("/todoitems/complete", async (TodoDb db) =>
await db.Todos.Where(t => t.IsComplete).ToListAsync());
app.MapGet("/todoitems/{id}", async (int id, TodoDb db) =>
await db.Todos.FindAsync(id)
is Todo todo
? Results.Ok(todo)
: Results.NotFound());
app.MapPost("/todoitems", async (Todo todo, TodoDb db) =>
{
db.Todos.Add(todo);
await db.SaveChangesAsync();
return Results.Created($"/todoitems/{todo.Id}", todo);
});
app.MapPut("/todoitems/{id}", async (int id, Todo inputTodo, TodoDb db) =>
{
var todo = await db.Todos.FindAsync(id);
if (todo is null) return Results.NotFound();
todo.Name = inputTodo.Name;
todo.IsComplete = inputTodo.IsComplete;
await db.SaveChangesAsync();
return Results.NoContent();
});
app.MapDelete("/todoitems/{id}", async (int id, TodoDb db) =>
{
if (await db.Todos.FindAsync(id) is Todo todo)
{
db.Todos.Remove(todo);
await db.SaveChangesAsync();
return Results.NoContent();
}
return Results.NotFound();
});
app.Run();
강조 표시된 다음 코드는 DI(종속성 주입) 컨테이너에 데이터베이스 컨텍스트를 추가하고 데이터베이스 관련 예외를 표시할 수 있도록 합니다.
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
var app = builder.Build();
DI 컨테이너는 데이터베이스 컨텍스트 및 기타 서비스에 대한 액세스를 제공합니다.
Swagger를 사용하여 API 테스트 UI 만들기
선택할 수 있는 여러 웹 API 테스트 도구가 있으며, 원하는 도구를 사용하여 이 자습서의 소개 API 테스트 단계를 따를 수 있습니다.
이 자습서에서는 OpenAPI 사양을 준수하는 테스트 UI를 생성하기 위한 Swagger 도구를 통합하는 .NET 패키지 NSwag.AspNetCore를 활용합니다.
- NSwag: Swagger를 ASP.NET Core 애플리케이션에 직접 통합하여 미들웨어 및 구성을 제공하는 .NET 라이브러리입니다.
- Swagger: OpenAPI 사양을 따르는 API 테스트 페이지를 생성하는 OpenAPIGenerator 및 SwaggerUI와 같은 오픈 소스 도구 집합입니다.
- OpenAPI 사양: 컨트롤러 및 모델 내의 XML 및 특성 주석을 기반으로 API의 기능을 설명하는 문서입니다.
ASP.NET OpenAPI 및 NSwag를 사용하는 방법에 대한 자세한 내용은 Swagger/OpenAPI를 사용하는 ASP.NET Core Web API 설명서를 참조하세요.
Swagger 도구 설치
다음 명령을 실행합니다.
dotnet add package NSwag.AspNetCore
이전 명령은 Swagger 문서 및 UI를 생성하는 도구가 포함된 NSwag.AspNetCore 패키지를 추가합니다.
Swagger 미들웨어 구성
줄에 정의되기 전에
app강조 표시된 다음 코드를 추가합니다.var app = builder.Build();using Microsoft.EntityFrameworkCore; var builder = WebApplication.CreateBuilder(args); builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList")); builder.Services.AddDatabaseDeveloperPageExceptionFilter(); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddOpenApiDocument(config => { config.DocumentName = "TodoAPI"; config.Title = "TodoAPI v1"; config.Version = "v1"; }); var app = builder.Build();
위의 코드에서:
builder.Services.AddEndpointsApiExplorer();: HTTP API에 대한 메타데이터를 제공하는 서비스인 API 탐색기를 사용하도록 설정합니다. API 탐색기는 Swagger에서 Swagger 문서를 생성하는 데 사용됩니다.builder.Services.AddOpenApiDocument(config => {...});: Swagger OpenAPI 문서 생성기를 애플리케이션 서비스에 추가하고 해당 제목 및 버전과 같은 API에 대한 자세한 정보를 제공하도록 구성합니다. 보다 강력한 API 세부 정보를 제공하는 방법에 대한 자세한 내용은 NSwag 및 ASP.NET Core 시작을 참조하세요.app줄에var app = builder.Build();가 정의된 후, 다음 줄에 다음 강조된 코드를 추가합니다.var app = builder.Build(); if (app.Environment.IsDevelopment()) { app.UseOpenApi(); app.UseSwaggerUi(config => { config.DocumentTitle = "TodoAPI"; config.Path = "/swagger"; config.DocumentPath = "/swagger/{documentName}/swagger.json"; config.DocExpansion = "list"; }); }이전 코드를 사용하면 Swagger 미들웨어가 생성된 JSON 문서 및 Swagger UI를 제공할 수 있습니다. Swagger는 개발 환경에서만 사용할 수 있습니다. 프로덕션 환경에서 Swagger를 사용하도록 설정하면 API의 구조 및 구현에 대한 잠재적으로 중요한 세부 정보가 노출될 수 있습니다.
데이터 게시 테스트
Program.cs에서 다음 코드는 HTTP POST 엔드포인트 /todoitems를 만들어 메모리 내 데이터베이스에 데이터를 추가합니다.
app.MapPost("/todoitems", async (Todo todo, TodoDb db) =>
{
db.Todos.Add(todo);
await db.SaveChangesAsync();
return Results.Created($"/todoitems/{todo.Id}", todo);
});
앱을 실행합니다. 더 이상 / 엔드포인트가 없으므로 브라우저에 404 오류가 표시됩니다.
POST 엔드포인트는 앱에 데이터를 추가하는 데 사용됩니다.
앱이 계속 실행 중인 상태에서 브라우저에서 Swagger에서 생성된 API 테스트 페이지를 표시하도록
https://localhost:<port>/swagger이동합니다.
Swagger API 테스트 페이지에서 Post /todoitems>를 선택하고 실행해 보기를 클릭합니다.
요청 본문 필드에는 API에 대한 매개 변수를 반영하는 생성된 예제 형식이 포함되어 있습니다.
요청 본문에서 선택 사항을
id지정하지 않고 할 일 항목에 대한 JSON을 입력합니다.{ "name":"walk dog", "isComplete":true }실행을 선택합니다.
Swagger는 실행 단추 아래에 응답 창을 제공합니다.
몇 가지 유용한 세부 정보를 확인합니다.
- cURL: Swagger는 Unix/Linux 구문의 예제 cURL 명령을 제공합니다. 이 명령은 Windows용 Git의 Git Bash를 포함하여 Unix/Linux 구문을 사용하는 bash 셸을 사용하여 명령줄에서 실행할 수 있습니다.
- 요청 URL: API 호출에 대한 Swagger UI의 JavaScript 코드에서 수행한 HTTP 요청의 간소화된 표현입니다. 실제 요청에는 헤더, 쿼리 매개 변수 및 요청 본문과 같은 세부 정보가 포함될 수 있습니다.
- 서버 응답: 응답 본문 및 헤더를 포함합니다. 응답 본문은
id가1로 설정되었음을 보여 줍니다. - 응답 코드: 요청이 성공적으로 처리되고 새 리소스가 생성되었음을 나타내는 201
HTTP상태 코드가 반환되었습니다.
GET 엔드포인트 검사
샘플 앱은 MapGet을 호출하여 여러 GET 엔드포인트를 구현합니다.
| API | Description | 요청 메시지 본문 | 응답 본문 |
|---|---|---|---|
GET /todoitems |
할 일 항목 모두 가져오기 | None | 할 일 항목의 배열 |
GET /todoitems/complete |
완성된 모든 할 일 항목 가져오기 | None | 할 일 항목의 배열 |
GET /todoitems/{id} |
ID로 항목 가져오기 | None | 할 일 항목 |
app.MapGet("/todoitems", async (TodoDb db) =>
await db.Todos.ToListAsync());
app.MapGet("/todoitems/complete", async (TodoDb db) =>
await db.Todos.Where(t => t.IsComplete).ToListAsync());
app.MapGet("/todoitems/{id}", async (int id, TodoDb db) =>
await db.Todos.FindAsync(id)
is Todo todo
? Results.Ok(todo)
: Results.NotFound());
GET 엔드포인트 테스트
브라우저 또는 Swagger에서 엔드포인트를 호출하여 앱을 테스트합니다.
Swagger에서 GET /todoitems을 선택한 다음, > 버튼을 클릭하고, 실행을 클릭하세요.
다른 방법으로, 브라우저에서 URI를 입력하여
http://localhost:<port>/todoitems를 호출하세요. 예를 들어http://localhost:5001/todoitems
GET /todoitems를 호출하면 다음과 비슷한 응답이 생성됩니다.
[
{
"id": 1,
"name": "walk dog",
"isComplete": true
}
]
Swagger에서 GET /todoitems/{id}를 호출하여 특정 ID에서 데이터를 반환합니다.
- GET /todoitems>시도해 보기
-
ID 필드를
1설정하고 실행을 선택합니다.
다른 방법으로, 브라우저에서 URI를 입력하여
https://localhost:<port>/todoitems/1를 호출하세요. 예를 들어https://localhost:5001/todoitems/1응답은 다음과 비슷합니다.
{ "id": 1, "name": "walk dog", "isComplete": true }
이 앱은 메모리 내 데이터베이스를 사용합니다. 앱이 다시 시작되면 GET 요청이 데이터를 반환하지 않습니다. 데이터가 반환되지 않으면 POST 데이터를 앱에 게시하고 GET 요청을 다시 시도합니다.
반환 값
ASP.NET Core는 자동으로 JSON에 개체를 직렬화하고 JSON을 응답 메시지의 본문에 기록합니다. 이 반환 형식의 응답 코드는 200 OK이며 처리되지 않은 예외가 없다고 가정합니다. 처리되지 않은 예외는 5xx 오류로 변환됩니다.
반환 형식은 다양한 HTTP 상태 코드를 나타낼 수 있습니다. 예를 들어 GET /todoitems/{id}은 두 가지 상태 값을 반환할 수 있습니다.
- 요청된 ID와 일치하는 항목이 없는 경우 메서드에서 404 상태NotFound 오류 코드를 반환합니다.
- 그렇지 않으면 메서드가 JSON 응답 본문에서 200을 반환합니다.
item을 반환하면 HTTP 200 응답이 발생합니다.
PUT 엔드포인트 검사
샘플 앱은 MapPut을 사용하여 단일 PUT 엔드포인트를 구현합니다.
app.MapPut("/todoitems/{id}", async (int id, Todo inputTodo, TodoDb db) =>
{
var todo = await db.Todos.FindAsync(id);
if (todo is null) return Results.NotFound();
todo.Name = inputTodo.Name;
todo.IsComplete = inputTodo.IsComplete;
await db.SaveChangesAsync();
return Results.NoContent();
});
이 메서드는 HTTP PUT을 사용한다는 점을 제외하고 MapPost 메서드와 비슷합니다. 성공적인 응답은 204(콘텐츠 없음)를 반환합니다. HTTP 사양에 따라 PUT 요청의 경우 클라이언트는 변경 내용만이 아니라 전체 업데이트된 엔터티를 보내야 합니다. 부분 업데이트를 지원하려면 HTTP PATCH를 사용합니다.
PUT 엔드포인트 테스트
이 샘플은 앱이 시작될 때마다 초기화되어야 하는 메모리 내 데이터베이스를 사용합니다. PUT 호출을 실행하기 전에 데이터베이스에 항목이 있어야 합니다. GET을 호출하여 PUT 호출을 실행하기 전에 데이터베이스에 항목이 있는지 확인합니다.
할 일 항목을 Id = 1 업데이트한 다음, 이름을 "feed fish"로 설정합니다.
Swagger를 사용하여 PUT 요청을 보냅니다.
Put /todoitems/{id}>를 선택한 후 시도해 보세요.
ID 필드를
1로 설정합니다.요청 본문을 다음 JSON으로 설정합니다.
{ "name": "feed fish", "isComplete": false }실행을 선택합니다.
PATCH 엔드포인트를 검사하십시오.
다음 코드를 사용하여 TodoPatchDto.cs라는 파일을 만듭니다.
public class TodoPatchDto
{
public string? Name { get; set; }
public bool? IsComplete { get; set; }
}
클래스는 TodoPatchDto nullable 속성(string? 및)을 사용하여 요청에 제공되지 않은 필드와 bool?명시적으로 값으로 설정된 필드를 구분합니다.
샘플 앱은 MapPatch를 사용하여 단일 PATCH 엔드포인트를 구현합니다.
app.MapPatch("/todoitems/{id}", async (int id, TodoPatchDto inputTodo, TodoDb db) =>
{
var todo = await db.Todos.FindAsync(id);
if (todo is null) return Results.NotFound();
if (inputTodo.Name is not null) todo.Name = inputTodo.Name;
if (inputTodo.IsComplete is not null) todo.IsComplete = inputTodo.IsComplete.Value;
await db.SaveChangesAsync();
return Results.NoContent();
});
이 메서드는 HTTP PATCH를 MapPut 사용하고 요청에 제공된 필드만 업데이트한다는 점을 제외하고 메서드와 비슷합니다. 성공적인 응답은 204(콘텐츠 없음)를 반환합니다. HTTP 사양에 따라 PATCH 요청은 부분 업데이트를 사용하도록 설정하므로 클라이언트는 변경해야 하는 필드만 보낼 수 있습니다.
PATCH 엔드포인트는 nullable 속성이 있는 TodoPatchDto 클래스를 사용하여 부분 업데이트를 제대로 처리합니다. nullable 속성을 사용하면 엔드포인트가 제공되지 않은 필드(null)와 명시적으로 값으로 설정된 필드(부울 필드의 경우 false 포함)를 구분할 수 있습니다. nullable 속성이 없으면 null을 허용하지 않는 부울은 기본적으로 false로 설정되며, 해당 필드가 요청에 포함되지 않은 경우 기존 true 값을 덮어쓸 수 있습니다.
Note
PATCH 작업을 통해 리소스를 부분적으로 업데이트할 수 있습니다. JSON 패치 문서를 사용하는 고급 부분 업데이트는 ASP.NET Core 웹 API의 JsonPatch를 참조하세요.
PATCH 엔드포인트를 테스트하십시오
이 샘플은 앱이 시작될 때마다 초기화되어야 하는 메모리 내 데이터베이스를 사용합니다. PATCH를 호출하기 전에 데이터베이스에 항목이 있어야 합니다. PATCH를 호출하기 전에 GET을 호출하여 데이터베이스에 항목이 있는지 확인합니다.
name이 있는 to-do 항목의 Id = 1 속성만 업데이트하고 이름을 "run errands"로 설정합니다.
Swagger를 사용하여 PATCH 요청을 보냅니다.
패치 /todoitems/{id}>사용해 보세요.
ID 필드를
1로 설정합니다.요청 본문을 다음 JSON으로 설정합니다.
{ "name": "run errands" }실행을 선택합니다.
DELETE 엔드포인트 검사 및 테스트
샘플 앱은 MapDelete를 사용하여 단일 DELETE 엔드포인트를 구현합니다.
app.MapDelete("/todoitems/{id}", async (int id, TodoDb db) =>
{
if (await db.Todos.FindAsync(id) is Todo todo)
{
db.Todos.Remove(todo);
await db.SaveChangesAsync();
return Results.NoContent();
}
return Results.NotFound();
});
Swagger를 사용하여 DELETE 요청을 보냅니다.
DELETE /todoitems/{id}>선택하십시오. 사용해 보세요.
ID 필드를
1설정하고 실행을 선택합니다.DELETE 요청이 앱으로 전송되고 응답 창에 응답이 표시됩니다. 응답 본문이 비어 있고 서버 응답 상태 코드는 204입니다.
MapGroup API 사용
샘플 앱 코드는 엔드포인트를 설정할 때마다 todoitems URL 접두사를 반복합니다. API에는 공통 URL 접두사를 사용하는 엔드포인트 그룹이 있는 경우가 많으며 MapGroup 메서드를 사용하여 이러한 그룹을 구성할 수 있습니다. 반복 코드를 줄이고 RequireAuthorization 및 WithMetadata와 같은 메서드에 대한 단일 호출로 전체 엔드포인트 그룹을 사용자 지정할 수 있습니다.
Program.cs의 내용을 다음 코드로 바꿉니다.
using NSwag.AspNetCore;
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddOpenApiDocument(config =>
{
config.DocumentName = "TodoAPI";
config.Title = "TodoAPI v1";
config.Version = "v1";
});
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.UseOpenApi();
app.UseSwaggerUi(config =>
{
config.DocumentTitle = "TodoAPI";
config.Path = "/swagger";
config.DocumentPath = "/swagger/{documentName}/swagger.json";
config.DocExpansion = "list";
});
}
var todoItems = app.MapGroup("/todoitems");
todoItems.MapGet("/", async (TodoDb db) =>
await db.Todos.ToListAsync());
todoItems.MapGet("/complete", async (TodoDb db) =>
await db.Todos.Where(t => t.IsComplete).ToListAsync());
todoItems.MapGet("/{id}", async (int id, TodoDb db) =>
await db.Todos.FindAsync(id)
is Todo todo
? Results.Ok(todo)
: Results.NotFound());
todoItems.MapPost("/", async (Todo todo, TodoDb db) =>
{
db.Todos.Add(todo);
await db.SaveChangesAsync();
return Results.Created($"/todoitems/{todo.Id}", todo);
});
todoItems.MapPut("/{id}", async (int id, Todo inputTodo, TodoDb db) =>
{
var todo = await db.Todos.FindAsync(id);
if (todo is null) return Results.NotFound();
todo.Name = inputTodo.Name;
todo.IsComplete = inputTodo.IsComplete;
await db.SaveChangesAsync();
return Results.NoContent();
});
todoItems.MapDelete("/{id}", async (int id, TodoDb db) =>
{
if (await db.Todos.FindAsync(id) is Todo todo)
{
db.Todos.Remove(todo);
await db.SaveChangesAsync();
return Results.NoContent();
}
return Results.NotFound();
});
app.Run();
위의 코드에는 다음과 같은 변경 내용이 있습니다.
- URL 접두사
var todoItems = app.MapGroup("/todoitems");을 사용하여 그룹을 설정하도록/todoitems을 추가합니다. - 모든
app.Map<HttpVerb>메서드를todoItems.Map<HttpVerb>로 변경합니다. -
/todoitems메서드 호출에서 URL 접두사Map<HttpVerb>를 제거합니다.
엔드포인트를 테스트하여 동일하게 작동하는지 확인합니다.
TypedResults API 사용
TypedResults을 반환하는 것은 Results을 반환하는 것보다 테스트 용이성, 그리고 OpenAPI가 엔드포인트를 설명하기 위한 응답 형식 메타데이터를 자동으로 반환하는 등 여러 가지 이점이 있습니다. 자세한 내용은 TypedResults vs Results를 참조하세요.
Map<HttpVerb> 메서드는 람다를 사용하는 대신 경로 처리기 메서드를 호출할 수 있습니다. 예제를 보려면 다음 코드로 Program.cs를 업데이트합니다.
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
var app = builder.Build();
var todoItems = app.MapGroup("/todoitems");
todoItems.MapGet("/", GetAllTodos);
todoItems.MapGet("/complete", GetCompleteTodos);
todoItems.MapGet("/{id}", GetTodo);
todoItems.MapPost("/", CreateTodo);
todoItems.MapPut("/{id}", UpdateTodo);
todoItems.MapDelete("/{id}", DeleteTodo);
app.Run();
static async Task<IResult> GetAllTodos(TodoDb db)
{
return TypedResults.Ok(await db.Todos.ToArrayAsync());
}
static async Task<IResult> GetCompleteTodos(TodoDb db)
{
return TypedResults.Ok(await db.Todos.Where(t => t.IsComplete).ToListAsync());
}
static async Task<IResult> GetTodo(int id, TodoDb db)
{
return await db.Todos.FindAsync(id)
is Todo todo
? TypedResults.Ok(todo)
: TypedResults.NotFound();
}
static async Task<IResult> CreateTodo(Todo todo, TodoDb db)
{
db.Todos.Add(todo);
await db.SaveChangesAsync();
return TypedResults.Created($"/todoitems/{todo.Id}", todo);
}
static async Task<IResult> UpdateTodo(int id, Todo inputTodo, TodoDb db)
{
var todo = await db.Todos.FindAsync(id);
if (todo is null) return TypedResults.NotFound();
todo.Name = inputTodo.Name;
todo.IsComplete = inputTodo.IsComplete;
await db.SaveChangesAsync();
return TypedResults.NoContent();
}
static async Task<IResult> DeleteTodo(int id, TodoDb db)
{
if (await db.Todos.FindAsync(id) is Todo todo)
{
db.Todos.Remove(todo);
await db.SaveChangesAsync();
return TypedResults.NoContent();
}
return TypedResults.NotFound();
}
이제 Map<HttpVerb> 코드는 람다 대신 메서드를 호출합니다.
var todoItems = app.MapGroup("/todoitems");
todoItems.MapGet("/", GetAllTodos);
todoItems.MapGet("/complete", GetCompleteTodos);
todoItems.MapGet("/{id}", GetTodo);
todoItems.MapPost("/", CreateTodo);
todoItems.MapPut("/{id}", UpdateTodo);
todoItems.MapDelete("/{id}", DeleteTodo);
이러한 메서드는 IResult를 구현하고 TypedResults에서 정의한 개체를 반환합니다.
static async Task<IResult> GetAllTodos(TodoDb db)
{
return TypedResults.Ok(await db.Todos.ToArrayAsync());
}
static async Task<IResult> GetCompleteTodos(TodoDb db)
{
return TypedResults.Ok(await db.Todos.Where(t => t.IsComplete).ToListAsync());
}
static async Task<IResult> GetTodo(int id, TodoDb db)
{
return await db.Todos.FindAsync(id)
is Todo todo
? TypedResults.Ok(todo)
: TypedResults.NotFound();
}
static async Task<IResult> CreateTodo(Todo todo, TodoDb db)
{
db.Todos.Add(todo);
await db.SaveChangesAsync();
return TypedResults.Created($"/todoitems/{todo.Id}", todo);
}
static async Task<IResult> UpdateTodo(int id, Todo inputTodo, TodoDb db)
{
var todo = await db.Todos.FindAsync(id);
if (todo is null) return TypedResults.NotFound();
todo.Name = inputTodo.Name;
todo.IsComplete = inputTodo.IsComplete;
await db.SaveChangesAsync();
return TypedResults.NoContent();
}
static async Task<IResult> DeleteTodo(int id, TodoDb db)
{
if (await db.Todos.FindAsync(id) is Todo todo)
{
db.Todos.Remove(todo);
await db.SaveChangesAsync();
return TypedResults.NoContent();
}
return TypedResults.NotFound();
}
단위 테스트는 이러한 메서드를 호출하고 올바른 형식을 반환하는지 테스트할 수 있습니다. 예를 들어 메서드가 GetAllTodos인 경우:
static async Task<IResult> GetAllTodos(TodoDb db)
{
return TypedResults.Ok(await db.Todos.ToArrayAsync());
}
단위 테스트 코드는 Ok<Todo[]> 형식의 개체가 처리기 메서드에서 반환되는지 확인할 수 있습니다. 다음은 그 예입니다.
public async Task GetAllTodos_ReturnsOkOfTodosResult()
{
// Arrange
var db = CreateDbContext();
// Act
var result = await TodosApi.GetAllTodos(db);
// Assert: Check for the correct returned type
Assert.IsType<Ok<Todo[]>>(result);
}
초과 게시 방지
현재 샘플 앱은 전체 Todo 개체를 공개합니다. 프로덕션 앱 프로덕션 애플리케이션에서 모델의 하위 집합은 입력 및 반환될 수 있는 데이터를 제한하는 데 자주 사용됩니다. 이 동작에는 여러 가지 이유가 있으며, 보안이 주요 이유 중 하나입니다. 일반적으로 모델의 하위 집합을 DTO(데이터 전송 개체), 입력 모델 또는 뷰 모델이라고 합니다. 이 문서에서는 DTO를 사용합니다.
DTO를 사용하여 다음을 수행할 수 있습니다.
- 초과 게시를 방지합니다.
- 클라이언트에서 볼 수 없는 속성을 숨깁니다.
- 페이로드 크기를 줄이기 위해 일부 속성을 생략합니다.
- 중첩된 개체를 포함하는 개체 그래프를 평면화합니다. 클라이언트에는 평면화된 개체 그래프가 더 편리할 수 있습니다.
DTO 방법을 설명하려면 비밀 필드를 포함하도록 Todo 클래스를 업데이트합니다.
public class Todo
{
public int Id { get; set; }
public string? Name { get; set; }
public bool IsComplete { get; set; }
public string? Secret { get; set; }
}
이 앱에서는 숨겨진 필드를 숨겨야 하지만, 관리 앱은 숨겨진 필드를 공개할 수 있습니다.
비밀 필드를 게시하고 가져올 수 있는지를 확인하세요.
다음 코드를 사용하여 TodoItemDTO.cs라는 파일을 만듭니다.
public class TodoItemDTO
{
public int Id { get; set; }
public string? Name { get; set; }
public bool IsComplete { get; set; }
public TodoItemDTO() { }
public TodoItemDTO(Todo todoItem) =>
(Id, Name, IsComplete) = (todoItem.Id, todoItem.Name, todoItem.IsComplete);
}
이 DTO 모델을 사용하려면 파일의 Program.cs 내용을 다음 코드로 바꿉니다.
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
var app = builder.Build();
app.MapGet("/todoitems", async (TodoDb db) =>
await db.Todos.Select(x => new TodoItemDTO(x)).ToListAsync());
app.MapGet("/todoitems/{id}", async (int id, TodoDb db) =>
await db.Todos.FindAsync(id)
is Todo todo
? Results.Ok(new TodoItemDTO(todo))
: Results.NotFound());
app.MapPost("/todoitems", async (TodoItemDTO todoItemDTO, TodoDb db) =>
{
var todoItem = new Todo
{
IsComplete = todoItemDTO.IsComplete,
Name = todoItemDTO.Name
};
db.Todos.Add(todoItem);
await db.SaveChangesAsync();
return Results.Created($"/todoitems/{todoItem.Id}", new TodoItemDTO(todoItem));
});
app.MapPut("/todoitems/{id}", async (int id, TodoItemDTO todoItemDTO, TodoDb db) =>
{
var todo = await db.Todos.FindAsync(id);
if (todo is null) return Results.NotFound();
todo.Name = todoItemDTO.Name;
todo.IsComplete = todoItemDTO.IsComplete;
await db.SaveChangesAsync();
return Results.NoContent();
});
app.MapDelete("/todoitems/{id}", async (int id, TodoDb db) =>
{
if (await db.Todos.FindAsync(id) is Todo todo)
{
db.Todos.Remove(todo);
await db.SaveChangesAsync();
return Results.NoContent();
}
return Results.NotFound();
});
app.Run();
public class Todo
{
public int Id { get; set; }
public string? Name { get; set; }
public bool IsComplete { get; set; }
public string? Secret { get; set; }
}
public class TodoItemDTO
{
public int Id { get; set; }
public string? Name { get; set; }
public bool IsComplete { get; set; }
public TodoItemDTO() { }
public TodoItemDTO(Todo todoItem) =>
(Id, Name, IsComplete) = (todoItem.Id, todoItem.Name, todoItem.IsComplete);
}
class TodoDb : DbContext
{
public TodoDb(DbContextOptions<TodoDb> options)
: base(options) { }
public DbSet<Todo> Todos => Set<Todo>();
}
비밀 필드를 제외한 모든 필드를 게시하고 가져올 수 있는지 확인합니다.
완료된 샘플을 사용한 문제 해결
해결할 수 없는 문제가 발생한 경우 완료된 프로젝트와 코드를 비교합니다. 완료된 프로젝트를 보거나 다운로드합니다(다운로드 방법).
다음 단계
- JSON serialization 옵션을 구성합니다.
- 오류 및 예외 처리: 최소 API 앱에 대한 환경에서는 개발자 예외 페이지 가
Development기본적으로 사용하도록 설정됩니다. 오류 및 예외를 처리하는 방법에 대한 자세한 내용은 ASP.NET Core API에서 오류 처리를 참조하세요. - 최소 API 앱을 테스트하는 예제는 이 GitHub 샘플을 참조하세요.
- 최소 API에서 OpenAPI를 지원합니다.
- 빠른 시작: Azure에 게시합니다.
- ASP.NET Core 최소 API 구성
Learn more
최소 API 빠른 참조 참조하세요.
최소 API는 최소한의 종속성으로 HTTP API를 만들도록 설계되었습니다. ASP.NET Core에 최소 파일, 기능, 종속성만 포함하려는 앱과 마이크로 서비스에 적합합니다.
이 자습서에서는 ASP.NET Core를 사용하여 최소 API를 빌드하는 기본 사항을 설명합니다. ASP.NET Core에서 API를 만드는 또 다른 방법은 컨트롤러를 사용하는 것입니다. 최소 API와 컨트롤러 기반 API 중에서 선택하는 방법에 대한 도움말은 API 개요를 참조하세요. 더 많은 기능이 포함된 컨트롤러를 기반으로 API 프로젝트를 만드는 방법에 대한 자습서는 웹 API 만들기를 참조하세요.
Overview
이 자습서에서는 다음 API를 만듭니다.
| API | Description | 요청 메시지 본문 | 응답 본문 |
|---|---|---|---|
GET /todoitems |
할 일 항목 모두 가져오기 | None | 할 일 항목의 배열 |
GET /todoitems/complete |
완성된 할 일 항목 가져오기 | None | 할 일 항목의 배열 |
GET /todoitems/{id} |
ID로 항목 가져오기 | None | 할 일 항목 |
POST /todoitems |
새 항목 추가 | 할 일 항목 | 할 일 항목 |
PUT /todoitems/{id} |
기존 항목 업데이트 | 할 일 항목 | None |
DELETE /todoitems/{id} |
항목 삭제 | None | None |
Prerequisites
- ASP.NET 및 웹 개발 워크로드가 있는 Visual Studio 2022
- .NET 6 SDK
API 프로젝트 만들기
Visual Studio 2022를 시작하고 새 프로젝트 만들기를 선택합니다.
새 프로젝트 만들기 대화 상자에서 다음을 수행합니다.
-
Empty검색 상자에 를 입력합니다. - ASP.NET Core Empty 템플릿을 선택하고 다음을 선택합니다.
-
프로젝트 이름을 TodoApi로 지정하고 다음을 선택합니다.
추가 정보 대화 상자에서 다음을 수행합니다.
- .NET 6.0 선택
- 최상위 문 사용 안 함 옵션의 체크를 해제
- 만들기를 선택하세요
코드 검사
Program.cs 파일에는 다음 코드가 포함되어 있습니다.
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/", () => "Hello World!");
app.Run();
앞의 코드가 하는 역할은 다음과 같습니다.
- 미리 구성된 기본값을 사용하여 WebApplicationBuilder 및 WebApplication을 만듭니다.
-
/를 반환하는 HTTP GET 엔드포인트헬로 월드!를 만듭니다.
앱 실행
Ctrl+F5를 눌러 디버거 없이 실행합니다.
Visual Studio는 다음 대화 상자를 표시합니다.
IIS Express SSL 인증서를 신뢰하는 경우 예를 선택합니다.
다음 대화 상자가 표시됩니다.
개발 인증서를 신뢰하는 데 동의하는 경우 예를 선택합니다.
Firefox 브라우저를 신뢰하는 방법에 대한 자세한 내용은 Firefox SEC_ERROR_INADEQUATE_KEY_USAGE 인증서 오류를 참조하세요.
Visual Studio에서 Kestrel 웹 서버를 시작하고 브라우저 창을 엽니다.
헬로 월드!가 브라우저에 표시됩니다.
Program.cs 파일에는 최소이지만 완전한 앱이 포함되어 있습니다.
NuGet 패키지 추가
이 자습서에서 사용되는 데이터베이스 및 진단을 지원하려면 NuGet 패키지를 추가해야 합니다.
- 도구 메뉴에서 NuGet 패키지 관리자 > 솔루션용 NuGet 패키지 관리를 선택합니다.
- 찾아보기 탭을 선택합니다.
- 검색 상자에 Microsoft.EntityFrameworkCore.InMemory를 입력한 다음
Microsoft.EntityFrameworkCore.InMemory를 선택합니다. - 오른쪽 창에서 프로젝트 확인란을 선택합니다.
-
버전 드롭다운에서 사용 가능한 최신 버전 7을
6.0.28선택한 다음 설치를 선택합니다. - 위의 지침에 따라 최신 버전 7이 포함된 패키지
Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore를 추가합니다.
모델 및 데이터베이스 컨텍스트 클래스
프로젝트 폴더에서 다음 코드가 포함된 Todo.cs라는 파일을 만듭니다.
public class Todo
{
public int Id { get; set; }
public string? Name { get; set; }
public bool IsComplete { get; set; }
}
앞의 코드는 이 앱에 대한 모델을 만듭니다. 모델은 앱에서 관리하는 데이터를 나타내는 일련의 클래스입니다.
다음 코드를 사용하여 TodoDb.cs라는 파일을 만듭니다.
using Microsoft.EntityFrameworkCore;
class TodoDb : DbContext
{
public TodoDb(DbContextOptions<TodoDb> options)
: base(options) { }
public DbSet<Todo> Todos => Set<Todo>();
}
앞의 코드는 데이터 모델에 대한 Entity Framework 기능을 조정하는 기본 클래스인 데이터베이스 컨텍스트를 정의합니다. 이 클래스는 Microsoft.EntityFrameworkCore.DbContext 클래스에서 파생됩니다.
API 코드 추가
Program.cs 파일의 내용을 다음 코드로 바꿉니다.
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
var app = builder.Build();
app.MapGet("/", () => "Hello World!");
app.MapGet("/todoitems", async (TodoDb db) =>
await db.Todos.ToListAsync());
app.MapGet("/todoitems/complete", async (TodoDb db) =>
await db.Todos.Where(t => t.IsComplete).ToListAsync());
app.MapGet("/todoitems/{id}", async (int id, TodoDb db) =>
await db.Todos.FindAsync(id)
is Todo todo
? Results.Ok(todo)
: Results.NotFound());
app.MapPost("/todoitems", async (Todo todo, TodoDb db) =>
{
db.Todos.Add(todo);
await db.SaveChangesAsync();
return Results.Created($"/todoitems/{todo.Id}", todo);
});
app.MapPut("/todoitems/{id}", async (int id, Todo inputTodo, TodoDb db) =>
{
var todo = await db.Todos.FindAsync(id);
if (todo is null) return Results.NotFound();
todo.Name = inputTodo.Name;
todo.IsComplete = inputTodo.IsComplete;
await db.SaveChangesAsync();
return Results.NoContent();
});
app.MapDelete("/todoitems/{id}", async (int id, TodoDb db) =>
{
if (await db.Todos.FindAsync(id) is Todo todo)
{
db.Todos.Remove(todo);
await db.SaveChangesAsync();
return Results.NoContent();
}
return Results.NotFound();
});
app.Run();
강조 표시된 다음 코드는 DI(종속성 주입) 컨테이너에 데이터베이스 컨텍스트를 추가하고 데이터베이스 관련 예외를 표시할 수 있도록 합니다.
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
var app = builder.Build();
DI 컨테이너는 데이터베이스 컨텍스트 및 기타 서비스에 대한 액세스를 제공합니다.
Swagger를 사용하여 API 테스트 UI 만들기
선택할 수 있는 여러 웹 API 테스트 도구가 있으며, 원하는 도구를 사용하여 이 자습서의 소개 API 테스트 단계를 따를 수 있습니다.
이 자습서에서는 OpenAPI 사양을 준수하는 테스트 UI를 생성하기 위한 Swagger 도구를 통합하는 .NET 패키지 NSwag.AspNetCore를 활용합니다.
- NSwag: Swagger를 ASP.NET Core 애플리케이션에 직접 통합하여 미들웨어 및 구성을 제공하는 .NET 라이브러리입니다.
- Swagger: OpenAPI 사양을 따르는 API 테스트 페이지를 생성하는 OpenAPIGenerator 및 SwaggerUI와 같은 오픈 소스 도구 집합입니다.
- OpenAPI 사양: 컨트롤러 및 모델 내의 XML 및 특성 주석을 기반으로 API의 기능을 설명하는 문서입니다.
ASP.NET OpenAPI 및 NSwag를 사용하는 방법에 대한 자세한 내용은 Swagger/OpenAPI를 사용하는 ASP.NET Core Web API 설명서를 참조하세요.
Swagger 도구 설치
다음 명령을 실행합니다.
dotnet add package NSwag.AspNetCore
이전 명령은 Swagger 문서 및 UI를 생성하는 도구가 포함된 NSwag.AspNetCore 패키지를 추가합니다.
Swagger 미들웨어 구성
Program.cs 맨 위에 다음
using문을 추가합니다.using NSwag.AspNetCore;줄에 정의되기 전에
app강조 표시된 다음 코드를 추가합니다.var app = builder.Build();using NSwag.AspNetCore; using Microsoft.EntityFrameworkCore; var builder = WebApplication.CreateBuilder(args); builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList")); builder.Services.AddDatabaseDeveloperPageExceptionFilter(); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddOpenApiDocument(config => { config.DocumentName = "TodoAPI"; config.Title = "TodoAPI v1"; config.Version = "v1"; }); var app = builder.Build();
위의 코드에서:
builder.Services.AddEndpointsApiExplorer();: HTTP API에 대한 메타데이터를 제공하는 서비스인 API 탐색기를 사용하도록 설정합니다. API 탐색기는 Swagger에서 Swagger 문서를 생성하는 데 사용됩니다.builder.Services.AddOpenApiDocument(config => {...});: Swagger OpenAPI 문서 생성기를 애플리케이션 서비스에 추가하고 해당 제목 및 버전과 같은 API에 대한 자세한 정보를 제공하도록 구성합니다. 보다 강력한 API 세부 정보를 제공하는 방법에 대한 자세한 내용은 NSwag 및 ASP.NET Core 시작을 참조하세요.app줄에var app = builder.Build();가 정의된 후, 다음 줄에 다음 강조된 코드를 추가합니다.var app = builder.Build(); if (app.Environment.IsDevelopment()) { app.UseOpenApi(); app.UseSwaggerUi(config => { config.DocumentTitle = "TodoAPI"; config.Path = "/swagger"; config.DocumentPath = "/swagger/{documentName}/swagger.json"; config.DocExpansion = "list"; }); }이전 코드를 사용하면 Swagger 미들웨어가 생성된 JSON 문서 및 Swagger UI를 제공할 수 있습니다. Swagger는 개발 환경에서만 사용할 수 있습니다. 프로덕션 환경에서 Swagger를 사용하도록 설정하면 API의 구조 및 구현에 대한 잠재적으로 중요한 세부 정보가 노출될 수 있습니다.
데이터 게시 테스트
Program.cs에서 다음 코드는 HTTP POST 엔드포인트 /todoitems를 만들어 메모리 내 데이터베이스에 데이터를 추가합니다.
app.MapPost("/todoitems", async (Todo todo, TodoDb db) =>
{
db.Todos.Add(todo);
await db.SaveChangesAsync();
return Results.Created($"/todoitems/{todo.Id}", todo);
});
앱을 실행합니다. 더 이상 / 엔드포인트가 없으므로 브라우저에 404 오류가 표시됩니다.
POST 엔드포인트는 앱에 데이터를 추가하는 데 사용됩니다.
앱이 계속 실행 중인 상태에서 브라우저에서 Swagger에서 생성된 API 테스트 페이지를 표시하도록
https://localhost:<port>/swagger이동합니다.
Swagger API 테스트 페이지에서 Post /todoitems>를 선택하고 실행해 보기를 클릭합니다.
요청 본문 필드에는 API에 대한 매개 변수를 반영하는 생성된 예제 형식이 포함되어 있습니다.
요청 본문에서 선택 사항을
id지정하지 않고 할 일 항목에 대한 JSON을 입력합니다.{ "name":"walk dog", "isComplete":true }실행을 선택합니다.
Swagger는 실행 단추 아래에 응답 창을 제공합니다.
몇 가지 유용한 세부 정보를 확인합니다.
- cURL: Swagger는 Unix/Linux 구문의 예제 cURL 명령을 제공합니다. 이 명령은 Windows용 Git의 Git Bash를 포함하여 Unix/Linux 구문을 사용하는 bash 셸을 사용하여 명령줄에서 실행할 수 있습니다.
- 요청 URL: API 호출에 대한 Swagger UI의 JavaScript 코드에서 수행한 HTTP 요청의 간소화된 표현입니다. 실제 요청에는 헤더, 쿼리 매개 변수 및 요청 본문과 같은 세부 정보가 포함될 수 있습니다.
- 서버 응답: 응답 본문 및 헤더를 포함합니다. 응답 본문은
id가1로 설정되었음을 보여 줍니다. - 응답 코드: 요청이 성공적으로 처리되고 새 리소스가 생성되었음을 나타내는 201
HTTP상태 코드가 반환되었습니다.
GET 엔드포인트 검사
샘플 앱은 MapGet을 호출하여 여러 GET 엔드포인트를 구현합니다.
| API | Description | 요청 메시지 본문 | 응답 본문 |
|---|---|---|---|
GET /todoitems |
할 일 항목 모두 가져오기 | None | 할 일 항목의 배열 |
GET /todoitems/complete |
완성된 모든 할 일 항목 가져오기 | None | 할 일 항목의 배열 |
GET /todoitems/{id} |
ID로 항목 가져오기 | None | 할 일 항목 |
app.MapGet("/", () => "Hello World!");
app.MapGet("/todoitems", async (TodoDb db) =>
await db.Todos.ToListAsync());
app.MapGet("/todoitems/complete", async (TodoDb db) =>
await db.Todos.Where(t => t.IsComplete).ToListAsync());
app.MapGet("/todoitems/{id}", async (int id, TodoDb db) =>
await db.Todos.FindAsync(id)
is Todo todo
? Results.Ok(todo)
: Results.NotFound());
GET 엔드포인트 테스트
브라우저 또는 Swagger에서 엔드포인트를 호출하여 앱을 테스트합니다.
Swagger에서 GET /todoitems을 선택한 다음, > 버튼을 클릭하고, 실행을 클릭하세요.
다른 방법으로, 브라우저에서 URI를 입력하여
http://localhost:<port>/todoitems를 호출하세요. 예를 들어http://localhost:5001/todoitems
GET /todoitems를 호출하면 다음과 비슷한 응답이 생성됩니다.
[
{
"id": 1,
"name": "walk dog",
"isComplete": true
}
]
Swagger에서 GET /todoitems/{id}를 호출하여 특정 ID에서 데이터를 반환합니다.
- GET /todoitems>시도해 보기
-
ID 필드를
1설정하고 실행을 선택합니다.
다른 방법으로, 브라우저에서 URI를 입력하여
https://localhost:<port>/todoitems/1를 호출하세요. 예를 들면 다음과 같습니다.https://localhost:5001/todoitems/1응답은 다음과 비슷합니다.
{ "id": 1, "name": "walk dog", "isComplete": true }
이 앱은 메모리 내 데이터베이스를 사용합니다. 앱이 다시 시작되면 GET 요청이 데이터를 반환하지 않습니다. 데이터가 반환되지 않으면 POST 데이터를 앱에 게시하고 GET 요청을 다시 시도합니다.
반환 값
ASP.NET Core는 자동으로 JSON에 개체를 직렬화하고 JSON을 응답 메시지의 본문에 기록합니다. 이 반환 형식의 응답 코드는 200 OK이며 처리되지 않은 예외가 없다고 가정합니다. 처리되지 않은 예외는 5xx 오류로 변환됩니다.
반환 형식은 다양한 HTTP 상태 코드를 나타낼 수 있습니다. 예를 들어 GET /todoitems/{id}은 두 가지 상태 값을 반환할 수 있습니다.
- 요청된 ID와 일치하는 항목이 없는 경우 메서드에서 404 상태NotFound 오류 코드를 반환합니다.
- 그렇지 않으면 메서드가 JSON 응답 본문에서 200을 반환합니다.
item을 반환하면 HTTP 200 응답이 발생합니다.
PUT 엔드포인트 검사
샘플 앱은 MapPut을 사용하여 단일 PUT 엔드포인트를 구현합니다.
app.MapPut("/todoitems/{id}", async (int id, Todo inputTodo, TodoDb db) =>
{
var todo = await db.Todos.FindAsync(id);
if (todo is null) return Results.NotFound();
todo.Name = inputTodo.Name;
todo.IsComplete = inputTodo.IsComplete;
await db.SaveChangesAsync();
return Results.NoContent();
});
이 메서드는 HTTP PUT을 사용한다는 점을 제외하고 MapPost 메서드와 비슷합니다. 성공적인 응답은 204(콘텐츠 없음)를 반환합니다. HTTP 사양에 따라 PUT 요청의 경우 클라이언트는 변경 내용만이 아니라 전체 업데이트된 엔터티를 보내야 합니다. 부분 업데이트를 지원하려면 HTTP PATCH를 사용합니다.
PUT 엔드포인트 테스트
이 샘플은 앱이 시작될 때마다 초기화되어야 하는 메모리 내 데이터베이스를 사용합니다. PUT 호출을 실행하기 전에 데이터베이스에 항목이 있어야 합니다. GET을 호출하여 PUT 호출을 실행하기 전에 데이터베이스에 항목이 있는지 확인합니다.
할 일 항목을 Id = 1 업데이트한 다음, 이름을 "feed fish"로 설정합니다.
Swagger를 사용하여 PUT 요청을 보냅니다.
Put /todoitems/{id}>를 선택한 후 시도해 보세요.
ID 필드를
1로 설정합니다.요청 본문을 다음 JSON으로 설정합니다.
{ "name": "feed fish", "isComplete": false }실행을 선택합니다.
DELETE 엔드포인트 검사 및 테스트
샘플 앱은 MapDelete를 사용하여 단일 DELETE 엔드포인트를 구현합니다.
app.MapDelete("/todoitems/{id}", async (int id, TodoDb db) =>
{
if (await db.Todos.FindAsync(id) is Todo todo)
{
db.Todos.Remove(todo);
await db.SaveChangesAsync();
return Results.NoContent();
}
return Results.NotFound();
});
Swagger를 사용하여 DELETE 요청을 보냅니다.
DELETE /todoitems/{id}>선택하십시오. 사용해 보세요.
ID 필드를
1설정하고 실행을 선택합니다.DELETE 요청이 앱으로 전송되고 응답 창에 응답이 표시됩니다. 응답 본문이 비어 있고 서버 응답 상태 코드는 204입니다.
초과 게시 방지
현재 샘플 앱은 전체 Todo 개체를 공개합니다. 프로덕션 앱 프로덕션 애플리케이션에서 모델의 하위 집합은 입력 및 반환될 수 있는 데이터를 제한하는 데 자주 사용됩니다. 이 동작에는 여러 가지 이유가 있으며, 보안이 주요 이유 중 하나입니다. 일반적으로 모델의 하위 집합을 DTO(데이터 전송 개체), 입력 모델 또는 뷰 모델이라고 합니다. 이 문서에서는 DTO를 사용합니다.
DTO를 사용하여 다음을 수행할 수 있습니다.
- 초과 게시를 방지합니다.
- 클라이언트에서 볼 수 없는 속성을 숨깁니다.
- 페이로드 크기를 줄이기 위해 일부 속성을 생략합니다.
- 중첩된 개체를 포함하는 개체 그래프를 평면화합니다. 클라이언트에는 평면화된 개체 그래프가 더 편리할 수 있습니다.
DTO 방법을 설명하려면 비밀 필드를 포함하도록 Todo 클래스를 업데이트합니다.
public class Todo
{
public int Id { get; set; }
public string? Name { get; set; }
public bool IsComplete { get; set; }
public string? Secret { get; set; }
}
이 앱에서는 숨겨진 필드를 숨겨야 하지만, 관리 앱은 숨겨진 필드를 공개할 수 있습니다.
비밀 필드를 게시하고 가져올 수 있는지를 확인하세요.
다음 코드를 사용하여 TodoItemDTO.cs라는 파일을 만듭니다.
public class TodoItemDTO
{
public int Id { get; set; }
public string? Name { get; set; }
public bool IsComplete { get; set; }
public TodoItemDTO() { }
public TodoItemDTO(Todo todoItem) =>
(Id, Name, IsComplete) = (todoItem.Id, todoItem.Name, todoItem.IsComplete);
}
이 DTO 모델을 사용하려면 파일의 Program.cs 내용을 다음 코드로 바꿉니다.
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
var app = builder.Build();
app.MapGet("/todoitems", async (TodoDb db) =>
await db.Todos.Select(x => new TodoItemDTO(x)).ToListAsync());
app.MapGet("/todoitems/{id}", async (int id, TodoDb db) =>
await db.Todos.FindAsync(id)
is Todo todo
? Results.Ok(new TodoItemDTO(todo))
: Results.NotFound());
app.MapPost("/todoitems", async (TodoItemDTO todoItemDTO, TodoDb db) =>
{
var todoItem = new Todo
{
IsComplete = todoItemDTO.IsComplete,
Name = todoItemDTO.Name
};
db.Todos.Add(todoItem);
await db.SaveChangesAsync();
return Results.Created($"/todoitems/{todoItem.Id}", new TodoItemDTO(todoItem));
});
app.MapPut("/todoitems/{id}", async (int id, TodoItemDTO todoItemDTO, TodoDb db) =>
{
var todo = await db.Todos.FindAsync(id);
if (todo is null) return Results.NotFound();
todo.Name = todoItemDTO.Name;
todo.IsComplete = todoItemDTO.IsComplete;
await db.SaveChangesAsync();
return Results.NoContent();
});
app.MapDelete("/todoitems/{id}", async (int id, TodoDb db) =>
{
if (await db.Todos.FindAsync(id) is Todo todo)
{
db.Todos.Remove(todo);
await db.SaveChangesAsync();
return Results.NoContent();
}
return Results.NotFound();
});
app.Run();
public class Todo
{
public int Id { get; set; }
public string? Name { get; set; }
public bool IsComplete { get; set; }
public string? Secret { get; set; }
}
public class TodoItemDTO
{
public int Id { get; set; }
public string? Name { get; set; }
public bool IsComplete { get; set; }
public TodoItemDTO() { }
public TodoItemDTO(Todo todoItem) =>
(Id, Name, IsComplete) = (todoItem.Id, todoItem.Name, todoItem.IsComplete);
}
class TodoDb : DbContext
{
public TodoDb(DbContextOptions<TodoDb> options)
: base(options) { }
public DbSet<Todo> Todos => Set<Todo>();
}
비밀 필드를 제외한 모든 필드를 게시하고 가져올 수 있는지 확인합니다.
최소 API 테스트
최소 API 앱을 테스트하는 예제는 이 GitHub 샘플을 참조하세요.
Azure에 게시
Azure에 배포에 대한 자세한 내용은 빠른 시작: ASP.NET 웹앱 배포를 참조하세요.
추가 리소스
최소 API는 최소한의 종속성으로 HTTP API를 만들도록 설계되었습니다. ASP.NET Core에 최소 파일, 기능 및 종속성만 포함하려는 마이크로 서비스 및 앱에 이상적입니다.
이 자습서에서는 ASP.NET Core를 사용하여 최소 API를 빌드하는 기본 사항을 설명합니다. ASP.NET Core에서 API를 만드는 또 다른 방법은 컨트롤러를 사용하는 것입니다. 최소 API와 컨트롤러 기반 API 중에서 선택하는 방법에 대한 도움말은 API 개요를 참조하세요. 더 많은 기능이 포함된 컨트롤러를 기반으로 API 프로젝트를 만드는 방법에 대한 자습서는 웹 API 만들기를 참조하세요.
Overview
이 자습서에서는 다음 API를 만듭니다.
| API | Description | 요청 메시지 본문 | 응답 본문 |
|---|---|---|---|
GET /todoitems |
할 일 항목 모두 가져오기 | None | 할 일 항목의 배열 |
GET /todoitems/complete |
완성된 할 일 항목 가져오기 | None | 할 일 항목의 배열 |
GET /todoitems/{id} |
ID로 항목 가져오기 | None | 할 일 항목 |
POST /todoitems |
새 항목 추가 | 할 일 항목 | 할 일 항목 |
PUT /todoitems/{id} |
기존 항목 업데이트 | 할 일 항목 | None |
PATCH /todoitems/{id} |
항목 부분 업데이트 | 부분 할 일 항목 | None |
DELETE /todoitems/{id} |
항목 삭제 | None | None |
Prerequisites
ASP.NET 및 웹 개발 워크로드가 있는 Visual Studio 2022
API 프로젝트 만들기
Visual Studio 2022를 시작하고 새 프로젝트 만들기를 선택합니다.
새 프로젝트 만들기 대화 상자에서 다음을 수행합니다.
-
Empty검색 상자에 를 입력합니다. - ASP.NET Core Empty 템플릿을 선택하고 다음을 선택합니다.
-
프로젝트 이름을 TodoApi로 지정하고 다음을 선택합니다.
추가 정보 대화 상자에서 다음을 수행합니다.
- .NET 8.0(장기 지원) 선택
- 최상위 문 사용 안 함 옵션의 체크를 해제
- 만들기를 선택하세요
코드 검사
Program.cs 파일에는 다음 코드가 포함되어 있습니다.
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/", () => "Hello World!");
app.Run();
앞의 코드가 하는 역할은 다음과 같습니다.
- 미리 구성된 기본값을 사용하여 WebApplicationBuilder 및 WebApplication을 만듭니다.
-
/를 반환하는 HTTP GET 엔드포인트헬로 월드!를 만듭니다.
앱 실행
Ctrl+F5를 눌러 디버거 없이 실행합니다.
Visual Studio는 다음 대화 상자를 표시합니다.
IIS Express SSL 인증서를 신뢰하는 경우 예를 선택합니다.
다음 대화 상자가 표시됩니다.
개발 인증서를 신뢰하는 데 동의하는 경우 예를 선택합니다.
Firefox 브라우저를 신뢰하는 방법에 대한 자세한 내용은 Firefox SEC_ERROR_INADEQUATE_KEY_USAGE 인증서 오류를 참조하세요.
Visual Studio에서 Kestrel 웹 서버를 시작하고 브라우저 창을 엽니다.
헬로 월드!가 브라우저에 표시됩니다.
Program.cs 파일에는 최소이지만 완전한 앱이 포함되어 있습니다.
브라우저 창을 닫습니다.
NuGet 패키지 추가
이 자습서에서 사용되는 데이터베이스 및 진단을 지원하려면 NuGet 패키지를 추가해야 합니다.
- 도구 메뉴에서 NuGet 패키지 관리자 > 솔루션용 NuGet 패키지 관리를 선택합니다.
- 찾아보기 탭을 선택합니다.
- 검색 상자에 Microsoft.EntityFrameworkCore.InMemory를 입력한 다음
Microsoft.EntityFrameworkCore.InMemory를 선택합니다. - 오른쪽 창에서 프로젝트 확인란을 선택하고 설치를 선택합니다.
- 이전 지침에 따라
Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore패키지를 추가합니다.
모델 및 데이터베이스 컨텍스트 클래스
- 프로젝트 폴더에서 다음 코드가 포함된
Todo.cs라는 파일을 만듭니다.
public class Todo
{
public int Id { get; set; }
public string? Name { get; set; }
public bool IsComplete { get; set; }
}
앞의 코드는 이 앱에 대한 모델을 만듭니다. 모델은 앱에서 관리하는 데이터를 나타내는 일련의 클래스입니다.
- 다음 코드를 사용하여
TodoDb.cs라는 파일을 만듭니다.
using Microsoft.EntityFrameworkCore;
class TodoDb : DbContext
{
public TodoDb(DbContextOptions<TodoDb> options)
: base(options) { }
public DbSet<Todo> Todos => Set<Todo>();
}
앞의 코드는 데이터 모델에 대한 Entity Framework 기능을 조정하는 기본 클래스인 데이터베이스 컨텍스트를 정의합니다. 이 클래스는 Microsoft.EntityFrameworkCore.DbContext 클래스에서 파생됩니다.
API 코드 추가
-
Program.cs파일의 내용을 다음 코드로 바꿉니다.
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
var app = builder.Build();
app.MapGet("/todoitems", async (TodoDb db) =>
await db.Todos.ToListAsync());
app.MapGet("/todoitems/complete", async (TodoDb db) =>
await db.Todos.Where(t => t.IsComplete).ToListAsync());
app.MapGet("/todoitems/{id}", async (int id, TodoDb db) =>
await db.Todos.FindAsync(id)
is Todo todo
? Results.Ok(todo)
: Results.NotFound());
app.MapPost("/todoitems", async (Todo todo, TodoDb db) =>
{
db.Todos.Add(todo);
await db.SaveChangesAsync();
return Results.Created($"/todoitems/{todo.Id}", todo);
});
app.MapPut("/todoitems/{id}", async (int id, Todo inputTodo, TodoDb db) =>
{
var todo = await db.Todos.FindAsync(id);
if (todo is null) return Results.NotFound();
todo.Name = inputTodo.Name;
todo.IsComplete = inputTodo.IsComplete;
await db.SaveChangesAsync();
return Results.NoContent();
});
app.MapDelete("/todoitems/{id}", async (int id, TodoDb db) =>
{
if (await db.Todos.FindAsync(id) is Todo todo)
{
db.Todos.Remove(todo);
await db.SaveChangesAsync();
return Results.NoContent();
}
return Results.NotFound();
});
app.Run();
강조 표시된 다음 코드는 DI(종속성 주입) 컨테이너에 데이터베이스 컨텍스트를 추가하고 데이터베이스 관련 예외를 표시할 수 있도록 합니다.
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
var app = builder.Build();
DI 컨테이너는 데이터베이스 컨텍스트 및 기타 서비스에 대한 액세스를 제공합니다.
이 자습서에서는 엔드포인트 탐색기 및 .http 파일을 사용하여 API를 테스트합니다.
데이터 게시 테스트
Program.cs에서 다음 코드는 HTTP POST 엔드포인트 /todoitems를 만들어 메모리 내 데이터베이스에 데이터를 추가합니다.
app.MapPost("/todoitems", async (Todo todo, TodoDb db) =>
{
db.Todos.Add(todo);
await db.SaveChangesAsync();
return Results.Created($"/todoitems/{todo.Id}", todo);
});
앱을 실행합니다. 더 이상 / 엔드포인트가 없으므로 브라우저에 404 오류가 표시됩니다.
POST 엔드포인트는 앱에 데이터를 추가하는 데 사용됩니다.
선택 보기>다른 Windows>Endpoints 탐색기.
POST 엔드포인트를 마우스 오른쪽 단추로 클릭하고 요청 생성을 선택합니다.
다음 예제와 유사한 내용이 포함된 새 파일이 프로젝트 폴더
TodoApi.http에 만들어집니다.@TodoApi_HostAddress = https://localhost:7031 Post {{TodoApi_HostAddress}}/todoitems ###- 첫 번째 줄은 모든 엔드포인트에 사용되는 변수를 만듭니다.
- 다음 줄은 POST 요청을 정의합니다.
- 삼중 해시 태그(
###) 줄은 요청 구분 기호입니다. 이는 다른 요청에 대한 것입니다.
POST 요청에는 헤더와 본문이 필요합니다. 요청의 해당 부분을 정의하려면 POST 요청 줄 바로 다음에 다음 줄을 추가합니다.
Content-Type: application/json { "name":"walk dog", "isComplete":true }앞의 코드는 Content-Type 헤더와 JSON 요청 본문을 추가합니다. 이제 TodoApi.http 파일은 다음 예제와 비슷하게 보이지만, 사용자 본인의 포트 번호를 사용해야 합니다.
@TodoApi_HostAddress = https://localhost:7057 Post {{TodoApi_HostAddress}}/todoitems Content-Type: application/json { "name":"walk dog", "isComplete":true } ###앱을 실행합니다.
요청 줄 위에
POST있는 요청 보내기 링크를 선택합니다.
POST 요청이 앱으로 전송되고 응답 창에 응답이 표시됩니다.
GET 엔드포인트 검사
샘플 앱은 MapGet을 호출하여 여러 GET 엔드포인트를 구현합니다.
| API | Description | 요청 메시지 본문 | 응답 본문 |
|---|---|---|---|
GET /todoitems |
할 일 항목 모두 가져오기 | None | 할 일 항목의 배열 |
GET /todoitems/complete |
완성된 모든 할 일 항목 가져오기 | None | 할 일 항목의 배열 |
GET /todoitems/{id} |
ID로 항목 가져오기 | None | 할 일 항목 |
app.MapGet("/todoitems", async (TodoDb db) =>
await db.Todos.ToListAsync());
app.MapGet("/todoitems/complete", async (TodoDb db) =>
await db.Todos.Where(t => t.IsComplete).ToListAsync());
app.MapGet("/todoitems/{id}", async (int id, TodoDb db) =>
await db.Todos.FindAsync(id)
is Todo todo
? Results.Ok(todo)
: Results.NotFound());
GET 엔드포인트 테스트
브라우저에서 엔드포인트를 GET 호출하거나 엔드포인트 탐색기를 사용하여 앱을 테스트합니다. 다음 단계는 엔드포인트 탐색기를 위한 것입니다.
엔드포인트 탐색기에서 첫 번째 GET 엔드포인트를 마우스 오른쪽 단추로 클릭하고 요청 생성을 선택합니다.
다음 콘텐츠가
TodoApi.http파일에 추가됩니다.Get {{TodoApi_HostAddress}}/todoitems ###새로운 요청 줄 위에 있는 요청 보내기 링크를 선택합니다.
GET 요청이 앱으로 전송되고 응답 창에 응답이 표시됩니다.
응답 본문은 다음 JSON과 유사합니다.
[ { "id": 1, "name": "walk dog", "isComplete": true } ]엔드포인트 탐색기에서 GET 엔드포인트를 마우스 오른쪽 버튼으로 클릭하고
/todoitems/{id}을 선택하세요. 다음 콘텐츠가TodoApi.http파일에 추가됩니다.GET {{TodoApi_HostAddress}}/todoitems/{id} ###{id}를1로 교체합니다.새 GET 요청 줄 위에 있는 요청 보내기 링크를 선택합니다.
GET 요청이 앱으로 전송되고 응답 창에 응답이 표시됩니다.
응답 본문은 다음 JSON과 유사합니다.
{ "id": 1, "name": "walk dog", "isComplete": true }
이 앱은 메모리 내 데이터베이스를 사용합니다. 앱이 다시 시작되면 GET 요청이 데이터를 반환하지 않습니다. 데이터가 반환되지 않으면 POST 데이터를 앱에 게시하고 GET 요청을 다시 시도합니다.
반환 값
ASP.NET Core는 자동으로 JSON에 개체를 직렬화하고 JSON을 응답 메시지의 본문에 기록합니다. 이 반환 형식의 응답 코드는 200 OK이며 처리되지 않은 예외가 없다고 가정합니다. 처리되지 않은 예외는 5xx 오류로 변환됩니다.
반환 형식은 다양한 HTTP 상태 코드를 나타낼 수 있습니다. 예를 들어 GET /todoitems/{id}은 두 가지 상태 값을 반환할 수 있습니다.
- 요청된 ID와 일치하는 항목이 없는 경우 메서드에서 404 상태NotFound 오류 코드를 반환합니다.
- 그렇지 않으면 메서드가 JSON 응답 본문에서 200을 반환합니다.
item을 반환하면 HTTP 200 응답이 발생합니다.
PUT 엔드포인트 검사
샘플 앱은 MapPut을 사용하여 단일 PUT 엔드포인트를 구현합니다.
app.MapPut("/todoitems/{id}", async (int id, Todo inputTodo, TodoDb db) =>
{
var todo = await db.Todos.FindAsync(id);
if (todo is null) return Results.NotFound();
todo.Name = inputTodo.Name;
todo.IsComplete = inputTodo.IsComplete;
await db.SaveChangesAsync();
return Results.NoContent();
});
이 메서드는 HTTP PUT을 사용한다는 점을 제외하고 MapPost 메서드와 비슷합니다. 성공적인 응답은 204(콘텐츠 없음)를 반환합니다. HTTP 사양에 따라 PUT 요청의 경우 클라이언트는 변경 내용만이 아니라 전체 업데이트된 엔터티를 보내야 합니다. 부분 업데이트를 지원하려면 HTTP PATCH를 사용합니다.
PUT 엔드포인트 테스트
이 샘플은 앱이 시작될 때마다 초기화되어야 하는 메모리 내 데이터베이스를 사용합니다. PUT 호출을 실행하기 전에 데이터베이스에 항목이 있어야 합니다. GET을 호출하여 PUT 호출을 실행하기 전에 데이터베이스에 항목이 있는지 확인합니다.
할 일 항목을 Id = 1 업데이트한 다음, 이름을 "feed fish"로 설정합니다.
엔드포인트 탐색기에서 PUT 엔드포인트를 마우스 오른쪽 단추로 클릭하고 요청 생성을 선택합니다.
다음 콘텐츠가
TodoApi.http파일에 추가됩니다.Put {{TodoApi_HostAddress}}/todoitems/{id} ###PUT 요청 줄에서 `
{id}`를 `1`로 교체하세요.PUT 요청 줄 바로 다음에 다음 줄을 추가합니다.
Content-Type: application/json { "name": "feed fish", "isComplete": false }앞의 코드는 Content-Type 헤더와 JSON 요청 본문을 추가합니다.
새 PUT 요청 줄 위에 있는 요청 보내기 링크를 선택합니다.
PUT 요청이 앱으로 전송되고 응답 창에 응답이 표시됩니다. 응답 본문이 비어 있고 상태 코드는 204입니다.
PATCH 엔드포인트를 검사하십시오.
다음 코드를 사용하여 TodoPatchDto.cs라는 파일을 만듭니다.
public class TodoPatchDto
{
public string? Name { get; set; }
public bool? IsComplete { get; set; }
}
클래스는 TodoPatchDto nullable 속성(string? 및)을 사용하여 요청에 제공되지 않은 필드와 bool?명시적으로 값으로 설정된 필드를 구분합니다.
샘플 앱은 MapPatch를 사용하여 단일 PATCH 엔드포인트를 구현합니다.
app.MapPatch("/todoitems/{id}", async (int id, TodoPatchDto inputTodo, TodoDb db) =>
{
var todo = await db.Todos.FindAsync(id);
if (todo is null) return Results.NotFound();
if (inputTodo.Name is not null) todo.Name = inputTodo.Name;
if (inputTodo.IsComplete is not null) todo.IsComplete = inputTodo.IsComplete.Value;
await db.SaveChangesAsync();
return Results.NoContent();
});
이 메서드는 HTTP PATCH를 MapPut 사용하고 요청에 제공된 필드만 업데이트한다는 점을 제외하고 메서드와 비슷합니다. 성공적인 응답은 204(콘텐츠 없음)를 반환합니다. HTTP 사양에 따라 PATCH 요청은 부분 업데이트를 사용하도록 설정하므로 클라이언트는 변경해야 하는 필드만 보낼 수 있습니다.
PATCH 엔드포인트는 nullable 속성이 있는 TodoPatchDto 클래스를 사용하여 부분 업데이트를 제대로 처리합니다. nullable 속성을 사용하면 엔드포인트가 제공되지 않은 필드(null)와 명시적으로 값으로 설정된 필드(부울 필드의 경우 false 포함)를 구분할 수 있습니다. nullable 속성이 없으면 null을 허용하지 않는 부울은 기본적으로 false로 설정되며, 해당 필드가 요청에 포함되지 않은 경우 기존 true 값을 덮어쓸 수 있습니다.
Note
PATCH 작업을 통해 리소스를 부분적으로 업데이트할 수 있습니다. JSON 패치 문서를 사용하는 고급 부분 업데이트는 ASP.NET Core 웹 API의 JsonPatch를 참조하세요.
PATCH 엔드포인트를 테스트하십시오
이 샘플은 앱이 시작될 때마다 초기화되어야 하는 메모리 내 데이터베이스를 사용합니다. PATCH를 호출하기 전에 데이터베이스에 항목이 있어야 합니다. PATCH를 호출하기 전에 GET을 호출하여 데이터베이스에 항목이 있는지 확인합니다.
name이 있는 to-do 항목의 Id = 1 속성만 업데이트하고 이름을 "run errands"로 설정합니다.
엔드포인트 탐색기에서 PATCH 엔드포인트를 마우스 오른쪽 단추로 클릭하고 요청 생성을 선택합니다.
다음 콘텐츠가
TodoApi.http파일에 추가됩니다.PATCH {{TodoApi_HostAddress}}/todoitems/{id} ###PATCH 요청 줄에서 `
{id}`를 `1`로 교체합니다.PATCH 요청 줄 바로 다음에 다음 줄을 추가합니다.
Content-Type: application/json { "name": "run errands" }앞의 코드는 업데이트할 필드만 있는 Content-Type 헤더 및 JSON 요청 본문을 추가합니다.
새 PATCH 요청 줄 위에 있는 요청 보내기 링크를 선택합니다.
PATCH 요청이 앱으로 전송되고 응답 창에 응답 이 표시됩니다. 응답 본문이 비어 있고 상태 코드는 204입니다.
DELETE 엔드포인트 검사 및 테스트
샘플 앱은 MapDelete를 사용하여 단일 DELETE 엔드포인트를 구현합니다.
app.MapDelete("/todoitems/{id}", async (int id, TodoDb db) =>
{
if (await db.Todos.FindAsync(id) is Todo todo)
{
db.Todos.Remove(todo);
await db.SaveChangesAsync();
return Results.NoContent();
}
return Results.NotFound();
});
엔드포인트 탐색기에서 DELETE 엔드포인트를 마우스 오른쪽 단추로 클릭하고 요청 생성을 선택합니다.
DELETE 요청이
TodoApi.http에 추가됩니다.DELETE 요청 줄에서
{id}을1으로 바꾸세요. DELETE 요청은 다음 예제와 같습니다.DELETE {{TodoApi_HostAddress}}/todoitems/1 ###DELETE 요청에 대한 요청 보내기 링크를 선택합니다.
DELETE 요청이 앱으로 전송되고 응답 창에 응답이 표시됩니다. 응답 본문이 비어 있고 상태 코드는 204입니다.
MapGroup API 사용
샘플 앱 코드는 엔드포인트를 설정할 때마다 todoitems URL 접두사를 반복합니다. API에는 공통 URL 접두사를 사용하는 엔드포인트 그룹이 있는 경우가 많으며 MapGroup 메서드를 사용하여 이러한 그룹을 구성할 수 있습니다. 반복 코드를 줄이고 RequireAuthorization 및 WithMetadata와 같은 메서드에 대한 단일 호출로 전체 엔드포인트 그룹을 사용자 지정할 수 있습니다.
Program.cs의 내용을 다음 코드로 바꿉니다.
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
var app = builder.Build();
var todoItems = app.MapGroup("/todoitems");
todoItems.MapGet("/", async (TodoDb db) =>
await db.Todos.ToListAsync());
todoItems.MapGet("/complete", async (TodoDb db) =>
await db.Todos.Where(t => t.IsComplete).ToListAsync());
todoItems.MapGet("/{id}", async (int id, TodoDb db) =>
await db.Todos.FindAsync(id)
is Todo todo
? Results.Ok(todo)
: Results.NotFound());
todoItems.MapPost("/", async (Todo todo, TodoDb db) =>
{
db.Todos.Add(todo);
await db.SaveChangesAsync();
return Results.Created($"/todoitems/{todo.Id}", todo);
});
todoItems.MapPut("/{id}", async (int id, Todo inputTodo, TodoDb db) =>
{
var todo = await db.Todos.FindAsync(id);
if (todo is null) return Results.NotFound();
todo.Name = inputTodo.Name;
todo.IsComplete = inputTodo.IsComplete;
await db.SaveChangesAsync();
return Results.NoContent();
});
todoItems.MapDelete("/{id}", async (int id, TodoDb db) =>
{
if (await db.Todos.FindAsync(id) is Todo todo)
{
db.Todos.Remove(todo);
await db.SaveChangesAsync();
return Results.NoContent();
}
return Results.NotFound();
});
app.Run();
위의 코드에는 다음과 같은 변경 내용이 있습니다.
- URL 접두사
var todoItems = app.MapGroup("/todoitems");을 사용하여 그룹을 설정하도록/todoitems을 추가합니다. - 모든
app.Map<HttpVerb>메서드를todoItems.Map<HttpVerb>로 변경합니다. -
/todoitems메서드 호출에서 URL 접두사Map<HttpVerb>를 제거합니다.
엔드포인트를 테스트하여 동일하게 작동하는지 확인합니다.
TypedResults API 사용
TypedResults을 반환하는 것은 Results을 반환하는 것보다 테스트 용이성, 그리고 OpenAPI가 엔드포인트를 설명하기 위한 응답 형식 메타데이터를 자동으로 반환하는 등 여러 가지 이점이 있습니다. 자세한 내용은 TypedResults vs Results를 참조하세요.
Map<HttpVerb> 메서드는 람다를 사용하는 대신 경로 처리기 메서드를 호출할 수 있습니다. 예제를 보려면 다음 코드로 Program.cs를 업데이트합니다.
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
var app = builder.Build();
var todoItems = app.MapGroup("/todoitems");
todoItems.MapGet("/", GetAllTodos);
todoItems.MapGet("/complete", GetCompleteTodos);
todoItems.MapGet("/{id}", GetTodo);
todoItems.MapPost("/", CreateTodo);
todoItems.MapPut("/{id}", UpdateTodo);
todoItems.MapDelete("/{id}", DeleteTodo);
app.Run();
static async Task<IResult> GetAllTodos(TodoDb db)
{
return TypedResults.Ok(await db.Todos.ToArrayAsync());
}
static async Task<IResult> GetCompleteTodos(TodoDb db)
{
return TypedResults.Ok(await db.Todos.Where(t => t.IsComplete).ToListAsync());
}
static async Task<IResult> GetTodo(int id, TodoDb db)
{
return await db.Todos.FindAsync(id)
is Todo todo
? TypedResults.Ok(todo)
: TypedResults.NotFound();
}
static async Task<IResult> CreateTodo(Todo todo, TodoDb db)
{
db.Todos.Add(todo);
await db.SaveChangesAsync();
return TypedResults.Created($"/todoitems/{todo.Id}", todo);
}
static async Task<IResult> UpdateTodo(int id, Todo inputTodo, TodoDb db)
{
var todo = await db.Todos.FindAsync(id);
if (todo is null) return TypedResults.NotFound();
todo.Name = inputTodo.Name;
todo.IsComplete = inputTodo.IsComplete;
await db.SaveChangesAsync();
return TypedResults.NoContent();
}
static async Task<IResult> DeleteTodo(int id, TodoDb db)
{
if (await db.Todos.FindAsync(id) is Todo todo)
{
db.Todos.Remove(todo);
await db.SaveChangesAsync();
return TypedResults.NoContent();
}
return TypedResults.NotFound();
}
이제 Map<HttpVerb> 코드는 람다 대신 메서드를 호출합니다.
var todoItems = app.MapGroup("/todoitems");
todoItems.MapGet("/", GetAllTodos);
todoItems.MapGet("/complete", GetCompleteTodos);
todoItems.MapGet("/{id}", GetTodo);
todoItems.MapPost("/", CreateTodo);
todoItems.MapPut("/{id}", UpdateTodo);
todoItems.MapDelete("/{id}", DeleteTodo);
이러한 메서드는 IResult를 구현하고 TypedResults에서 정의한 개체를 반환합니다.
static async Task<IResult> GetAllTodos(TodoDb db)
{
return TypedResults.Ok(await db.Todos.ToArrayAsync());
}
static async Task<IResult> GetCompleteTodos(TodoDb db)
{
return TypedResults.Ok(await db.Todos.Where(t => t.IsComplete).ToListAsync());
}
static async Task<IResult> GetTodo(int id, TodoDb db)
{
return await db.Todos.FindAsync(id)
is Todo todo
? TypedResults.Ok(todo)
: TypedResults.NotFound();
}
static async Task<IResult> CreateTodo(Todo todo, TodoDb db)
{
db.Todos.Add(todo);
await db.SaveChangesAsync();
return TypedResults.Created($"/todoitems/{todo.Id}", todo);
}
static async Task<IResult> UpdateTodo(int id, Todo inputTodo, TodoDb db)
{
var todo = await db.Todos.FindAsync(id);
if (todo is null) return TypedResults.NotFound();
todo.Name = inputTodo.Name;
todo.IsComplete = inputTodo.IsComplete;
await db.SaveChangesAsync();
return TypedResults.NoContent();
}
static async Task<IResult> DeleteTodo(int id, TodoDb db)
{
if (await db.Todos.FindAsync(id) is Todo todo)
{
db.Todos.Remove(todo);
await db.SaveChangesAsync();
return TypedResults.NoContent();
}
return TypedResults.NotFound();
}
단위 테스트는 이러한 메서드를 호출하고 올바른 형식을 반환하는지 테스트할 수 있습니다. 예를 들어 메서드가 GetAllTodos인 경우:
static async Task<IResult> GetAllTodos(TodoDb db)
{
return TypedResults.Ok(await db.Todos.ToArrayAsync());
}
단위 테스트 코드는 Ok<Todo[]> 형식의 개체가 처리기 메서드에서 반환되는지 확인할 수 있습니다. 다음은 그 예입니다.
public async Task GetAllTodos_ReturnsOkOfTodosResult()
{
// Arrange
var db = CreateDbContext();
// Act
var result = await TodosApi.GetAllTodos(db);
// Assert: Check for the correct returned type
Assert.IsType<Ok<Todo[]>>(result);
}
초과 게시 방지
현재 샘플 앱은 전체 Todo 개체를 공개합니다. 프로덕션 앱 프로덕션 애플리케이션에서 모델의 하위 집합은 입력 및 반환될 수 있는 데이터를 제한하는 데 자주 사용됩니다. 이 동작에는 여러 가지 이유가 있으며, 보안이 주요 이유 중 하나입니다. 일반적으로 모델의 하위 집합을 DTO(데이터 전송 개체), 입력 모델 또는 뷰 모델이라고 합니다. 이 문서에서는 DTO를 사용합니다.
DTO를 사용하여 다음을 수행할 수 있습니다.
- 초과 게시를 방지합니다.
- 클라이언트에서 볼 수 없는 속성을 숨깁니다.
- 페이로드 크기를 줄이기 위해 일부 속성을 생략합니다.
- 중첩된 개체를 포함하는 개체 그래프를 평면화합니다. 클라이언트에는 평면화된 개체 그래프가 더 편리할 수 있습니다.
DTO 방법을 설명하려면 비밀 필드를 포함하도록 Todo 클래스를 업데이트합니다.
public class Todo
{
public int Id { get; set; }
public string? Name { get; set; }
public bool IsComplete { get; set; }
public string? Secret { get; set; }
}
이 앱에서는 숨겨진 필드를 숨겨야 하지만, 관리 앱은 숨겨진 필드를 공개할 수 있습니다.
비밀 필드를 게시하고 가져올 수 있는지를 확인하세요.
다음 코드를 사용하여 TodoItemDTO.cs라는 파일을 만듭니다.
public class TodoItemDTO
{
public int Id { get; set; }
public string? Name { get; set; }
public bool IsComplete { get; set; }
public TodoItemDTO() { }
public TodoItemDTO(Todo todoItem) =>
(Id, Name, IsComplete) = (todoItem.Id, todoItem.Name, todoItem.IsComplete);
}
이 DTO 모델을 사용하려면 파일의 Program.cs 내용을 다음 코드로 바꿉니다.
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
var app = builder.Build();
RouteGroupBuilder todoItems = app.MapGroup("/todoitems");
todoItems.MapGet("/", GetAllTodos);
todoItems.MapGet("/complete", GetCompleteTodos);
todoItems.MapGet("/{id}", GetTodo);
todoItems.MapPost("/", CreateTodo);
todoItems.MapPut("/{id}", UpdateTodo);
todoItems.MapDelete("/{id}", DeleteTodo);
app.Run();
static async Task<IResult> GetAllTodos(TodoDb db)
{
return TypedResults.Ok(await db.Todos.Select(x => new TodoItemDTO(x)).ToArrayAsync());
}
static async Task<IResult> GetCompleteTodos(TodoDb db) {
return TypedResults.Ok(await db.Todos.Where(t => t.IsComplete).Select(x => new TodoItemDTO(x)).ToListAsync());
}
static async Task<IResult> GetTodo(int id, TodoDb db)
{
return await db.Todos.FindAsync(id)
is Todo todo
? TypedResults.Ok(new TodoItemDTO(todo))
: TypedResults.NotFound();
}
static async Task<IResult> CreateTodo(TodoItemDTO todoItemDTO, TodoDb db)
{
var todoItem = new Todo
{
IsComplete = todoItemDTO.IsComplete,
Name = todoItemDTO.Name
};
db.Todos.Add(todoItem);
await db.SaveChangesAsync();
todoItemDTO = new TodoItemDTO(todoItem);
return TypedResults.Created($"/todoitems/{todoItem.Id}", todoItemDTO);
}
static async Task<IResult> UpdateTodo(int id, TodoItemDTO todoItemDTO, TodoDb db)
{
var todo = await db.Todos.FindAsync(id);
if (todo is null) return TypedResults.NotFound();
todo.Name = todoItemDTO.Name;
todo.IsComplete = todoItemDTO.IsComplete;
await db.SaveChangesAsync();
return TypedResults.NoContent();
}
static async Task<IResult> DeleteTodo(int id, TodoDb db)
{
if (await db.Todos.FindAsync(id) is Todo todo)
{
db.Todos.Remove(todo);
await db.SaveChangesAsync();
return TypedResults.NoContent();
}
return TypedResults.NotFound();
}
비밀 필드를 제외한 모든 필드를 게시하고 가져올 수 있는지 확인합니다.
완료된 샘플을 사용한 문제 해결
해결할 수 없는 문제가 발생한 경우 완료된 프로젝트와 코드를 비교합니다. 완료된 프로젝트를 보거나 다운로드합니다(다운로드 방법).
다음 단계
- JSON serialization 옵션을 구성합니다.
- 오류 및 예외 처리: 최소 API 앱에 대한 환경에서는 개발자 예외 페이지 가
Development기본적으로 사용하도록 설정됩니다. 오류 및 예외를 처리하는 방법에 대한 자세한 내용은 ASP.NET Core API에서 오류 처리를 참조하세요. - 최소 API 앱을 테스트하는 예제는 이 GitHub 샘플을 참조하세요.
- 최소 API에서 OpenAPI를 지원합니다.
- 빠른 시작: Azure에 게시합니다.
- ASP.NET Core 최소 API 구성
Learn more
최소 API 빠른 참조 참조하세요.
최소 API는 최소한의 종속성으로 HTTP API를 만들도록 설계되었습니다. ASP.NET Core에 최소 파일, 기능 및 종속성만 포함하려는 마이크로 서비스 및 앱에 이상적입니다.
이 자습서에서는 ASP.NET Core를 사용하여 최소 API를 빌드하는 기본 사항을 설명합니다. ASP.NET Core에서 API를 만드는 또 다른 방법은 컨트롤러를 사용하는 것입니다. 최소 API와 컨트롤러 기반 API 중에서 선택하는 방법에 대한 도움말은 API 개요를 참조하세요. 더 많은 기능이 포함된 컨트롤러를 기반으로 API 프로젝트를 만드는 방법에 대한 자습서는 웹 API 만들기를 참조하세요.
Overview
이 자습서에서는 다음 API를 만듭니다.
| API | Description | 요청 메시지 본문 | 응답 본문 |
|---|---|---|---|
GET /todoitems |
할 일 항목 모두 가져오기 | None | 할 일 항목의 배열 |
GET /todoitems/complete |
완성된 할 일 항목 가져오기 | None | 할 일 항목의 배열 |
GET /todoitems/{id} |
ID로 항목 가져오기 | None | 할 일 항목 |
POST /todoitems |
새 항목 추가 | 할 일 항목 | 할 일 항목 |
PUT /todoitems/{id} |
기존 항목 업데이트 | 할 일 항목 | None |
PATCH /todoitems/{id} |
항목 부분 업데이트 | 부분 할 일 항목 | None |
DELETE /todoitems/{id} |
항목 삭제 | None | None |
Prerequisites
ASP.NET 및 웹 개발 워크로드가 있는 Visual Studio 2022
API 프로젝트 만들기
Visual Studio 2022를 시작하고 새 프로젝트 만들기를 선택합니다.
새 프로젝트 만들기 대화 상자에서 다음을 수행합니다.
-
Empty검색 상자에 를 입력합니다. - ASP.NET Core Empty 템플릿을 선택하고 다음을 선택합니다.
-
프로젝트 이름을 TodoApi로 지정하고 다음을 선택합니다.
추가 정보 대화 상자에서 다음을 수행합니다.
- .NET 9.0 선택합니다.
- 최상위 문 사용 안 함 옵션의 체크를 해제
- 만들기를 선택하세요
코드 검사
Program.cs 파일에는 다음 코드가 포함되어 있습니다.
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/", () => "Hello World!");
app.Run();
앞의 코드가 하는 역할은 다음과 같습니다.
- 미리 구성된 기본값을 사용하여 WebApplicationBuilder 및 WebApplication을 만듭니다.
- 를 반환
/하는 HTTP GET 엔드포인트헬로 월드!를 만듭니다.
앱 실행
Ctrl+F5를 눌러 디버거 없이 실행합니다.
Visual Studio는 다음 대화 상자를 표시합니다.
IIS Express SSL 인증서를 신뢰하는 경우 예를 선택합니다.
다음 대화 상자가 표시됩니다.
개발 인증서를 신뢰하는 데 동의하는 경우 예를 선택합니다.
Firefox 브라우저를 신뢰하는 방법에 대한 자세한 내용은 Firefox SEC_ERROR_INADEQUATE_KEY_USAGE 인증서 오류를 참조하세요.
Visual Studio에서 Kestrel 웹 서버를 시작하고 브라우저 창을 엽니다.
헬로 월드!가 브라우저에 표시됩니다.
Program.cs 파일에는 최소이지만 완전한 앱이 포함되어 있습니다.
브라우저 창을 닫습니다.
NuGet 패키지 추가
이 자습서에서 사용되는 데이터베이스 및 진단을 지원하려면 NuGet 패키지를 추가해야 합니다.
- 도구 메뉴에서 NuGet 패키지 관리자 > 솔루션용 NuGet 패키지 관리를 선택합니다.
- 찾아보기 탭을 선택합니다.
- 미리 보기 버전 포함을 선택합니다.
- 검색 상자에 Microsoft.EntityFrameworkCore.InMemory를 입력한 다음
Microsoft.EntityFrameworkCore.InMemory를 선택합니다. - 오른쪽 창에서 프로젝트 확인란을 선택하고 설치를 선택합니다.
- 이전 지침에 따라
Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore패키지를 추가합니다.
모델 및 데이터베이스 컨텍스트 클래스
- 프로젝트 폴더에서 다음 코드가 포함된
Todo.cs라는 파일을 만듭니다.
public class Todo
{
public int Id { get; set; }
public string? Name { get; set; }
public bool IsComplete { get; set; }
}
앞의 코드는 이 앱에 대한 모델을 만듭니다. 모델은 앱에서 관리하는 데이터를 나타내는 일련의 클래스입니다.
- 다음 코드를 사용하여
TodoDb.cs라는 파일을 만듭니다.
using Microsoft.EntityFrameworkCore;
class TodoDb : DbContext
{
public TodoDb(DbContextOptions<TodoDb> options)
: base(options) { }
public DbSet<Todo> Todos => Set<Todo>();
}
앞의 코드는 데이터 모델에 대한 Entity Framework 기능을 조정하는 기본 클래스인 데이터베이스 컨텍스트를 정의합니다. 이 클래스는 Microsoft.EntityFrameworkCore.DbContext 클래스에서 파생됩니다.
API 코드 추가
-
Program.cs파일의 내용을 다음 코드로 바꿉니다.
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
var app = builder.Build();
app.MapGet("/todoitems", async (TodoDb db) =>
await db.Todos.ToListAsync());
app.MapGet("/todoitems/complete", async (TodoDb db) =>
await db.Todos.Where(t => t.IsComplete).ToListAsync());
app.MapGet("/todoitems/{id}", async (int id, TodoDb db) =>
await db.Todos.FindAsync(id)
is Todo todo
? Results.Ok(todo)
: Results.NotFound());
app.MapPost("/todoitems", async (Todo todo, TodoDb db) =>
{
db.Todos.Add(todo);
await db.SaveChangesAsync();
return Results.Created($"/todoitems/{todo.Id}", todo);
});
app.MapPut("/todoitems/{id}", async (int id, Todo inputTodo, TodoDb db) =>
{
var todo = await db.Todos.FindAsync(id);
if (todo is null) return Results.NotFound();
todo.Name = inputTodo.Name;
todo.IsComplete = inputTodo.IsComplete;
await db.SaveChangesAsync();
return Results.NoContent();
});
app.MapDelete("/todoitems/{id}", async (int id, TodoDb db) =>
{
if (await db.Todos.FindAsync(id) is Todo todo)
{
db.Todos.Remove(todo);
await db.SaveChangesAsync();
return Results.NoContent();
}
return Results.NotFound();
});
app.Run();
강조 표시된 다음 코드는 DI(종속성 주입) 컨테이너에 데이터베이스 컨텍스트를 추가하고 데이터베이스 관련 예외를 표시할 수 있도록 합니다.
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
var app = builder.Build();
DI 컨테이너는 데이터베이스 컨텍스트 및 기타 서비스에 대한 액세스를 제공합니다.
이 자습서에서는 엔드포인트 탐색기 및 .http 파일을 사용하여 API를 테스트합니다.
데이터 게시 테스트
Program.cs에서 다음 코드는 HTTP POST 엔드포인트 /todoitems를 만들어 메모리 내 데이터베이스에 데이터를 추가합니다.
app.MapPost("/todoitems", async (Todo todo, TodoDb db) =>
{
db.Todos.Add(todo);
await db.SaveChangesAsync();
return Results.Created($"/todoitems/{todo.Id}", todo);
});
앱을 실행합니다. 더 이상 / 엔드포인트가 없으므로 브라우저에 404 오류가 표시됩니다.
POST 엔드포인트는 앱에 데이터를 추가하는 데 사용됩니다.
선택 보기>다른 Windows>Endpoints 탐색기.
POST 엔드포인트를 마우스 오른쪽 단추로 클릭하고 요청 생성을 선택합니다.
다음 예제와 유사한 내용이 포함된 새 파일이 프로젝트 폴더
TodoApi.http에 만들어집니다.@TodoApi_HostAddress = https://localhost:7031 POST {{TodoApi_HostAddress}}/todoitems ###- 첫 번째 줄은 모든 엔드포인트에 사용되는 변수를 만듭니다.
- 다음 줄은 POST 요청을 정의합니다.
- 삼중 해시 태그(
###) 줄은 요청 구분 기호입니다. 이는 다른 요청에 대한 것입니다.
POST 요청에는 헤더와 본문이 필요합니다. 요청의 해당 부분을 정의하려면 POST 요청 줄 바로 다음에 다음 줄을 추가합니다.
Content-Type: application/json { "name":"walk dog", "isComplete":true }앞의 코드는 Content-Type 헤더와 JSON 요청 본문을 추가합니다. 이제 TodoApi.http 파일은 다음 예제와 비슷하게 보이지만, 사용자 본인의 포트 번호를 사용해야 합니다.
@TodoApi_HostAddress = https://localhost:7057 POST {{TodoApi_HostAddress}}/todoitems Content-Type: application/json { "name":"walk dog", "isComplete":true } ###앱을 실행합니다.
요청 줄 위에
POST있는 요청 보내기 링크를 선택합니다.
POST 요청이 앱으로 전송되고 응답 창에 응답이 표시됩니다.
GET 엔드포인트 검사
샘플 앱은 MapGet을 호출하여 여러 GET 엔드포인트를 구현합니다.
| API | Description | 요청 메시지 본문 | 응답 본문 |
|---|---|---|---|
GET /todoitems |
할 일 항목 모두 가져오기 | None | 할 일 항목의 배열 |
GET /todoitems/complete |
완성된 모든 할 일 항목 가져오기 | None | 할 일 항목의 배열 |
GET /todoitems/{id} |
ID로 항목 가져오기 | None | 할 일 항목 |
app.MapGet("/todoitems", async (TodoDb db) =>
await db.Todos.ToListAsync());
app.MapGet("/todoitems/complete", async (TodoDb db) =>
await db.Todos.Where(t => t.IsComplete).ToListAsync());
app.MapGet("/todoitems/{id}", async (int id, TodoDb db) =>
await db.Todos.FindAsync(id)
is Todo todo
? Results.Ok(todo)
: Results.NotFound());
GET 엔드포인트 테스트
브라우저에서 엔드포인트를 GET 호출하거나 엔드포인트 탐색기를 사용하여 앱을 테스트합니다. 다음 단계는 엔드포인트 탐색기를 위한 것입니다.
엔드포인트 탐색기에서 첫 번째 GET 엔드포인트를 마우스 오른쪽 단추로 클릭하고 요청 생성을 선택합니다.
다음 콘텐츠가
TodoApi.http파일에 추가됩니다.GET {{TodoApi_HostAddress}}/todoitems ###새로운 요청 줄 위에 있는 요청 보내기 링크를 선택합니다.
GET 요청이 앱으로 전송되고 응답 창에 응답이 표시됩니다.
응답 본문은 다음 JSON과 유사합니다.
[ { "id": 1, "name": "walk dog", "isComplete": true } ]엔드포인트 탐색기에서 GET 엔드포인트를 마우스 오른쪽 버튼으로 클릭하고
/todoitems/{id}을 선택하세요. 다음 콘텐츠가TodoApi.http파일에 추가됩니다.GET {{TodoApi_HostAddress}}/todoitems/{id} ###{id}를1로 교체합니다.새 GET 요청 줄 위에 있는 요청 보내기 링크를 선택합니다.
GET 요청이 앱으로 전송되고 응답 창에 응답이 표시됩니다.
응답 본문은 다음 JSON과 유사합니다.
{ "id": 1, "name": "walk dog", "isComplete": true }
이 앱은 메모리 내 데이터베이스를 사용합니다. 앱이 다시 시작되면 GET 요청이 데이터를 반환하지 않습니다. 데이터가 반환되지 않으면 POST 데이터를 앱에 게시하고 GET 요청을 다시 시도합니다.
반환 값
ASP.NET Core는 자동으로 JSON에 개체를 직렬화하고 JSON을 응답 메시지의 본문에 기록합니다. 이 반환 형식의 응답 코드는 200 OK이며 처리되지 않은 예외가 없다고 가정합니다. 처리되지 않은 예외는 5xx 오류로 변환됩니다.
반환 형식은 다양한 HTTP 상태 코드를 나타낼 수 있습니다. 예를 들어 GET /todoitems/{id}은 두 가지 상태 값을 반환할 수 있습니다.
- 요청된 ID와 일치하는 항목이 없는 경우 메서드에서 404 상태NotFound 오류 코드를 반환합니다.
- 그렇지 않으면 메서드가 JSON 응답 본문에서 200을 반환합니다.
item을 반환하면 HTTP 200 응답이 발생합니다.
PUT 엔드포인트 검사
샘플 앱은 MapPut을 사용하여 단일 PUT 엔드포인트를 구현합니다.
app.MapPut("/todoitems/{id}", async (int id, Todo inputTodo, TodoDb db) =>
{
var todo = await db.Todos.FindAsync(id);
if (todo is null) return Results.NotFound();
todo.Name = inputTodo.Name;
todo.IsComplete = inputTodo.IsComplete;
await db.SaveChangesAsync();
return Results.NoContent();
});
이 메서드는 HTTP PUT을 사용한다는 점을 제외하고 MapPost 메서드와 비슷합니다. 성공적인 응답은 204(콘텐츠 없음)를 반환합니다. HTTP 사양에 따라 PUT 요청의 경우 클라이언트는 변경 내용만이 아니라 전체 업데이트된 엔터티를 보내야 합니다. 부분 업데이트를 지원하려면 HTTP PATCH를 사용합니다.
PUT 엔드포인트 테스트
이 샘플은 앱이 시작될 때마다 초기화되어야 하는 메모리 내 데이터베이스를 사용합니다. PUT 호출을 실행하기 전에 데이터베이스에 항목이 있어야 합니다. GET을 호출하여 PUT 호출을 실행하기 전에 데이터베이스에 항목이 있는지 확인합니다.
할 일 항목을 Id = 1 업데이트한 다음, 이름을 "feed fish"로 설정합니다.
엔드포인트 탐색기에서 PUT 엔드포인트를 마우스 오른쪽 단추로 클릭하고 요청 생성을 선택합니다.
다음 콘텐츠가
TodoApi.http파일에 추가됩니다.PUT {{TodoApi_HostAddress}}/todoitems/{id} ###PUT 요청 줄에서 `
{id}`를 `1`로 교체하세요.PUT 요청 줄 바로 다음에 다음 줄을 추가합니다.
Content-Type: application/json { "id": 1, "name": "feed fish", "isComplete": false }앞의 코드는 Content-Type 헤더와 JSON 요청 본문을 추가합니다.
새 PUT 요청 줄 위에 있는 요청 보내기 링크를 선택합니다.
PUT 요청이 앱으로 전송되고 응답 창에 응답이 표시됩니다. 응답 본문이 비어 있고 상태 코드는 204입니다.
PATCH 엔드포인트를 검사하십시오.
다음 코드를 사용하여 TodoPatchDto.cs라는 파일을 만듭니다.
public class TodoPatchDto
{
public string? Name { get; set; }
public bool? IsComplete { get; set; }
}
클래스는 TodoPatchDto nullable 속성(string? 및)을 사용하여 요청에 제공되지 않은 필드와 bool?명시적으로 값으로 설정된 필드를 구분합니다.
샘플 앱은 MapPatch를 사용하여 단일 PATCH 엔드포인트를 구현합니다.
app.MapPatch("/todoitems/{id}", async (int id, TodoPatchDto inputTodo, TodoDb db) =>
{
var todo = await db.Todos.FindAsync(id);
if (todo is null) return Results.NotFound();
if (inputTodo.Name is not null) todo.Name = inputTodo.Name;
if (inputTodo.IsComplete is not null) todo.IsComplete = inputTodo.IsComplete.Value;
await db.SaveChangesAsync();
return Results.NoContent();
});
이 메서드는 HTTP PATCH를 MapPut 사용하고 요청에 제공된 필드만 업데이트한다는 점을 제외하고 메서드와 비슷합니다. 성공적인 응답은 204(콘텐츠 없음)를 반환합니다. HTTP 사양에 따라 PATCH 요청은 부분 업데이트를 사용하도록 설정하므로 클라이언트는 변경해야 하는 필드만 보낼 수 있습니다.
PATCH 엔드포인트는 nullable 속성이 있는 TodoPatchDto 클래스를 사용하여 부분 업데이트를 제대로 처리합니다. nullable 속성을 사용하면 엔드포인트가 제공되지 않은 필드(null)와 명시적으로 값으로 설정된 필드(부울 필드의 경우 false 포함)를 구분할 수 있습니다. nullable 속성이 없으면 null을 허용하지 않는 부울은 기본적으로 false로 설정되며, 해당 필드가 요청에 포함되지 않은 경우 기존 true 값을 덮어쓸 수 있습니다.
Note
PATCH 작업을 통해 리소스를 부분적으로 업데이트할 수 있습니다. JSON 패치 문서를 사용하는 고급 부분 업데이트는 ASP.NET Core 웹 API의 JsonPatch를 참조하세요.
PATCH 엔드포인트를 테스트하십시오
이 샘플은 앱이 시작될 때마다 초기화되어야 하는 메모리 내 데이터베이스를 사용합니다. PATCH를 호출하기 전에 데이터베이스에 항목이 있어야 합니다. PATCH를 호출하기 전에 GET을 호출하여 데이터베이스에 항목이 있는지 확인합니다.
name이 있는 to-do 항목의 Id = 1 속성만 업데이트하고 이름을 "run errands"로 설정합니다.
엔드포인트 탐색기에서 PATCH 엔드포인트를 마우스 오른쪽 단추로 클릭하고 요청 생성을 선택합니다.
다음 콘텐츠가
TodoApi.http파일에 추가됩니다.PATCH {{TodoApi_HostAddress}}/todoitems/{id} ###PATCH 요청 줄에서 `
{id}`를 `1`로 교체합니다.PATCH 요청 줄 바로 다음에 다음 줄을 추가합니다.
Content-Type: application/json { "name": "run errands" }앞의 코드는 업데이트할 필드만 있는 Content-Type 헤더 및 JSON 요청 본문을 추가합니다.
새 PATCH 요청 줄 위에 있는 요청 보내기 링크를 선택합니다.
PATCH 요청이 앱으로 전송되고 응답 창에 응답 이 표시됩니다. 응답 본문이 비어 있고 상태 코드는 204입니다.
DELETE 엔드포인트 검사 및 테스트
샘플 앱은 MapDelete를 사용하여 단일 DELETE 엔드포인트를 구현합니다.
app.MapDelete("/todoitems/{id}", async (int id, TodoDb db) =>
{
if (await db.Todos.FindAsync(id) is Todo todo)
{
db.Todos.Remove(todo);
await db.SaveChangesAsync();
return Results.NoContent();
}
return Results.NotFound();
});
엔드포인트 탐색기에서 DELETE 엔드포인트를 마우스 오른쪽 단추로 클릭하고 요청 생성을 선택합니다.
DELETE 요청이
TodoApi.http에 추가됩니다.DELETE 요청 줄에서
{id}을1으로 바꾸세요. DELETE 요청은 다음 예제와 같습니다.DELETE {{TodoApi_HostAddress}}/todoitems/1 ###DELETE 요청에 대한 요청 보내기 링크를 선택합니다.
DELETE 요청이 앱으로 전송되고 응답 창에 응답이 표시됩니다. 응답 본문이 비어 있고 상태 코드는 204입니다.
MapGroup API 사용
샘플 앱 코드는 엔드포인트를 설정할 때마다 todoitems URL 접두사를 반복합니다. API에는 공통 URL 접두사를 사용하는 엔드포인트 그룹이 있는 경우가 많으며 MapGroup 메서드를 사용하여 이러한 그룹을 구성할 수 있습니다. 반복 코드를 줄이고 RequireAuthorization 및 WithMetadata와 같은 메서드에 대한 단일 호출로 전체 엔드포인트 그룹을 사용자 지정할 수 있습니다.
Program.cs의 내용을 다음 코드로 바꿉니다.
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
var app = builder.Build();
var todoItems = app.MapGroup("/todoitems");
todoItems.MapGet("/", async (TodoDb db) =>
await db.Todos.ToListAsync());
todoItems.MapGet("/complete", async (TodoDb db) =>
await db.Todos.Where(t => t.IsComplete).ToListAsync());
todoItems.MapGet("/{id}", async (int id, TodoDb db) =>
await db.Todos.FindAsync(id)
is Todo todo
? Results.Ok(todo)
: Results.NotFound());
todoItems.MapPost("/", async (Todo todo, TodoDb db) =>
{
db.Todos.Add(todo);
await db.SaveChangesAsync();
return Results.Created($"/todoitems/{todo.Id}", todo);
});
todoItems.MapPut("/{id}", async (int id, Todo inputTodo, TodoDb db) =>
{
var todo = await db.Todos.FindAsync(id);
if (todo is null) return Results.NotFound();
todo.Name = inputTodo.Name;
todo.IsComplete = inputTodo.IsComplete;
await db.SaveChangesAsync();
return Results.NoContent();
});
todoItems.MapDelete("/{id}", async (int id, TodoDb db) =>
{
if (await db.Todos.FindAsync(id) is Todo todo)
{
db.Todos.Remove(todo);
await db.SaveChangesAsync();
return Results.NoContent();
}
return Results.NotFound();
});
app.Run();
위의 코드에는 다음과 같은 변경 내용이 있습니다.
- URL 접두사
var todoItems = app.MapGroup("/todoitems");을 사용하여 그룹을 설정하도록/todoitems을 추가합니다. - 모든
app.Map<HttpVerb>메서드를todoItems.Map<HttpVerb>로 변경합니다. -
/todoitems메서드 호출에서 URL 접두사Map<HttpVerb>를 제거합니다.
엔드포인트를 테스트하여 동일하게 작동하는지 확인합니다.
TypedResults API 사용
TypedResults을 반환하는 것은 Results을 반환하는 것보다 테스트 용이성, 그리고 OpenAPI가 엔드포인트를 설명하기 위한 응답 형식 메타데이터를 자동으로 반환하는 등 여러 가지 이점이 있습니다. 자세한 내용은 TypedResults vs Results를 참조하세요.
Map<HttpVerb> 메서드는 람다를 사용하는 대신 경로 처리기 메서드를 호출할 수 있습니다. 예제를 보려면 다음 코드로 Program.cs를 업데이트합니다.
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
var app = builder.Build();
var todoItems = app.MapGroup("/todoitems");
todoItems.MapGet("/", GetAllTodos);
todoItems.MapGet("/complete", GetCompleteTodos);
todoItems.MapGet("/{id}", GetTodo);
todoItems.MapPost("/", CreateTodo);
todoItems.MapPut("/{id}", UpdateTodo);
todoItems.MapDelete("/{id}", DeleteTodo);
app.Run();
static async Task<IResult> GetAllTodos(TodoDb db)
{
return TypedResults.Ok(await db.Todos.ToArrayAsync());
}
static async Task<IResult> GetCompleteTodos(TodoDb db)
{
return TypedResults.Ok(await db.Todos.Where(t => t.IsComplete).ToListAsync());
}
static async Task<IResult> GetTodo(int id, TodoDb db)
{
return await db.Todos.FindAsync(id)
is Todo todo
? TypedResults.Ok(todo)
: TypedResults.NotFound();
}
static async Task<IResult> CreateTodo(Todo todo, TodoDb db)
{
db.Todos.Add(todo);
await db.SaveChangesAsync();
return TypedResults.Created($"/todoitems/{todo.Id}", todo);
}
static async Task<IResult> UpdateTodo(int id, Todo inputTodo, TodoDb db)
{
var todo = await db.Todos.FindAsync(id);
if (todo is null) return TypedResults.NotFound();
todo.Name = inputTodo.Name;
todo.IsComplete = inputTodo.IsComplete;
await db.SaveChangesAsync();
return TypedResults.NoContent();
}
static async Task<IResult> DeleteTodo(int id, TodoDb db)
{
if (await db.Todos.FindAsync(id) is Todo todo)
{
db.Todos.Remove(todo);
await db.SaveChangesAsync();
return TypedResults.NoContent();
}
return TypedResults.NotFound();
}
이제 Map<HttpVerb> 코드는 람다 대신 메서드를 호출합니다.
var todoItems = app.MapGroup("/todoitems");
todoItems.MapGet("/", GetAllTodos);
todoItems.MapGet("/complete", GetCompleteTodos);
todoItems.MapGet("/{id}", GetTodo);
todoItems.MapPost("/", CreateTodo);
todoItems.MapPut("/{id}", UpdateTodo);
todoItems.MapDelete("/{id}", DeleteTodo);
이러한 메서드는 IResult를 구현하고 TypedResults에서 정의한 개체를 반환합니다.
static async Task<IResult> GetAllTodos(TodoDb db)
{
return TypedResults.Ok(await db.Todos.ToArrayAsync());
}
static async Task<IResult> GetCompleteTodos(TodoDb db)
{
return TypedResults.Ok(await db.Todos.Where(t => t.IsComplete).ToListAsync());
}
static async Task<IResult> GetTodo(int id, TodoDb db)
{
return await db.Todos.FindAsync(id)
is Todo todo
? TypedResults.Ok(todo)
: TypedResults.NotFound();
}
static async Task<IResult> CreateTodo(Todo todo, TodoDb db)
{
db.Todos.Add(todo);
await db.SaveChangesAsync();
return TypedResults.Created($"/todoitems/{todo.Id}", todo);
}
static async Task<IResult> UpdateTodo(int id, Todo inputTodo, TodoDb db)
{
var todo = await db.Todos.FindAsync(id);
if (todo is null) return TypedResults.NotFound();
todo.Name = inputTodo.Name;
todo.IsComplete = inputTodo.IsComplete;
await db.SaveChangesAsync();
return TypedResults.NoContent();
}
static async Task<IResult> DeleteTodo(int id, TodoDb db)
{
if (await db.Todos.FindAsync(id) is Todo todo)
{
db.Todos.Remove(todo);
await db.SaveChangesAsync();
return TypedResults.NoContent();
}
return TypedResults.NotFound();
}
단위 테스트는 이러한 메서드를 호출하고 올바른 형식을 반환하는지 테스트할 수 있습니다. 예를 들어 메서드가 GetAllTodos인 경우:
static async Task<IResult> GetAllTodos(TodoDb db)
{
return TypedResults.Ok(await db.Todos.ToArrayAsync());
}
단위 테스트 코드는 Ok<Todo[]> 형식의 개체가 처리기 메서드에서 반환되는지 확인할 수 있습니다. 다음은 그 예입니다.
public async Task GetAllTodos_ReturnsOkOfTodosResult()
{
// Arrange
var db = CreateDbContext();
// Act
var result = await TodosApi.GetAllTodos(db);
// Assert: Check for the correct returned type
Assert.IsType<Ok<Todo[]>>(result);
}
초과 게시 방지
현재 샘플 앱은 전체 Todo 개체를 공개합니다. 프로덕션 애플리케이션에서 모델의 하위 집합은 입력 및 반환될 수 있는 데이터를 제한하는 데 자주 사용됩니다. 이 동작에는 여러 가지 이유가 있으며, 보안이 주요 이유 중 하나입니다. 일반적으로 모델의 하위 집합을 DTO(데이터 전송 개체), 입력 모델 또는 뷰 모델이라고 합니다. 이 문서에서는 DTO를 사용합니다.
DTO를 사용하여 다음을 수행할 수 있습니다.
- 초과 게시를 방지합니다.
- 클라이언트에서 볼 수 없는 속성을 숨깁니다.
- 페이로드 크기를 줄이기 위해 일부 속성을 생략합니다.
- 중첩된 개체를 포함하는 개체 그래프를 평면화합니다. 클라이언트에는 평면화된 개체 그래프가 더 편리할 수 있습니다.
DTO 방법을 설명하려면 비밀 필드를 포함하도록 Todo 클래스를 업데이트합니다.
public class Todo
{
public int Id { get; set; }
public string? Name { get; set; }
public bool IsComplete { get; set; }
public string? Secret { get; set; }
}
이 앱에서는 숨겨진 필드를 숨겨야 하지만, 관리 앱은 숨겨진 필드를 공개할 수 있습니다.
비밀 필드를 게시하고 가져올 수 있는지를 확인하세요.
다음 코드를 사용하여 TodoItemDTO.cs라는 파일을 만듭니다.
public class TodoItemDTO
{
public int Id { get; set; }
public string? Name { get; set; }
public bool IsComplete { get; set; }
public TodoItemDTO() { }
public TodoItemDTO(Todo todoItem) =>
(Id, Name, IsComplete) = (todoItem.Id, todoItem.Name, todoItem.IsComplete);
}
이 DTO 모델을 사용하려면 파일의 Program.cs 내용을 다음 코드로 바꿉니다.
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
var app = builder.Build();
RouteGroupBuilder todoItems = app.MapGroup("/todoitems");
todoItems.MapGet("/", GetAllTodos);
todoItems.MapGet("/complete", GetCompleteTodos);
todoItems.MapGet("/{id}", GetTodo);
todoItems.MapPost("/", CreateTodo);
todoItems.MapPut("/{id}", UpdateTodo);
todoItems.MapDelete("/{id}", DeleteTodo);
app.Run();
static async Task<IResult> GetAllTodos(TodoDb db)
{
return TypedResults.Ok(await db.Todos.Select(x => new TodoItemDTO(x)).ToArrayAsync());
}
static async Task<IResult> GetCompleteTodos(TodoDb db) {
return TypedResults.Ok(await db.Todos.Where(t => t.IsComplete).Select(x => new TodoItemDTO(x)).ToListAsync());
}
static async Task<IResult> GetTodo(int id, TodoDb db)
{
return await db.Todos.FindAsync(id)
is Todo todo
? TypedResults.Ok(new TodoItemDTO(todo))
: TypedResults.NotFound();
}
static async Task<IResult> CreateTodo(TodoItemDTO todoItemDTO, TodoDb db)
{
var todoItem = new Todo
{
IsComplete = todoItemDTO.IsComplete,
Name = todoItemDTO.Name
};
db.Todos.Add(todoItem);
await db.SaveChangesAsync();
todoItemDTO = new TodoItemDTO(todoItem);
return TypedResults.Created($"/todoitems/{todoItem.Id}", todoItemDTO);
}
static async Task<IResult> UpdateTodo(int id, TodoItemDTO todoItemDTO, TodoDb db)
{
var todo = await db.Todos.FindAsync(id);
if (todo is null) return TypedResults.NotFound();
todo.Name = todoItemDTO.Name;
todo.IsComplete = todoItemDTO.IsComplete;
await db.SaveChangesAsync();
return TypedResults.NoContent();
}
static async Task<IResult> DeleteTodo(int id, TodoDb db)
{
if (await db.Todos.FindAsync(id) is Todo todo)
{
db.Todos.Remove(todo);
await db.SaveChangesAsync();
return TypedResults.NoContent();
}
return TypedResults.NotFound();
}
비밀 필드를 제외한 모든 필드를 게시하고 가져올 수 있는지 확인합니다.
완료된 샘플을 사용한 문제 해결
해결할 수 없는 문제가 발생한 경우 완료된 프로젝트와 코드를 비교합니다. 완료된 프로젝트를 보거나 다운로드합니다(다운로드 방법).
다음 단계
- JSON serialization 옵션을 구성합니다.
- 오류 및 예외 처리: 최소 API 앱에 대한 환경에서는 개발자 예외 페이지 가
Development기본적으로 사용하도록 설정됩니다. 오류 및 예외를 처리하는 방법에 대한 자세한 내용은 ASP.NET Core API에서 오류 처리를 참조하세요. - 최소 API 앱을 테스트하는 예제는 이 GitHub 샘플을 참조하세요.
- 최소 API에서 OpenAPI를 지원합니다.
- 빠른 시작: Azure에 게시합니다.
- ASP.NET Core 최소 API 구성
Learn more
최소 API 빠른 참조 참조하세요.
ASP.NET Core