TODO 리스트 CRUD Web API 만들기 with .NET 8 Minimal APIs

  • 41 minutes to read

이번 강의에서는 ASP.NET Core 8.0최소 API(Minimal APIs)를 사용하여 TODO 리스트 앱 작성에 필요한 Web API를 작성하는 방법에 대한 내용을 따라하기 형식으로 진행합니다.

TODO 리스트 CRUD API

이 강좌에서 구현할 Web API 목록입니다.

REST Web API 기능 요청 본문 응답 본문
GET /todos 모든 TODO List 목록 반환 없음 TODO List 컬렉션
POST /todos TODO 항목 하나 추가 TODO 항목 TODO 항목
GET /todos/{id} ID에 해당하는 TODO 항목 하나 없음 TODO 항목
PUT /todos/{id} 이미 있는 TODO 항목 수정   TODO 항목 없음
DELETE /todos/{id}     TODO 항목 삭제     없음 없음
GET /todos/complete 완료된 TODO List 목록 반환 없음 TODO List 컬렉션

TODO 리스트 Web API 만들기 프로젝트 생성

ASP.NET Core Empty 프로젝트 템플릿을 사용하여 VisualAcademy.Todos 이름으로 프로젝트를 생성합니다.

이 강좌는 다음 버전을 사용합니다.

  • Visual Studio 2022
  • .NET 8.0
  • C# 12.0

만약, .NET 7.0 프로젝트를 사용하고 있다면 .NET 8.0 프로젝트로 업그레이드한 후 이 강좌를 진행하세요.

<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
-    <TargetFramework>net7.0</TargetFramework>
+    <TargetFramework>net8.0</TargetFramework>
  </PropertyGroup>

</Project>

처음 생성된 프로젝트는 다음 그림과 같습니다.

그림: VisualAcademy.Todos 프로젝트

VisualAcademy.Todos 프로젝트

NOTE

VisualAcademy.Todos 최종 소스는 다음 경로의 Todos 프로젝트를 참고하세요.

https://github.com/VisualAcademy/VisualAcademy

솔루션 탐색기에서 Program.cs 파일을 더블 클릭하여 엽니다.

코드: VisualAcademy.Todos/Program.cs

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

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

app.Run();

Empty 프로젝트로 생성된 소스는 단 4줄로 하나의 완성된 웹 애플리케이션을 만들 수 있습니다.

Visual Studio에서 Ctrl+F5 단축키로 실행하면 웹브라우저에 Hello World!가 출력이 됩니다.

그림: 기본 코드 실행 결과

VisualAcademy.Todos 프로젝트 실행

Todo 클래스 추가

TODO 리스트를 만들기 위한 첫 번째 단계로 Program.cs 파일 제일 밑에 Todo 클래스를 추가합니다.

코드: Todo 클래스

class Todo
{
    public int Id { get; set; }
    public string? Name { get; set; }
    public bool IsComplete { get; set; } // IsDone, ...
}

참고로, 불리언 프로퍼티인 IsComplete는 제가 다른 강의에서는 IsDone으로 많이 사용했습니다.

EF Core DbContext 사용을 위한 NuGet 패키지 추가

Entity Framework Core의 DbContext 클래스를 사용하려면 프로젝트에 패키지를 추가해야 합니다.

이번 강좌에서는 SQL Server를 사용하지 않고 SQL Server InMemory 데이터베이스를 사용하기에 NuGet 갤러리에서 다음 패키지를 추가하면 됩니다.

  • Microsoft.EntityFrameworkCore.InMemory.dll

패키지 설치는 NuGet 패키지 관리자를 사용여 개별 패키지를 하나씩 설치하거나 프로젝트 파일 편집을 사용해서 여러 개의 패키지를 한 번에 설치할 수 있습니다.

NuGet 패키지 관리자를 사용할 때에는 다음 그림과 같이 패키지를 찾아서 설치하면 됩니다.

그림: NuGet 패키지 관리자

NuGet 패키지 관리자

프로젝트에 우클릭해서 프로젝트 파일 편집 메뉴를 사용해도 됩니다.

그림: 프로젝트 파일 편집

프로젝트 파일 편집

처음 시작 내용은 다음과 비슷합니다.

<Project Sdk="Microsoft.NET.Sdk.Web">
  <PropertyGroup>
    <TargetFramework>net7.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
  </PropertyGroup>
</Project>

프로젝트 파일 편집에서 <ItemGroup>을 추가하고 다음 코드를 복사해서 붙여넣기합니다.

<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <TargetFramework>net7.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="7.0.0" />
  </ItemGroup>

</Project>

NuGet 패키지 관리자에서 패키지를 추가하면 <PackageReference> 항목이 추가되는 형태입니다.

프로젝트 파일이 저장되는 순간에 NuGet 패키지 설치가 바로 진행됩니다.

EF Core DbContext 클래스인 TodoDb 클래스 추가

Program.cs 파일의 제일 밑에 다음 코드와 같이 TodoDb 클래스를 추가합니다.

코드: TodoDb 클래스

class TodoDb : DbContext
{
    public TodoDb(DbContextOptions<TodoDb> options)
        : base(options) { }

    public DbSet<Todo> Todos { get; set; }
}

Todos 컬렉션 속성은 다음과 같이 속성값을 지정해도 됩니다.

public DbSet<Todo> Todos => Set<Todo>();

현재까지 작성한 Program.cs 파일의 전체 코드는 다음과 같습니다.

코드: VisualAcademy.Todos/Program.cs

using Microsoft.EntityFrameworkCore;

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

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

app.Run();

class Todo
{
    public int Id { get; set; }
    public string? Name { get; set; }
    public bool IsComplete { get; set; }
}

class TodoDb : DbContext
{
    public TodoDb(DbContextOptions<TodoDb> options)
        : base(options) { }

    public DbSet<Todo> Todos => Set<Todo>();
}

TodoDb Service 등록

TodoDb 클래스를 현재 애플리케이션에 주입해서 사용하려면 DbContext를 서비스로 등록하는 절차가 필요합니다.

Program.cs 파일의 서비스 등록 영역에 다음 코드를 추가해야합니다.

builder.Services.AddDbContext<TodoDb>(
    opt => opt.UseInMemoryDatabase("Todos"));

만약, SQLite 데이터베이스를 사용하는 경우라면 다음과 같은 코드를 사용합니다.

builder.Services.AddDbContext<TodoDb>(
    opt => opt.UseSqlite("Data Source=Todos.db"));

