다음을 통해 공유


자습서: ASP.NET Core를 사용하여 최소 API 만들기

참고 항목

이 문서의 최신 버전은 아닙니다. 현재 릴리스는 이 문서의 .NET 8 버전을 참조 하세요.

Warning

이 버전의 ASP.NET Core는 더 이상 지원되지 않습니다. 자세한 내용은 .NET 및 .NET Core 지원 정책을 참조 하세요. 현재 릴리스는 이 문서의 .NET 8 버전을 참조 하세요.

Important

이 정보는 상업적으로 출시되기 전에 실질적으로 수정될 수 있는 시험판 제품과 관련이 있습니다. Microsoft는 여기에 제공된 정보에 대해 어떠한 명시적, 또는 묵시적인 보증을 하지 않습니다.

현재 릴리스는 이 문서의 .NET 8 버전을 참조 하세요.

작성자: Rick AndersonTom Dykstra

최소 API는 최소한의 종속성으로 HTTP API를 만들도록 설계되었습니다. ASP.NET Core에 최소 파일, 기능 및 종속성만 포함하려는 마이크로 서비스 및 앱에 이상적입니다.

이 자습서에서는 ASP.NET Core를 사용한 최소 API를 빌드하는 기본 사항을 설명합니다. ASP.NET Core에서 API를 만드는 또 다른 방법은 컨트롤러를 사용하는 것입니다. 최소 API와 컨트롤러 기반 API 중에서 선택하는 방법에 대한 도움말은 API 개요를 참조하세요. 더 많은 기능이 포함된 컨트롤러를 기반으로 API 프로젝트를 만드는 방법에 대한 자습서는 웹 API 만들기를 참조하세요.

개요

이 자습서에서는 다음 API를 만듭니다.

API 설명 요청 본문 응답 본문
GET /todoitems 할 일 항목 모두 가져오기 None 할 일 항목의 배열
GET /todoitems/complete 완성된 할 일 항목 가져오기 None 할 일 항목의 배열
GET /todoitems/{id} ID로 항목 가져오기 None 할 일 항목
POST /todoitems 새 항목 추가 할 일 항목 할 일 항목
PUT /todoitems/{id} 기존 항목 업데이트 할 일 항목 None
DELETE /todoitems/{id}     항목 삭제 None None

필수 조건

API 프로젝트 만들기

  • Visual Studio 2022를 시작하고 새 프로젝트 만들기를 선택합니다.

  • 새 프로젝트 만들기 대화 상자에서 다음을 수행합니다.

    • 템플릿 검색 검색 상자에 Empty를 입력합니다.
    • ASP.NET Core Empty 템플릿을 선택하고 다음을 선택합니다.

    Visual Studio 새 프로젝트 만들기

  • 프로젝트 이름을 TodoApi로 지정하고 다음을 선택합니다.

  • 추가 정보 대화 상자에서 다음을 수행합니다.

    • .NET 9.0(미리 보기) 선택
    • 최상위 문 사용 안 함 선택 취소
    • 만들기를 선택합니다.

    추가 정보

코드 검사

Program.cs 파일에는 다음 코드가 포함되어 있습니다.

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/", () => "Hello World!");

app.Run();

앞의 코드가 하는 역할은 다음과 같습니다.

  • 미리 구성된 기본값을 사용하여 WebApplicationBuilderWebApplication을 만듭니다.
  • Hello World!를 반환하는 HTTP GET 엔드포인트 /를 만듭니다.

앱 실행

Ctrl+F5를 눌러 디버거 없이 실행합니다.

Visual Studio는 다음 대화 상자를 표시합니다.

이 프로젝트는 SSL을 사용하도록 구성되었습니다. 브라우저에서 SSL 경고를 피하려면 IIS Express에서 생성한 자체 서명된 인증서를 신뢰하도록 선택할 수 있습니다. IIS Express SSL 인증서를 신뢰하시겠습니까?

IIS Express SSL 인증서를 신뢰하는 경우 를 선택합니다.