Program.cs 파일에 다음 코드를 추가합니다. Minimal APIs에 의존성 주입(Dependency Injection, DI)을 설정하는 교과서다운 코드입니다.

코드: VisualAcademy.Todos/Program.cs

using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDbContext<TodoDb>(
    opt => opt.UseInMemoryDatabase("Todos"));

var app = builder.Build();

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

app.Run();

class Todo
{
    public int Id { get; set; }
    public string? Name { get; set; }
    public bool IsComplete { get; set; }
}

class TodoDb : DbContext
{
    public TodoDb(DbContextOptions<TodoDb> options)
        : base(options) { }

    public DbSet<Todo> Todos => Set<Todo>();
}

반드시 builder.Build() 메서드 호출 전에 서비스가 등록되어야 합니다.

의존성 주입 관련 다음 링크의 내용을 읽어보세요.

Inversion of Control (IoC)

GET todos

첫 번째 Web API를 만들어 보겠습니다. GET 방식으로 /todos 경로를 요청하면 Todos 컬렉션(테이블)에 들어있는 모든 데이터를 출력합니다.

app.MapGet("/todos", (TodoDb db) => db.Todos.ToList());

Minimal APIs의 신선한 점은 이 한 줄로 모든 데이터를 출력할 수 있습니다.

위 코드는 다음 코드와 완전 같은 코드이겠죠?

app.MapGet("/todos", (TodoDb db) => 
{
    return db.Todos.ToList();
});

MapGet() 메서드가 포함된 Program.cs 파일의 전체 내용입니다.

코드: VisualAcademy.Todos/Program.cs

using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDbContext<TodoDb>(
    opt => opt.UseInMemoryDatabase("Todos"));

var app = builder.Build();

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

app.MapGet("/todos", (TodoDb db) => db.Todos.ToList());

app.Run();

class Todo
{
    public int Id { get; set; }
    public string? Name { get; set; }
    public bool IsComplete { get; set; }
}

class TodoDb : DbContext
{
    public TodoDb(DbContextOptions<TodoDb> options)
        : base(options) { }

    public DbSet<Todo> Todos => Set<Todo>();
}

Visual Studio에서 Ctrl+F5 단축키로 실행하고 /todos 경로를 요청하면 웹브라우저에 빈 JSON 배열인 []가 출력이 됩니다.

그림: 기본 코드 실행 결과

VisualAcademy.Todos 프로젝트 실행

HttpGet 메서드인 MapGet("/todos") 메서드를 동기 방식에서 asyncawait 키워드를 추가하고 ToListAsync()로 변경하여 비동기 방식으로 변경하겠습니다.

app.MapGet("/todos", async (TodoDb db) => 
    await db.Todos.ToListAsync());

최종 변경 완료된 전체 소스 코드입니다.

코드: VisualAcademy.Todos/Program.cs

using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDbContext<TodoDb>(
    opt => opt.UseInMemoryDatabase("Todos"));

var app = builder.Build();

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

app.MapGet("/todos", async (TodoDb db) => 
    await db.Todos.ToListAsync());

app.Run();

class Todo
{
    public int Id { get; set; }
    public string? Name { get; set; }
    public bool IsComplete { get; set; }
}

class TodoDb : DbContext
{
    public TodoDb(DbContextOptions<TodoDb> options)
        : base(options) { }

    public DbSet<Todo> Todos => Set<Todo>();
}

Web API의 결괏값은 Results.Ok() 형태의 반환값 형식을 지정하는 메서드로 묶어서 반환할 수 있습니다.

Minimal APIs의 라우팅(Routing) 기능

ASP.NET Core의 Minimal APIs는 경량화된, 작고 효율적인 API 엔드포인트를 생성하는 방법을 제공합니다. 이들의 라우팅 기능은 다음과 같은 주요 특징을 가집니다:

간결한 라우팅 구문

Minimal APIs는 라우팅을 설정할 때 간결하고 직관적인 구문을 사용합니다. 예를 들어, app.MapGet("/items", () => ...)는 GET 요청을 "/items" 경로에 매핑합니다.

HTTP 메서드 지정

라우팅 구문에서는 HTTP 메서드(GET, POST, PUT, DELETE 등)를 명시적으로 지정합니다. 이는 각 API 엔드포인트의 의도와 동작을 명확하게 합니다.

람다 표현식 지원

엔드포인트 로직은 직접적으로 람다 표현식을 통해 정의될 수 있습니다. 이는 별도의 컨트롤러 클래스 없이도 라우트 로직을 간단하게 구현할 수 있게 해줍니다.

파라미터 바인딩

라우트 핸들러는 쿼리 문자열, 경로 변수, 바디 컨텐츠 등에서 파라미터를 자동으로 바인딩할 수 있습니다. 이는 API 개발을 더욱 효율적으로 만듭니다.

미들웨어(Middleware) 통합

Minimal APIs는 ASP.NET Core의 기존 미들웨어 인프라와 잘 통합됩니다. 따라서 인증, 권한 부여, 로깅 등의 기능을 쉽게 추가할 수 있습니다.

OpenAPI(Swagger) 지원

Minimal APIs는 OpenAPI 사양을 자동으로 지원하여 API 문서화 및 테스트를 간편하게 할 수 있습니다.

성능

전통적인 MVC 패턴에 비해 더 가벼운 구조를 가지고 있어, 더 빠른 성능을 제공합니다. 이는 특히 단순한 서비스나 마이크로서비스 아키텍처에 적합합니다.

확장성과 유연성

필요에 따라 더 복잡한 라우팅 로직이나 추가 기능을 구현할 수 있는 충분한 유연성을 제공합니다.

Minimal APIs는 API 개발을 간소화하고 빠르게 만들면서도, ASP.NET Core의 강력한 기능과 성능을 그대로 유지하는 방법을 제공합니다.

POST todos

이번에는 TODO 데이터를 저장하는 POST 메서드를 작성하겠습니다.

Post 메서드는 MapPost() 메서드를 사용하여 다음과 같이 작성합니다.

app.MapPost("/todos", (Todo todo, TodoDb db) => 
{ 
    db.Todos.Add(todo);
    db.SaveChanges();
    return TypedResults.Ok();
});

Post 메서드도 Get 메서드와 마찬가지로 같은 URI를 사용해서 구현합니다. 첫 번째 매개 변수로 Todo 개체를 요청 본문으로 받습니다. MapPost 메서드의 반환값은 지금은 TypedResults.Ok() 메서드를 사용하여 200 OK 상태 메시지를 반환하도록 합니다. 잠시 후에 이 내용은 다른 내용으로 변경됩니다.

MapPost() 메서드의 최종 코드도 비동기 방식으로 변경해서 사용하면 됩니다.

현재까지 완성된 코드는 다음과 같습니다.

코드: VisualAcademy.Todos/Program.cs

using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDbContext<TodoDb>(
    opt => opt.UseInMemoryDatabase("Todos"));

var app = builder.Build();

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

app.MapGet("/todos", async (TodoDb db) => 
    await db.Todos.ToListAsync());

app.MapPost("/todos", async (Todo todo, TodoDb db) => 
{ 
    db.Todos.Add(todo);
    await db.SaveChangesAsync();
    return TypedResults.Ok();
});

app.Run();

class Todo
{
    public int Id { get; set; }
    public string? Name { get; set; }
    public bool IsComplete { get; set; }
}

class TodoDb : DbContext
{
    public TodoDb(DbContextOptions<TodoDb> options)
        : base(options) { }

    public DbSet<Todo> Todos => Set<Todo>();
}

HttPost 메서드인 MapPost는 기본 웹브라우저로 테스트가 불가능합니다. 그래서 잠시후에 Swagger UI를 사용해서 테스트할건데요. 그 전에 HTTP 테스트 도구 중 하나인 Postman을 사용해서 POST를 테스트하는 내용을 데모로 보여드립니다.

다음 그림은 POST 방식으로 /todos 경로에 JSON 데이터를 전달하면 정상적으로 저장되고 200 OK 상태 메시지가 출력되는 내용입니다. HTTP 테스트 도구인 Postman에 대한 사용 경험이 있다면 다음 그림을 바탕으로 직접 테스트해보세요.

그림: HTTP 테스트 도구인 Postman으로 Todo 데이터를 JSON으로 전달

HTTP 테스트 도구인 Postman으로 Todo 데이터를 JSON으로 전달

HttpPost 메서드의 가장 좋은 반환값은 200 OK 대신에 201 Created를 상태 메시지로 반환해야 합니다.

Program.cs 파일에 단일 데이터를 출력해주는 GetById 형태의 HttpGet 메서드를 하나 더 추가합니다. {id} 형태로 라우트 파라미터를 받을 수 있습니다.

app.MapGet("/todos/{id}", async Task<Results<Ok<Todo>, NotFound>> (int id, TodoDb db) => 
    await db.Todos.FindAsync(id)
        is Todo todo 
            ? TypedResults.Ok(todo)
            : TypedResults.NotFound());

Post 메서드의 결괏값으로는 201 Created가 가장 좋은데요. 닷넷 7 이후로는 TypedResults.Created() 메서드를 제공해서 결과를 201 응답코드로 반환합니다.

return TypedResults.Created($"/todos/{todo.Id}", todo);

현재까지 작성된 전체 소스 코드입니다.

코드: VisualAcademy.Todos/Program.cs

using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDbContext<TodoDb>(
    opt => opt.UseInMemoryDatabase("Todos"));

var app = builder.Build();

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

app.MapGet("/todos", async (TodoDb db) => 
    await db.Todos.ToListAsync());

app.MapPost("/todos", async (Todo todo, TodoDb db) => 
{ 
    db.Todos.Add(todo);
    await db.SaveChangesAsync();
    return TypedResults.Created($"/todos/{todo.Id}", todo);
});

app.MapGet("/todos/{id}", async Task<Results<Ok<Todo>, NotFound>> (int id, TodoDb db) => 
    await db.Todos.FindAsync(id)
        is Todo todo 
            ? TypedResults.Ok(todo)
            : TypedResults.NotFound());

app.Run();

class Todo
{
    public int Id { get; set; }
    public string? Name { get; set; }
    public bool IsComplete { get; set; }
}

class TodoDb : DbContext
{
    public TodoDb(DbContextOptions<TodoDb> options)
        : base(options) { }

    public DbSet<Todo> Todos => Set<Todo>();
}

HttpPost 메서드의 반환값을 변경 후 다시 POST 메서드를 테스트해보면 다음 그림과 같이 201 Created 값이 반환됩니다. Postman이 아닌 웹브라우저에서 우리가 직접 테스트하는 것은 잠시 후에 진행하겠습니다.

그림: 데이터 저장 후 201 Created 상태 메시지 출력

데이터 저장 후 201 Created 상태 메시지 출력

파라미터 바인딩 정리

이번에는 ASP.NET Core의 Minimal APIs에서 파라미터 바인딩을 사용하는 방법에 대해 설명합니다. 파라미터 바인딩은 클라이언트로부터 전달받은 데이터를 API의 매개변수에 자동으로 할당하는 과정을 의미합니다.

파라미터 바인딩의 종류

ASP.NET Core Minimal APIs에서는 다양한 형태의 파라미터 바인딩을 지원합니다:

  1. 쿼리 문자열 바인딩: URL의 쿼리 문자열을 메서드의 매개변수로 바인딩합니다.
  2. 경로 변수 바인딩: URL 경로의 일부분을 매개변수로 바인딩합니다.
  3. 요청 본문 바인딩: HTTP 요청 본문의 데이터를 매개변수로 바인딩합니다.

파라미터 바인딩의 구현

쿼리 문자열 바인딩

app.MapGet("/search", (string query) => $"검색어: {query}");

이 코드는 쿼리 문자열 query를 매개변수로 바인딩합니다.

경로 변수 바인딩

app.MapGet("/items/{id}", (int id) => $"아이템 ID: {id}");

이 코드는 URL 경로의 {id} 부분을 int 타입의 id 매개변수로 바인딩합니다.

요청 본문 바인딩

app.MapPost("/items", (Item item) => {
    // Item 객체 처리
});

이 코드는 HTTP 요청 본문의 JSON 데이터를 Item 클래스의 인스턴스로 바인딩합니다.

고급 바인딩 기능

  • 맞춤형 바인딩: 사용자 정의 바인딩 로직을 구현하여 특정 형식의 데이터 처리 방식을 정의할 수 있습니다.
  • 모델 유효성 검사: 데이터 모델의 유효성을 검증하여 API의 안정성을 향상시킬 수 있습니다.

결론

ASP.NET Core의 Minimal API에서 파라미터 바인딩은 API 개발의 효율성과 유연성을 크게 증가시킵니다. 적절한 파라미터 바인딩 전략을 사용하면, 개발자는 클라이언트로부터 데이터를 효율적으로 수집하고 처리할 수 있습니다.

MapGroup 메서드로 API 그룹화