다음 대화 상자가 표시됩니다.

보안 경고 대화 상자

개발 인증서를 신뢰하는 데 동의하는 경우 를 선택합니다.

Firefox 브라우저를 신뢰하는 방법에 대한 자세한 내용은 Firefox SEC_ERROR_INADEQUATE_KEY_USAGE 인증서 오류를 참조하세요.

Visual Studio에서 Kestrel 웹 서버를 시작하고 브라우저 창을 엽니다.

Hello World!가 브라우저에 표시됩니다. 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>엔드포인트 탐색기 보기를>선택합니다.

  • 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 있는 요청 보내기 링크를 선택합니다.

    실행 링크가 강조 표시된 .http 파일 창입니다.

    POST 요청이 앱으로 전송되고 응답 창에 응답표시됩니다.

    POST 요청의 응답이 포함된 .http 파일 창입니다.

GET 엔드포인트 검사

샘플 앱은 MapGet을 호출하여 여러 GET 엔드포인트를 구현합니다.

API 설명 요청 본문 응답 본문
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 요청 줄 위에 있는 요청 보내기 링크를 선택합니다.

    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입니다.

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 요청 줄에서 .로 바1{id} 다. DELETE 요청은 다음 예제와 같습니다.

    DELETE {{TodoApi_HostAddress}}/todoitems/1
    
    ###
    
  • DELETE 요청에 대한 요청 보내기 링크를 선택합니다.

    DELETE 요청이 앱으로 전송되고 응답 창에 응답표시됩니다. 응답 본문이 비어 있고 상태 코드는 204입니다.

MapGroup API 사용

샘플 앱 코드는 엔드포인트를 설정할 때마다 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.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 접두사 /todoitems을 사용하여 그룹을 설정하도록 var todoItems = app.MapGroup("/todoitems");을 추가합니다.
  • 모든 app.Map<HttpVerb> 메서드를 todoItems.Map<HttpVerb>로 변경합니다.
  • Map<HttpVerb> 메서드 호출에서 URL 접두사 /todoitems를 제거합니다.

엔드포인트를 테스트하여 동일하게 작동하는지 확인합니다.

TypedResults API 사용

TypedResults 테스트 용이성 및 엔드포인트를 설명하기 위해 OpenAPI에 대한 응답 형식 메타데이터를 자동으로 반환하는 등 여러 가지 이점이 있는 대신 Results 반환합니다. 자세한 내용은 TypedResults 및 결과를 참조하세요.

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();
}

비밀 필드를 제외한 모든 필드를 게시하고 가져올 수 있는지 확인합니다.

완료된 샘플 문제 해결

해결할 수 없는 문제가 발생한 경우 완료된 프로젝트와 코드를 비교합니다. 완료된 프로젝트를 보거나 다운로드합니다(다운로드 방법).

다음 단계

자세한 정보

최소 API 빠른 참조 참조

최소 API는 최소한의 종속성으로 HTTP API를 만들도록 설계되었습니다. ASP.NET Core에 최소 파일, 기능, 종속성만 포함하려는 앱과 마이크로 서비스에 적합합니다.

이 자습서에서는 ASP.NET Core를 사용한 최소 API를 빌드하는 기본 사항을 설명합니다. ASP.NET Core에서 API를 만드는 또 다른 방법은 컨트롤러를 사용하는 것입니다. 최소 API와 컨트롤러 기반 API 중에서 선택하는 방법에 대한 도움말은 API 개요를 참조하세요. 더 많은 기능이 포함된 컨트롤러를 기반으로 API 프로젝트를 만드는 방법에 대한 자습서는 웹 API 만들기를 참조하세요.

개요

이 자습서에서는 다음 API를 만듭니다.

API 설명 요청 본문 응답 본문
GET /todoitems 할 일 항목 모두 가져오기 None 할 일 항목의 배열
GET /todoitems/complete 완성된 할 일 항목 가져오기 None 할 일 항목의 배열
GET /todoitems/{id} ID로 항목 가져오기 None 할 일 항목
POST /todoitems 새 항목 추가 할 일 항목 할 일 항목
PUT /todoitems/{id} 기존 항목 업데이트 할 일 항목 None
DELETE /todoitems/{id}     항목 삭제 None None