Minimal APIs에서 반복되는 URL은 그룹으로 묶어서 관리할 수 있습니다. 닷넷 7에서 새롭게 도입된 기능인데요. 엔드포인트를 그룹화할 수 있도록 MapGroup() 확장 메서드를 제공합니다.

MapGroup() 메서드에 URL 접두사를 지정하고 같은 API들을 묶어주면 됩니다.

var todos = app.MapGroup("/todos");

다음 코드의 apptodos로 변경합니다.

app.MapGet("/todos", async (TodoDb db) => 
    await db.Todos.ToListAsync());

각각의 Web API는 모두 그룹화된 todos로 변경하고 URL도 /todos에서 /로 변경합니다.

todos.MapGet("/", async (TodoDb db) => 
    await db.Todos.ToListAsync());

전체 변경된 내용은 다음 코드를 참고하세요.

코드: VisualAcademy.Todos/Program.cs

using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDbContext<TodoDb>(
    opt => opt.UseInMemoryDatabase("Todos"));

var app = builder.Build();

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

var todos = app.MapGroup("/todos");

todos.MapGet("/",
    async (TodoDb db) =>
    await db.Todos.ToListAsync());

todos.MapPost("/",
    async (Todo todo, TodoDb db) =>
    {
        db.Todos.Add(todo);
        await db.SaveChangesAsync();
        return TypedResults.Created($"/todos/{todo.Id}", todo);
    });

todos.MapGet("/{id}",
    async Task<Results<Ok<Todo>, NotFound>> (int id, TodoDb db) =>
    await db.Todos.FindAsync(id)
        is Todo todo
            ? TypedResults.Ok(todo)
            : TypedResults.NotFound());

app.Run();

class Todo
{
    public int Id { get; set; }
    public string? Name { get; set; }
    public bool IsComplete { get; set; }
}

class TodoDb : DbContext
{
    public TodoDb(DbContextOptions<TodoDb> options)
        : base(options) { }

    public DbSet<Todo> Todos => Set<Todo>();
}

Open API와 Swagger

프로젝트에 웹 버전으로 REST API를 테스트하기 위한 Swagger UI를 설치하겠습니다.

Swagger는 API 문서화를 위한 프레임워크입니다. Swagger를 사용하면 프로젝트의 모든 API에 대한 기본 제공 문서 UI를 추가할 수 있습니다. Postman 도구를 사용하지 않고도 기본적인 Web API에 대한 CRUD를 웹 기반 UI를 사용하여 테스트할 수 있습니다.

  • OpenAPI - REST API에 대한 스펙
  • Swagger - OpenAPI에 대한 실제 구현체

Swagger UI

Swagger 사용을 위한 필수 패키지

Swagger UI를 사용하기 위한 필수 NuGet 패키지는 다음 2개입니다.

  • Microsoft.AspNetCore.OpenApi.dll
  • Swashbuckle.AspNetCore.dll

현재 강의 시점의 사용 버전은 다음과 같습니다. NuGet 패키지 관리자 또는 프로젝트 파일 편집 메뉴를 통해서 2개의 패키지를 추가합니다.

<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <TargetFramework>net7.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="7.0.0" />
    <PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" />
  </ItemGroup>

</Project>

현재까지 추가된 패키지 리스트는 프로젝트 파일 편집 메뉴를 사용해서 csproj 파일에서 볼 수 있습니다.

<Project Sdk="Microsoft.NET.Sdk.Web">

    <PropertyGroup>
        <TargetFramework>net7.0</TargetFramework>
        <Nullable>enable</Nullable>
        <ImplicitUsings>enable</ImplicitUsings>
    </PropertyGroup>

    <ItemGroup>
        <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="7.0.0" />
        <PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="7.0.0" />
        <PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" />
    </ItemGroup>

</Project>

Swagger services

서비스 등록 영역에 다음 두 줄을 추가합니다.

// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

Swagger endpoints

Swagger 엔드포인트를 사용하려면 미들웨어 등록 영역에 다음 두 개의 메서드를 호출합니다.

if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

Swagger 서비스와 엔드포인트가 등록된 전체 변경된 내용은 다음 코드를 참고하세요.

코드: VisualAcademy.Todos/Program.cs

using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDbContext<TodoDb>(
    opt => opt.UseInMemoryDatabase("Todos"));

builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

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

var todos = app.MapGroup("/todos");

todos.MapGet("/",
    async (TodoDb db) =>
    await db.Todos.ToListAsync());

todos.MapPost("/",
    async (Todo todo, TodoDb db) =>
    {
        db.Todos.Add(todo);
        await db.SaveChangesAsync();
        return TypedResults.Created($"/todos/{todo.Id}", todo);
    });

todos.MapGet("/{id}",
    async Task<Results<Ok<Todo>, NotFound>> (int id, TodoDb db) =>
    await db.Todos.FindAsync(id)
        is Todo todo
            ? TypedResults.Ok(todo)
            : TypedResults.NotFound());

app.Run();

class Todo
{
    public int Id { get; set; }
    public string? Name { get; set; }
    public bool IsComplete { get; set; }
}

class TodoDb : DbContext
{
    public TodoDb(DbContextOptions<TodoDb> options)
        : base(options) { }

    public DbSet<Todo> Todos => Set<Todo>();
}

Swagger UI를 시작 페이지로 설정

Swagger UI는 /swagger 경로에서 볼 수 있습니다. 프로젝트 실행할 때 바로 해당 경로로 이동하도록 launchUrl 속성을 추가하도록 하겠습니다.

프로젝트의 Properties 폴더에 있는 launchSettings.json 파일을 열고 다음 항목을 추가합니다.

"launchUrl": "swagger"

3군데 추가한 내용은 다음과 같습니다.

코드: Properties/launchSettings.json

{
    "iisSettings": {
        "windowsAuthentication": false,
        "anonymousAuthentication": true,
        "iisExpress": {
            "applicationUrl": "http://localhost:37345",
            "sslPort": 44323
        }
    },
    "profiles": {
        "http": {
            "commandName": "Project",
            "dotnetRunMessages": true,
            "launchBrowser": true,
            "launchUrl": "swagger",
            "applicationUrl": "http://localhost:5213",
            "environmentVariables": {
                "ASPNETCORE_ENVIRONMENT": "Development"
            }
        },
        "https": {
            "commandName": "Project",
            "dotnetRunMessages": true,
            "launchBrowser": true,
            "launchUrl": "swagger",
            "applicationUrl": "https://localhost:7071;http://localhost:5213",
            "environmentVariables": {
                "ASPNETCORE_ENVIRONMENT": "Development"
            }
        },
        "IIS Express": {
            "commandName": "IISExpress",
            "launchBrowser": true,
            "launchUrl": "swagger",
            "environmentVariables": {
                "ASPNETCORE_ENVIRONMENT": "Development"
            }
        }
    }
}

Swagger UI 테스트

프로젝트를 실행하면 swagger 경로에서 Swagger UI를 볼 수 있습니다.

Swagger UI 실행

Post 메뉴를 확장합니다. Try it out 버튼을 클릭합니다.

Swagger POST 실행

Post 메뉴에서 다음 형태의 JSON을 입력 후 실행 버튼을 클릭합니다.

{
    "id": 0,
    "name": "VisualAcademy Todo List",
    "isComplete": true
}

Swagger POST JSON

Post를 실행하면 201 Created 메시지가 출력됩니다.

Swagger POST 실행 확인

Get 메뉴를 실행합니다.

Swagger GET 실행

Execute 버튼을 클릭합니다.

Swagger GET 실행 시도

전체 데이터가 JSON으로 반환되고 200 OK 상태 메시지가 출력됩니다.

Swagger GET 실행 결과

WithOpenApi 확장 메서드

참고로, WithOpenApi() 메서드를 사용하면 Swagger UI로 출력되는 항목을 설정할 수 있습니다. 이 메서드에 대한 내용은 Microsoft Learn을 참고하세요.

opr.OperationId = "";
opr.Description = "";
opr.Summary = "";
opr.Tags = new List<OpenApiTag>() { new OpenApiTag() { Name = "" }}; 

Todo List CRUD 전체 코드 추가

수정, 삭제 및 완료 확인에 대한 API를 추가합니다. 처음 시작 API는 다음과 같이 작성합니다.

먼저 MapPut() 메서드를 다음과 같이 작성합니다.

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

마찬가지로 MapDelete() 메서드를 다음과 같이 작성합니다.

todos.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.Ok(todo); 
    }

    return Results.NotFound(); 
});

마지막으로 완료된 항목만 조회하는 메서드를 추가합니다.

todos.MapGet("/complete", async (TodoDb db) =>
    await db.Todos.Where(t => t.IsComplete).ToListAsync());

Task<Results<T, T>> 사용

Put 메서드와 Delete 메서드의 반환값을 Task<Results<T, T>> 형태로 변경하고 메서드 반환값도 Results에서 TypedResults로 변경합니다.

todos.MapPut("/{id}", async Task<Results<NotFound, NoContent>> (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();
});

todos.MapDelete("/{id}", async Task<Results<Ok<Todo>, NotFound>> (int id, TodoDb db) =>
{
    if (await db.Todos.FindAsync(id) is Todo todo)
    {
        db.Todos.Remove(todo);
        await db.SaveChangesAsync();
        return TypedResults.Ok(todo);
    }

    return TypedResults.NotFound();
});

Task<IResult> 사용

최종적으로 Task<Results<T, T>>Task<IResult>로 줄여서 표현하겠습니다.

todos.MapPut("/{id}", async Task<IResult> (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();
});

todos.MapDelete("/{id}", async Task<IResult> (int id, TodoDb db) => 
{
    if (await db.Todos.FindAsync(id) is Todo todo)
    {
        db.Todos.Remove(todo);
        await db.SaveChangesAsync();
        return TypedResults.Ok(todo); 
    }

    return TypedResults.NotFound(); 
});

CRUD가 전체 완성된 내용은 다음 코드를 참고하세요.

코드: VisualAcademy.Todos/Program.cs

using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDbContext<TodoDb>(
    opt => opt.UseInMemoryDatabase("Todos"));

builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

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

var todos = app.MapGroup("/todos");

todos.MapGet("/",
    async (TodoDb db) =>
    await db.Todos.ToListAsync());

todos.MapPost("/",
    async (Todo todo, TodoDb db) =>
    {
        db.Todos.Add(todo);
        await db.SaveChangesAsync();
        return TypedResults.Created($"/todos/{todo.Id}", todo);
    });

todos.MapGet("/{id}",
    async Task<Results<Ok<Todo>, NotFound>> (int id, TodoDb db) =>
    await db.Todos.FindAsync(id)
        is Todo todo
            ? TypedResults.Ok(todo)
            : TypedResults.NotFound());

todos.MapPut("/{id}", async Task<IResult> (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();
});

todos.MapDelete("/{id}", async Task<IResult> (int id, TodoDb db) => 
{
    if (await db.Todos.FindAsync(id) is Todo todo)
    {
        db.Todos.Remove(todo);
        await db.SaveChangesAsync();
        return TypedResults.Ok(todo); 
    }

    return TypedResults.NotFound(); 
});

todos.MapGet("/complete", async (TodoDb db) => 
    await db.Todos.Where(t => t.IsComplete).ToListAsync());

app.Run();

class Todo
{
    public int Id { get; set; }
    public string? Name { get; set; }
    public bool IsComplete { get; set; }
}

class TodoDb : DbContext
{
    public TodoDb(DbContextOptions<TodoDb> options)
        : base(options) { }

    public DbSet<Todo> Todos => Set<Todo>();
}

Web API에 JWT 인증 기능 추가

Minimal APIs에 인증 기능을 추가합니다.

JwtBearer 패키지 추가

먼저, JwtBearer 패키지를 추가합니다. 다음 패키지를 NuGet 패키지 관리자, 프로젝트 파일 편집 메뉴, NuGet 패키지 관리자 콘솔 등을 사용하여 추가합니다.

  • Microsoft.AspNetCore.Authentication.JwtBearer

패키지 관리자 콘솔에서 Install-Package 명령을 사용해도 됩니다.

PM> Install-Package Microsoft.AspNetCore.Authentication.Jwtbearer

패키지 관리자 콘솔을 사용하면 다음 형태로 NuGet 패키지가 설치됩니다.