필수 조건

API 프로젝트 만들기

  • Visual Studio 2022를 시작하고 새 프로젝트 만들기를 선택합니다.

  • 새 프로젝트 만들기 대화 상자에서 다음을 수행합니다.

    • 템플릿 검색 검색 상자에 Empty를 입력합니다.
    • ASP.NET Core Empty 템플릿을 선택하고 다음을 선택합니다.

    Visual Studio 새 프로젝트 만들기

  • 프로젝트 이름을 TodoApi로 지정하고 다음을 선택합니다.

  • 추가 정보 대화 상자에서 다음을 수행합니다.

    • .NET 7.0 선택
    • 최상위 문 사용 안 함 선택 취소
    • 만들기를 선택합니다.

    추가 정보

코드 검사

Program.cs 파일에는 다음 코드가 포함되어 있습니다.

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/", () => "Hello World!");

app.Run();

앞의 코드가 하는 역할은 다음과 같습니다.

  • 미리 구성된 기본값을 사용하여 WebApplicationBuilderWebApplication을 만듭니다.
  • Hello World!를 반환하는 HTTP GET 엔드포인트 /를 만듭니다.

앱 실행

Ctrl+F5를 눌러 디버거 없이 실행합니다.

Visual Studio는 다음 대화 상자를 표시합니다.

이 프로젝트는 SSL을 사용하도록 구성되었습니다. 브라우저에서 SSL 경고를 피하려면 IIS Express에서 생성한 자체 서명된 인증서를 신뢰하도록 선택할 수 있습니다. IIS Express SSL 인증서를 신뢰하시겠습니까?

IIS Express SSL 인증서를 신뢰하는 경우 를 선택합니다.

다음 대화 상자가 표시됩니다.

보안 경고 대화 상자

개발 인증서를 신뢰하는 데 동의하는 경우 를 선택합니다.

Firefox 브라우저를 신뢰하는 방법에 대한 자세한 내용은 Firefox SEC_ERROR_INADEQUATE_KEY_USAGE 인증서 오류를 참조하세요.

Visual Studio에서 Kestrel 웹 서버를 시작하고 브라우저 창을 엽니다.

Hello World!가 브라우저에 표시됩니다. 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 테스트 페이지

  • Swagger API 테스트 페이지에서 Post /todoitems>Try it out을 선택합니다.

  • 요청 본문 필드에는 API에 대한 매개 변수를 반영하는 생성된 예제 형식이 포함되어 있습니다.

  • 요청 본문에서 선택 사항을 id지정하지 않고 할 일 항목에 대한 JSON을 입력합니다.

    {
      "name":"walk dog",
      "isComplete":true
    }
    
  • 실행을 선택합니다.

    Swagger with Post

Swagger는 실행 단추 아래에 응답 창을 제공합니다.

Post 응답을 사용하여 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 설명 요청 본문 응답 본문
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를 선택하여 실행해 보세요>.>

  • 또는 URIhttp://localhost:<port>/todoitems를 입력하여 브라우저에서 GET /todoitems를 호출합니다. 예를 들어 http://localhost:5001/todoitems

GET /todoitems를 호출하면 다음과 비슷한 응답이 생성됩니다.

[
  {
    "id": 1,
    "name": "walk dog",
    "isComplete": true
  }
]
  • Swagger에서 GET /todoitems/{id}를 호출하여 특정 ID에서 데이터를 반환합니다.

    • GET /todoitems Try it out>선택합니다.
    • ID 필드를 1 설정하고 실행을 선택합니다.
  • 또는 URIhttps://localhost:<port>/todoitems/1를 입력하여 브라우저에서 GET /todoitems를 호출합니다. 예를 들어 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 요청을 보냅니다.

  • /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입니다.

MapGroup API 사용

샘플 앱 코드는 엔드포인트를 설정할 때마다 todoitems URL 접두사를 반복합니다. API에는 공통 URL 접두사를 사용하는 엔드포인트 그룹이 있는 경우가 많으며 MapGroup 메서드를 사용하여 이러한 그룹을 구성할 수 있습니다. 반복 코드를 줄이고 RequireAuthorizationWithMetadata와 같은 메서드에 대한 단일 호출로 전체 엔드포인트 그룹을 사용자 지정할 수 있습니다.

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 접두사 /todoitems을 사용하여 그룹을 설정하도록 var todoItems = app.MapGroup("/todoitems");을 추가합니다.
  • 모든 app.Map<HttpVerb> 메서드를 todoItems.Map<HttpVerb>로 변경합니다.
  • Map<HttpVerb> 메서드 호출에서 URL 접두사 /todoitems를 제거합니다.

엔드포인트를 테스트하여 동일하게 작동하는지 확인합니다.

TypedResults API 사용

TypedResults 테스트 용이성 및 엔드포인트를 설명하기 위해 OpenAPI에 대한 응답 형식 메타데이터를 자동으로 반환하는 등 여러 가지 이점이 있는 대신 Results 반환합니다. 자세한 내용은 TypedResults 및 결과를 참조하세요.

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>();
}

비밀 필드를 제외한 모든 필드를 게시하고 가져올 수 있는지 확인합니다.

완료된 샘플 문제 해결

해결할 수 없는 문제가 발생한 경우 완료된 프로젝트와 코드를 비교합니다. 완료된 프로젝트를 보거나 다운로드합니다(다운로드 방법).

다음 단계

자세한 정보

최소 API 빠른 참조 참조

최소 API는 최소한의 종속성으로 HTTP API를 만들도록 설계되었습니다. ASP.NET Core에 최소 파일, 기능, 종속성만 포함하려는 앱과 마이크로 서비스에 적합합니다.

이 자습서에서는 ASP.NET Core를 사용한 최소 API를 빌드하는 기본 사항을 설명합니다. ASP.NET Core에서 API를 만드는 또 다른 방법은 컨트롤러를 사용하는 것입니다. 최소 API와 컨트롤러 기반 API 중에서 선택하는 방법에 대한 도움말은 API 개요를 참조하세요. 더 많은 기능이 포함된 컨트롤러를 기반으로 API 프로젝트를 만드는 방법에 대한 자습서는 웹 API 만들기를 참조하세요.

개요

이 자습서에서는 다음 API를 만듭니다.

API 설명 요청 본문 응답 본문
GET /todoitems 할 일 항목 모두 가져오기 None 할 일 항목의 배열
GET /todoitems/complete 완성된 할 일 항목 가져오기 None 할 일 항목의 배열
GET /todoitems/{id} ID로 항목 가져오기 None 할 일 항목
POST /todoitems 새 항목 추가 할 일 항목 할 일 항목
PUT /todoitems/{id} 기존 항목 업데이트 할 일 항목 None
DELETE /todoitems/{id}     항목 삭제 None None

필수 조건

API 프로젝트 만들기

  • Visual Studio 2022를 시작하고 새 프로젝트 만들기를 선택합니다.

  • 새 프로젝트 만들기 대화 상자에서 다음을 수행합니다.

    • 템플릿 검색 검색 상자에 Empty를 입력합니다.
    • ASP.NET Core Empty 템플릿을 선택하고 다음을 선택합니다.

    Visual Studio 새 프로젝트 만들기

  • 프로젝트 이름을 TodoApi로 지정하고 다음을 선택합니다.

  • 추가 정보 대화 상자에서 다음을 수행합니다.

    • .NET 6.0 선택
    • 최상위 문 사용 안 함 선택 취소
    • 만들기를 선택합니다.

코드 검사

Program.cs 파일에는 다음 코드가 포함되어 있습니다.

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/", () => "Hello World!");

app.Run();