PM> Install-Package Microsoft.AspNetCore.Authentication.Jwtbearer
C:\VisualAcademy\App\src\VisualAcademy\VisualAcademy.Todos\VisualAcademy.Todos.csproj의 패키지를 복원하는 중...
NuGet 패키지 Microsoft.AspNetCore.Authentication.Jwtbearer 7.0.0을(를) 설치하고 있습니다.
자산 파일을 디스크에 쓰는 중입니다. 경로: C:\VisualAcademy\App\src\VisualAcademy\VisualAcademy.Todos\obj\project.assets.json
C:\VisualAcademy\App\src\VisualAcademy\VisualAcademy.Todos\VisualAcademy.Todos.csproj을(를) 273밀리초 동안 복원했습니다.
VisualAcademy.Todos에 'Microsoft.AspNetCore.Authentication.JwtBearer 7.0.0'을(를) 설치했습니다.
VisualAcademy.Todos에 'Microsoft.CSharp 4.5.0'을(를) 설치했습니다.
VisualAcademy.Todos에 'Microsoft.IdentityModel.JsonWebTokens 6.15.1'을(를) 설치했습니다.
VisualAcademy.Todos에 'Microsoft.IdentityModel.Logging 6.15.1'을(를) 설치했습니다.
VisualAcademy.Todos에 'Microsoft.IdentityModel.Protocols 6.15.1'을(를) 설치했습니다.
VisualAcademy.Todos에 'Microsoft.IdentityModel.Protocols.OpenIdConnect 6.15.1'을(를) 설치했습니다.
VisualAcademy.Todos에 'Microsoft.IdentityModel.Tokens 6.15.1'을(를) 설치했습니다.
VisualAcademy.Todos에 'System.IdentityModel.Tokens.Jwt 6.15.1'을(를) 설치했습니다.
VisualAcademy.Todos에 'System.Security.Cryptography.Cng 4.5.0'을(를) 설치했습니다.
Nuget 작업 실행 시간: 281밀리초
경과 시간: 00:00:05.6640110
PM> 

패키지 설치 후 프로젝트 파일 편집 메뉴로 들어가면 다음과 같습니다.

<Project Sdk="Microsoft.NET.Sdk.Web">

    <PropertyGroup>
        <TargetFramework>net7.0</TargetFramework>
        <Nullable>enable</Nullable>
        <ImplicitUsings>enable</ImplicitUsings>
    </PropertyGroup>

    <ItemGroup>
        <PackageReference Include="Microsoft.AspNetCore.Authentication.Jwtbearer" Version="7.0.0" />
        <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="7.0.0" />
        <PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="7.0.0" />
        <PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" />
    </ItemGroup>

</Project>

Program.cs 파일의 서비스 등록 영역에 다음 두 줄을 추가합니다.

builder.Services.AddAuthentication().AddJwtBearer();
builder.Services.AddAuthorization();

엔드포인트 그룹에 추가적으로 RequireAuthorization() 확장 메서드를 호출합니다.

var todos = app.MapGroup("/todos").RequireAuthorization();

이렇게 세단계를 거치면 TODO 관련 모든 API는 인증된 사용자만 접근을 할 수 있습니다.

변경된 전체 코드는 다음과 같습니다.

코드: VisualAcademy.Todos/Program.cs

using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDbContext<TodoDb>(
    opt => opt.UseInMemoryDatabase("Todos"));

builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.AddAuthentication().AddJwtBearer();
builder.Services.AddAuthorization(); 

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

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

var todos = app.MapGroup("/todos").RequireAuthorization();

todos.MapGet("/",
    async (TodoDb db) =>
    await db.Todos.ToListAsync());

todos.MapPost("/",
    async (Todo todo, TodoDb db) =>
    {
        db.Todos.Add(todo);
        await db.SaveChangesAsync();
        return TypedResults.Created($"/todos/{todo.Id}", todo);
    });

todos.MapGet("/{id}",
    async Task<Results<Ok<Todo>, NotFound>> (int id, TodoDb db) =>
    await db.Todos.FindAsync(id)
        is Todo todo
            ? TypedResults.Ok(todo)
            : TypedResults.NotFound());

todos.MapPut("/{id}", async Task<IResult> (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();
});

todos.MapDelete("/{id}", async Task<IResult> (int id, TodoDb db) => 
{
    if (await db.Todos.FindAsync(id) is Todo todo)
    {
        db.Todos.Remove(todo);
        await db.SaveChangesAsync();
        return TypedResults.Ok(todo); 
    }

    return TypedResults.NotFound(); 
});

todos.MapGet("/complete", async (TodoDb db) => 
    await db.Todos.Where(t => t.IsComplete).ToListAsync());

app.Run();

class Todo
{
    public int Id { get; set; }
    public string? Name { get; set; }
    public bool IsComplete { get; set; }
}

class TodoDb : DbContext
{
    public TodoDb(DbContextOptions<TodoDb> options)
        : base(options) { }

    public DbSet<Todo> Todos => Set<Todo>();
}

만약, 인증되지 않은 상태로 API에 접근을 시도하면 401 에러 메시지가 상태값으로 반환됩니다. Swagger UI를 사용하여 GET, POST 등을 테스트해보세요.

401 Unauthorized 에러 메시지

JWT

JSON Web Token(JWT)은 웹 표준(RFC 7519)으로, 두 당사자 사이에서 정보를 안전하게 전송하기 위한 컴팩트하고 독립적인 방법을 제공합니다. JWT는 디지털 서명이 가능하므로, 정보가 검증되고 신뢰할 수 있는지 확인할 수 있으며, 송신자가 정말로 클레임한 대로 해당 정보를 보낸 것인지 검증할 수 있습니다. 이 토큰은 자체적으로 필요한 모든 정보를 포함하고 있어, 중앙 인증 서버를 통하지 않고도 통신하는 양 당사자 간에 인증과 정보 교환을 진행할 수 있습니다. JWT는 주로 인증 및 정보 교환의 목적으로 사용되며, 특히 단일 로그인(SSO)과 같은 시나리오에서 널리 채택되고 있습니다.

dotnet user-jwts create 명령

JWT 인증을 사용하기 위해서는 토큰 발행 절차가 필요한데요. 현재는 회원가입, 로그인, 로그아웃 등의 기능이 없는 상태이기에 개발 시점에 테스트 용도로 인증 토큰을 발행할 수 있습니다.

프로젝트에서 테스트 용도로 JWT 토큰을 생성할 때에는 Visual Studio Developer PowerShell에서 다음 명령을 실행합니다.

dotnet user-jwts create

프로젝트에 우클릭해서 터미널에서 열기 메뉴를 선택하면 개발자용 터미널이 실행됩니다.

터미널에서 열기

터미널에서 명령을 실행합니다.

user-jwts 명령 실행

인증 토큰은 복사 메뉴를 사용해서 클립보드로 복사를 합니다.

dotnet user-jwts create 명령을 실행하면 appsettings.Development.json 파일에 다음 설정 파일이 자동으로 등록됩니다.