앞의 코드가 하는 역할은 다음과 같습니다.

  • 미리 구성된 기본값을 사용하여 WebApplicationBuilderWebApplication을 만듭니다.
  • Hello World!를 반환하는 HTTP GET 엔드포인트 /를 만듭니다.

앱 실행

Ctrl+F5를 눌러 디버거 없이 실행합니다.

Visual Studio는 다음 대화 상자를 표시합니다.

이 프로젝트는 SSL을 사용하도록 구성되었습니다. 브라우저에서 SSL 경고를 피하려면 IIS Express에서 생성한 자체 서명된 인증서를 신뢰하도록 선택할 수 있습니다. IIS Express SSL 인증서를 신뢰하시겠습니까?

IIS Express SSL 인증서를 신뢰하는 경우 를 선택합니다.

다음 대화 상자가 표시됩니다.

보안 경고 대화 상자

개발 인증서를 신뢰하는 데 동의하는 경우 를 선택합니다.

Firefox 브라우저를 신뢰하는 방법에 대한 자세한 내용은 Firefox SEC_ERROR_INADEQUATE_KEY_USAGE 인증서 오류를 참조하세요.

Visual Studio에서 Kestrel 웹 서버를 시작하고 브라우저 창을 엽니다.

Hello World!가 브라우저에 표시됩니다. 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 테스트 페이지

  • Swagger API 테스트 페이지에서 Post /todoitems>Try it out을 선택합니다.

  • 요청 본문 필드에는 API에 대한 매개 변수를 반영하는 생성된 예제 형식이 포함되어 있습니다.

  • 요청 본문에서 선택 사항을 id지정하지 않고 할 일 항목에 대한 JSON을 입력합니다.

    {
      "name":"walk dog",
      "isComplete":true
    }
    
  • 실행을 선택합니다.

    데이터 게시를 사용하여 Swagger

Swagger는 실행 단추 아래에 응답 창을 제공합니다.

Post 공명 창이 있는 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 설명 요청 본문 응답 본문
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를 선택하여 실행해 보세요>.>

  • 또는 URIhttp://localhost:<port>/todoitems를 입력하여 브라우저에서 GET /todoitems를 호출합니다. 예를 들어 http://localhost:5001/todoitems

GET /todoitems를 호출하면 다음과 비슷한 응답이 생성됩니다.

[
  {
    "id": 1,
    "name": "walk dog",
    "isComplete": true
  }
]
  • Swagger에서 GET /todoitems/{id}를 호출하여 특정 ID에서 데이터를 반환합니다.

    • GET /todoitems Try it out>선택합니다.
    • ID 필드를 1 설정하고 실행을 선택합니다.
  • 또는 URIhttps://localhost:<port>/todoitems/1를 입력하여 브라우저에서 GET /todoitems를 호출합니다. 예를 들면 다음과 같습니다. 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 요청을 보냅니다.

  • /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 만들기를 참조하세요.

개요

이 자습서에서는 다음 API를 만듭니다.

API 설명 요청 본문 응답 본문
GET /todoitems 할 일 항목 모두 가져오기 None 할 일 항목의 배열
GET /todoitems/complete 완성된 할 일 항목 가져오기 None 할 일 항목의 배열
GET /todoitems/{id} ID로 항목 가져오기 None 할 일 항목
POST /todoitems 새 항목 추가 할 일 항목 할 일 항목
PUT /todoitems/{id} 기존 항목 업데이트 할 일 항목 None
DELETE /todoitems/{id}     항목 삭제 None None

필수 조건

API 프로젝트 만들기

  • Visual Studio 2022를 시작하고 새 프로젝트 만들기를 선택합니다.

  • 새 프로젝트 만들기 대화 상자에서 다음을 수행합니다.

    • 템플릿 검색 검색 상자에 Empty를 입력합니다.
    • ASP.NET Core Empty 템플릿을 선택하고 다음을 선택합니다.

    Visual Studio 새 프로젝트 만들기

  • 프로젝트 이름을 TodoApi로 지정하고 다음을 선택합니다.

  • 추가 정보 대화 상자에서 다음을 수행합니다.

    • .NET 8.0(장기 지원) 선택
    • 최상위 문 사용 안 함 선택 취소
    • 만들기를 선택합니다.

    추가 정보