"Authentication": {
    "Schemes": {
        "Bearer": {
            "ValidAudiences": [
                "http://localhost:37345",
                "https://localhost:44323",
                "http://localhost:5213",
                "https://localhost:7071"
            ],
            "ValidIssuer": "dotnet-user-jwts"
        }
    }
}

솔루션 탐색기에서 appsettings.Development.json 파일을 열어보면 다음과 같습니다. 포트 번호는 사용자 환경마다 다릅니다.

코드: appsettings.Development.json

{
    "Logging": {
        "LogLevel": {
            "Default": "Information",
            "Microsoft.AspNetCore": "Warning"
        }
    },
    "Authentication": {
        "Schemes": {
            "Bearer": {
                "ValidAudiences": [
                    "http://localhost:37345",
                    "http://localhost:5213",
                    "https://localhost:7071",
                    "http://localhost:5213"
                ],
                "ValidIssuer": "dotnet-user-jwts"
            }
        }
    }
}

Postman 사용 인증 통과 확인

우선 Postman을 사용하여 앞서 복사한 JWT 인증 토큰을 입력하고, JWT 토큰을 사용하여 인증이 통과되는 걸 확인하겠습니다. 잠시 후에 Swagger UI에서도 테스트할 수 있도록 변경하겠습니다.

JWT 토큰 테스트

Swagger UI에 인증 표시

Swagger UI에 인증 관련 기능을 표시하려면 AddSwaggerGen() 메서드를 다음과 같이 수정하고 Configure() 메서드를 하나 더 추가합니다.

builder.Services.AddSwaggerGen(options => 
{
    options.InferSecuritySchemes();
    options.AddSecurityRequirement(new Microsoft.OpenApi.Models.OpenApiSecurityRequirement 
    {
        { 
            new OpenApiSecurityScheme
            {
                Reference = new OpenApiReference 
                { 
                    Type = ReferenceType.SecurityScheme, Id = "Bearer"  
                }
            },
            new string[] { }
        }
    });
});
builder.Services.Configure<SwaggerGeneratorOptions>(options =>
{
    options.InferSecuritySchemes = true;
});

인증 관련해서 Swagger UI에 대한 사용자 지정된 모양으로 변경하는 코드까지 적용된 내용은 다음 코드를 참고하세요.

코드: VisualAcademy.Todos/Program.cs

using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.EntityFrameworkCore;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDbContext<TodoDb>(
    opt => opt.UseInMemoryDatabase("Todos"));

builder.Services.AddEndpointsApiExplorer();

builder.Services.AddSwaggerGen(options =>
{
    options.InferSecuritySchemes();
    options.AddSecurityRequirement(new OpenApiSecurityRequirement
    {
        {
            new OpenApiSecurityScheme
            {
                Reference = new OpenApiReference
                {
                    Type = ReferenceType.SecurityScheme, Id = "Bearer"
                }
            },
            new string[] { }
        }
    });
});
builder.Services.Configure<SwaggerGeneratorOptions>(options =>
{
    options.InferSecuritySchemes = true;
});

builder.Services.AddAuthentication().AddJwtBearer();
builder.Services.AddAuthorization(); 

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

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

var todos = app.MapGroup("/todos").RequireAuthorization();

todos.MapGet("/",
    async (TodoDb db) =>
    await db.Todos.ToListAsync());

todos.MapPost("/",
    async (Todo todo, TodoDb db) =>
    {
        db.Todos.Add(todo);
        await db.SaveChangesAsync();
        return TypedResults.Created($"/todos/{todo.Id}", todo);
    });

todos.MapGet("/{id}",
    async Task<Results<Ok<Todo>, NotFound>> (int id, TodoDb db) =>
    await db.Todos.FindAsync(id)
        is Todo todo
            ? TypedResults.Ok(todo)
            : TypedResults.NotFound());

todos.MapPut("/{id}", async Task<IResult> (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();
});

todos.MapDelete("/{id}", async Task<IResult> (int id, TodoDb db) => 
{
    if (await db.Todos.FindAsync(id) is Todo todo)
    {
        db.Todos.Remove(todo);
        await db.SaveChangesAsync();
        return TypedResults.Ok(todo); 
    }

    return TypedResults.NotFound(); 
});

todos.MapGet("/complete", async (TodoDb db) => 
    await db.Todos.Where(t => t.IsComplete).ToListAsync());

app.Run();

class Todo
{
    public int Id { get; set; }
    public string? Name { get; set; }
    public bool IsComplete { get; set; }
}

class TodoDb : DbContext
{
    public TodoDb(DbContextOptions<TodoDb> options)
        : base(options) { }

    public DbSet<Todo> Todos => Set<Todo>();
}

Swagger UI를 다시 실행하면 자물쇠 아이콘이 표시됩니다.

자물쇠

Authorize 버튼을 클릭하면 토큰을 입력할 수 있습니다.

인증 버튼

발행 토큰이 등록되면 인증이 완료됩니다.

인증 완료

인증이 완료된 상태에서 Web API를 테스트하면 정상적으로 실행이 됩니다.

200 OK

TODO API 전체 소스

VisualAcademy.Todos 프로젝트에 주요 변경 사항입니다.

코드: VisualAcademy.Todos.csproj

<Project Sdk="Microsoft.NET.Sdk.Web">

    <PropertyGroup>
        <TargetFramework>net7.0</TargetFramework>
        <Nullable>enable</Nullable>
        <ImplicitUsings>enable</ImplicitUsings>
        <UserSecretsId>...</UserSecretsId>
    </PropertyGroup>

    <ItemGroup>
        <PackageReference Include="Microsoft.AspNetCore.Authentication.Jwtbearer" Version="7.0.0" />
        <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="7.0.0" />
        <PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="7.0.0" />
        <PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" />
    </ItemGroup>

</Project>

코드: launchSettings.json

{
    "iisSettings": {
        "windowsAuthentication": false,
        "anonymousAuthentication": true,
        "iisExpress": {
            "applicationUrl": "http://localhost:37345",
            "sslPort": 44323
        }
    },
    "profiles": {
        "http": {
            "commandName": "Project",
            "dotnetRunMessages": true,
            "launchBrowser": true,
            "applicationUrl": "http://localhost:5213",
            "environmentVariables": {
                "ASPNETCORE_ENVIRONMENT": "Development"
            }
        },
        "https": {
            "commandName": "Project",
            "dotnetRunMessages": true,
            "launchBrowser": true,
            "launchUrl": "swagger",
            "applicationUrl": "https://localhost:7071;http://localhost:5213",
            "environmentVariables": {
                "ASPNETCORE_ENVIRONMENT": "Development"
            }
        },
        "IIS Express": {
            "commandName": "IISExpress",
            "launchBrowser": true,
            "environmentVariables": {
                "ASPNETCORE_ENVIRONMENT": "Development"
            }
        }
    }
}