코드 검사

Program.cs 파일에는 다음 코드가 포함되어 있습니다.

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/", () => "Hello World!");

app.Run();

앞의 코드가 하는 역할은 다음과 같습니다.

  • 미리 구성된 기본값을 사용하여 WebApplicationBuilderWebApplication을 만듭니다.
  • Hello World!를 반환하는 HTTP GET 엔드포인트 /를 만듭니다.

앱 실행

Ctrl+F5를 눌러 디버거 없이 실행합니다.

Visual Studio는 다음 대화 상자를 표시합니다.

이 프로젝트는 SSL을 사용하도록 구성되었습니다. 브라우저에서 SSL 경고를 피하려면 IIS Express에서 생성한 자체 서명된 인증서를 신뢰하도록 선택할 수 있습니다. IIS Express SSL 인증서를 신뢰하시겠습니까?

IIS Express SSL 인증서를 신뢰하는 경우 를 선택합니다.

다음 대화 상자가 표시됩니다.

보안 경고 대화 상자

개발 인증서를 신뢰하는 데 동의하는 경우 를 선택합니다.

Firefox 브라우저를 신뢰하는 방법에 대한 자세한 내용은 Firefox SEC_ERROR_INADEQUATE_KEY_USAGE 인증서 오류를 참조하세요.

Visual Studio에서 Kestrel 웹 서버를 시작하고 브라우저 창을 엽니다.

Hello World!가 브라우저에 표시됩니다. 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>엔드포인트 탐색기 보기를>선택합니다.

  • 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 있는 요청 보내기 링크를 선택합니다.

    실행 링크가 강조 표시된 .http 파일 창입니다.

    POST 요청이 앱으로 전송되고 응답 창에 응답표시됩니다.

    POST 요청의 응답이 포함된 .http 파일 창입니다.

GET 엔드포인트 검사

샘플 앱은 MapGet을 호출하여 여러 GET 엔드포인트를 구현합니다.

API 설명 요청 본문 응답 본문
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 요청 줄 위에 있는 요청 보내기 링크를 선택합니다.

    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입니다.

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 요청 줄에서 .로 바1{id} 다. DELETE 요청은 다음 예제와 같습니다.

    DELETE {{TodoApi_HostAddress}}/todoitems/1
    
    ###
    
  • DELETE 요청에 대한 요청 보내기 링크를 선택합니다.

    DELETE 요청이 앱으로 전송되고 응답 창에 응답표시됩니다. 응답 본문이 비어 있고 상태 코드는 204입니다.

MapGroup API 사용

샘플 앱 코드는 엔드포인트를 설정할 때마다 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.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 접두사 /todoitems을 사용하여 그룹을 설정하도록 var todoItems = app.MapGroup("/todoitems");을 추가합니다.
  • 모든 app.Map<HttpVerb> 메서드를 todoItems.Map<HttpVerb>로 변경합니다.
  • Map<HttpVerb> 메서드 호출에서 URL 접두사 /todoitems를 제거합니다.

엔드포인트를 테스트하여 동일하게 작동하는지 확인합니다.

TypedResults API 사용

TypedResults 테스트 용이성 및 엔드포인트를 설명하기 위해 OpenAPI에 대한 응답 형식 메타데이터를 자동으로 반환하는 등 여러 가지 이점이 있는 대신 Results 반환합니다. 자세한 내용은 TypedResults 및 결과를 참조하세요.

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();
}

비밀 필드를 제외한 모든 필드를 게시하고 가져올 수 있는지 확인합니다.

완료된 샘플 문제 해결

해결할 수 없는 문제가 발생한 경우 완료된 프로젝트와 코드를 비교합니다. 완료된 프로젝트를 보거나 다운로드합니다(다운로드 방법).

다음 단계

자세한 정보

최소 API 빠른 참조 참조