코드: appsettings.Development.json

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "Authentication": {
    "Schemes": {
      "Bearer": {
        "ValidAudiences": [
          "http://localhost:37345",
          "https://localhost:44323",
          "http://localhost:5213",
          "https://localhost:7071"
        ],
        "ValidIssuer": "dotnet-user-jwts"
      }
    }
  }
}

지금까지 작성 완료한 전체 소스 코드입니다. 단일 파일인 Program.cs 파일에서 TODO API를 위한 모든 코드가 들어있습니다.

코드: VisualAcademy.Todos/Program.cs

using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.EntityFrameworkCore;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDbContext<TodoDb>(
    opt => opt.UseInMemoryDatabase("Todos"));

builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(options =>
{
    options.InferSecuritySchemes();
    options.AddSecurityRequirement(new OpenApiSecurityRequirement
    {
        {
            new OpenApiSecurityScheme
            {
                Reference = new OpenApiReference
                {
                    Type = ReferenceType.SecurityScheme, Id = "Bearer"
                }
            },
            new string[] { }
        }
    });
});
builder.Services.Configure<SwaggerGeneratorOptions>(options =>
{
    options.InferSecuritySchemes = true;
});
builder.Services.AddAuthentication().AddJwtBearer();
builder.Services.AddAuthorization();

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

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

var todos = app.MapGroup("/todos").RequireAuthorization();

todos.MapGet("/", async (TodoDb db) =>
    await db.Todos.ToListAsync());

todos.MapPost("/", async (Todo todo, TodoDb db) =>
{
    db.Todos.Add(todo);
    await db.SaveChangesAsync();
    return TypedResults.Created($"/todos/{todo.Id}", todo);
});

todos.MapGet("/{id}", async Task<Results<Ok<Todo>, NotFound>> (int id, TodoDb db) =>
    await db.Todos.FindAsync(id)
        is Todo todo
            ? TypedResults.Ok(todo)
            : TypedResults.NotFound());

todos.MapPut("/{id}", async Task<IResult> (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();
});

todos.MapDelete("/{id}", async Task<IResult> (int id, TodoDb db) =>
{
    if (await db.Todos.FindAsync(id) is Todo todo)
    {
        db.Todos.Remove(todo);
        await db.SaveChangesAsync();
        return TypedResults.Ok(todo);
    }

    return TypedResults.NotFound();
});

todos.MapGet("/complete", async (TodoDb db) =>
    await db.Todos.Where(t => t.IsComplete).ToListAsync());

app.Run();

class Todo
{
    public int Id { get; set; }
    public string? Name { get; set; }
    public bool IsComplete { get; set; }
}

class TodoDb : DbContext
{
    public TodoDb(DbContextOptions<TodoDb> options)
        : base(options) { }

    public DbSet<Todo> Todos => Set<Todo>();
}

실제 데이터베이스를 사용한 인증 방식은 다음 링크를 참고하세요.

ASP.NET Core 8.0 최소 API 인증 적용하기

추가 학습 고려 사항

이 강좌에서는 Model 클래스를 직접 Web API에 노출하였습니다. 향후 발전 가능성으로, 별도의 DTO(Data Transfer Object) 클래스를 생성하여 이를 Web API에 사용하고자 합니다. 이 과정에서 Model 클래스와 DTO 클래스 간의 데이터 변환은 AutoMapper와 같은 NuGet 패키지를 활용하여 효율적으로 진행할 수 있을 것입니다.

참고 자료

이 강좌는 다음 경로의 .NET Conf 2022 동영상의 첫 번째 데모 영상을 참고하였습니다.

긴 글 읽어주셔서 감사합니다.

끝.

ASP.NET Core 7.0 Minimal APIs

ASP.NET Core 7.0 Empty 프로젝트를 사용하여 Web API를 만들고 사용하는 방법에 대한 가이드 동영상입니다.

1. ASP.NET Core Empty 프로젝트 템플릿을 사용하여 VisualAcademy.Todos 프로젝트 생성 및 실행

https://youtu.be/Lh-O5tGEsxw

2. Todo 모델 클래스 생성

https://youtu.be/ynqCx72rfxw

3. Microsoft.EntityFrameworkCore.InMemory 패키지 추가 및 TodoDb 이름으로 DbContext 클래스 생성

https://youtu.be/wjzjyM6Fd9s

4. TodoDb DbContext 클래스를 builder 개체의 서비스로 등록하기

https://youtu.be/GLKphLzsMLE

5. Todos 테이블의 모든 데이터를 JSON으로 반환하는 HttpGet 메서드 구현하고 빈 JSON 배열 출력 확인

https://youtu.be/YMupOLGwAYw

6. HttpGet 메서드를 동기 방식에서 async와 await를 사용하는 비동기 방식으로 변경

https://youtu.be/KD-fFP_dnDU

7. HttpPost 메서드를 MapPost 메서드를 사용하여 기본 모양으로 구현하고 Postman 도구로 테스트 데모

https://youtu.be/hAkTvn-WriA

8. 상세 보기용 HttpGet 메서드 만들고 Post 메서드에서 201 상태를 반환하는 방식으로 변경

https://youtu.be/Ay227QV9KvM

9. MapGroup 메서드로 엔드포인트를 그룹화하여 묶어 관리하기

https://youtu.be/28k_tBf4jYA

10. Swagger UI 사용을 위한 패키지 및 서비스 그리고 미들웨어 등록하기

https://youtu.be/6hBszE4B9kc

Todo application with ASP.NET Core

Todo 앱에 대한 Microsoft 개발자가 직접 구현한 내용의 아주 좋은 내용의 강좌는 다음 링크를 통해서 살펴볼 수 있습니다.

https://github.com/davidfowl/TodoApi

VisualAcademy Docs의 모든 콘텐츠, 이미지, 동영상의 저작권은 박용준에게 있습니다. 저작권법에 의해 보호를 받는 저작물이므로 무단 전재와 복제를 금합니다. 사이트의 콘텐츠를 복제하여 블로그, 웹사이트 등에 게시할 수 없습니다. 단, 링크와 SNS 공유, Youtube 동영상 공유는 허용합니다. www.VisualAcademy.com
박용준 강사의 모든 동영상 강의는 데브렉에서 독점으로 제공됩니다. www.devlec.com