최소 API 빠른 참조

이 문서:

  • 최소 API에 대한 빠른 참조를 제공합니다.
  • 숙련된 개발자를 대상으로 합니다. 소개는 자습서: ASP.NET Core를 사용하여 최소 API 만들기를 참조 하세요.

최소 API는 다음으로 구성됩니다.

WebApplication

다음 코드는 ASP.NET Core 템플릿에서 생성됩니다.

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

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

app.Run();

위의 코드는 명령줄에서 dotnet new web을 통해 만들거나 Visual Studio에서 빈 웹 템플릿을 선택하여 만들 수 있습니다.

다음 코드는 WebApplicationBuilder를 명시적으로 만들지 않고 WebApplication(app)을 만듭니다.

var app = WebApplication.Create(args);

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

app.Run();

WebApplication.Create는 미리 구성된 기본값을 사용하여 WebApplication 클래스의 새 인스턴스를 초기화합니다.

WebApplication 는 특정 조건에 따라 다음 미들웨어 Minimal API applications 를 자동으로 추가합니다.

  • HostingEnvironment"Development"일 때 UseDeveloperExceptionPage가 먼저 추가됩니다.
  • 사용자 코드가 아직 UseRouting를 호출하지 않은 경우 및 구성된 엔드포인트가 있는 경우(예: app.MapGet), UseRouting가 두 번째로 추가됩니다.
  • UseEndpoints는 엔드포인트가 구성된 경우 미들웨어 파이프라인의 끝에 추가됩니다.
  • UseAuthentication은 사용자 코드가 아직 호출 UseAuthentication 되지 않은 경우와 서비스 공급자에서 검색할 수 있는 경우 IAuthenticationSchemeProvider 즉시 UseRouting 추가됩니다. IAuthenticationSchemeProviderAddAuthentication를 사용할 때 기본적으로 추가되고 IServiceProviderIsService를 사용하여 서비스가 검색됩니다.
  • UseAuthorization 는 사용자 코드가 아직 호출 UseAuthorization 되지 않았고 서비스 공급자에서 검색할 수 있는 경우 IAuthorizationHandlerProvider 다음에 추가됩니다. IAuthorizationHandlerProviderAddAuthorization를 사용할 때 기본적으로 추가되고 IServiceProviderIsService를 사용하여 서비스가 검색됩니다.
  • 사용자 구성 미들웨어 및 엔드포인트는 UseRoutingUseEndpoints 사이에 추가됩니다.

다음 코드는 앱에 추가되는 자동 미들웨어가 생성하는 것입니다.

if (isDevelopment)
{
    app.UseDeveloperExceptionPage();
}

app.UseRouting();

if (isAuthenticationConfigured)
{
    app.UseAuthentication();
}

if (isAuthorizationConfigured)
{
    app.UseAuthorization();
}

// user middleware/endpoints
app.CustomMiddleware(...);
app.MapGet("/", () => "hello world");
// end user middleware/endpoints

app.UseEndpoints(e => {});

경우에 따라 앱에 대한 기본 미들웨어 구성이 올바르지 않으며 수정이 필요합니다. 예를 들어, UseCorsUseAuthenticationUseAuthorization 전에 호출되어야 합니다. 앱은 호출 UseAuthentication 해야 하며 UseAuthorization 호출되는 경우 UseCors :

app.UseCors();
app.UseAuthentication();
app.UseAuthorization();

경로 일치가 발생하기 전에 미들웨어를 실행해야 하는 경우, UseRouting를 호출해야 하며 UseRouting에 대한 호출 전에 미들웨어를 배치해야 합니다. UseEndpoints 는 앞에서 설명한 대로 자동으로 추가되므로 이 경우에는 필요하지 않습니다.

app.Use((context, next) =>
{
    return next(context);
});

app.UseRouting();

// other middleware and endpoints

터미널 미들웨어를 추가하는 경우:

  • 미들웨어는 UseEndpoints 다음에 추가해야 합니다.
  • 터미널 미들웨어를 올바른 위치에 배치할 수 있도록 앱은 UseRoutingUseEndpoints를 호출해야 합니다.
app.UseRouting();

app.MapGet("/", () => "hello world");

app.UseEndpoints(e => {});

app.Run(context =>
{
    context.Response.StatusCode = 404;
    return Task.CompletedTask;
});

터미널 미들웨어는 요청을 처리하는 엔드포인트가 없는 경우 실행되는 미들웨어입니다.

포트 작업

Visual Studio 또는 dotnet new를 사용하여 웹앱을 만들면 앱이 응답하는 포트를 지정하는 Properties/launchSettings.json 파일이 만들어집니다. 다음에 오는 포트 설정 샘플에서는 Visual Studio에서 앱을 실행하면 Unable to connect to web server 'AppName' 오류 대화 상자가 반환됩니다. Visual Studio는 Properties/launchSettings.json에 지정된 포트가 예상되기 때문에 오류를 반환하지만 앱은 app.Run("http://localhost:3000")에서 지정한 포트를 사용하고 있습니다. 명령줄에서 다음 포트 변경 샘플을 실행합니다.

다음 섹션에서는 앱이 응답하는 포트를 설정합니다.

var app = WebApplication.Create(args);

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

app.Run("http://localhost:3000");

위의 코드에서 앱은 포트 3000에 응답합니다.

여러 포트

위의 코드에서 앱은 포트 30004000에 응답합니다.

var app = WebApplication.Create(args);

app.Urls.Add("http://localhost:3000");
app.Urls.Add("http://localhost:4000");

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

app.Run();

명령줄에서 포트 설정

다음 명령은 앱이 포트 7777에 응답하도록 합니다.

dotnet run --urls="https://localhost:7777"

Kestrel 엔드포인트가 appsettings.json 파일에도 구성된 경우 appsettings.json 파일에서 지정된 URL이 사용됩니다. 자세한 내용은 Kestrel 엔드포인트 구성을 참조하세요.

환경에서 포트 읽기

다음 코드는 환경에서 포트를 읽습니다.

var app = WebApplication.Create(args);

var port = Environment.GetEnvironmentVariable("PORT") ?? "3000";

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

app.Run($"http://localhost:{port}");

환경에서 포트를 설정하는 기본 방법은 다음 섹션에 나오는 ASPNETCORE_URLS 환경 변수를 사용하는 것입니다.

ASPNETCORE_URLS 환경 변수를 통해 포트 설정

ASPNETCORE_URLS 환경 변수를 사용하여 포트를 설정할 수 있습니다.

ASPNETCORE_URLS=http://localhost:3000

ASPNETCORE_URLS는 여러 URL을 지원합니다.

ASPNETCORE_URLS=http://localhost:3000;https://localhost:5000

모든 인터페이스에서 수신 대기

다음 샘플은 모든 인터페이스에서의 수신 대기를 보여 줍니다.

http://*:3000

var app = WebApplication.Create(args);

app.Urls.Add("http://*:3000");

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

app.Run();

http://+:3000

var app = WebApplication.Create(args);

app.Urls.Add("http://+:3000");

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

app.Run();

http://0.0.0.0:3000

var app = WebApplication.Create(args);

app.Urls.Add("http://0.0.0.0:3000");

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

app.Run();

ASPNETCORE_URLS를 사용하여 모든 인터페이스에서 수신 대기

이전 샘플은 ASPNETCORE_URLS를 사용할 수 있습니다.

ASPNETCORE_URLS=http://*:3000;https://+:5000;http://0.0.0.0:5005

ASPNETCORE_HTTPS_PORTS 사용하여 모든 인터페이스에서 수신 대기

위의 샘플은 사용할 ASPNETCORE_HTTPS_PORTS 수 있습니다.ASPNETCORE_HTTP_PORTS

ASPNETCORE_HTTP_PORTS=3000;5005
ASPNETCORE_HTTPS_PORTS=5000

자세한 내용은 ASP.NET Core Kestrel 웹 서버에 대한 엔드포인트 구성을 참조하세요.

개발 인증서로 HTTPS 지정

var app = WebApplication.Create(args);

app.Urls.Add("https://localhost:3000");

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

app.Run();

개발 인증서에 대한 자세한 내용은 Windows 및 macOS에서 ASP.NET Core HTTPS 개발 인증서 신뢰를 참조하세요.

사용자 지정 인증서를 사용하여 HTTPS 지정

다음 섹션에서는 파일 및 구성을 통해 사용자 지정 인증서를 지정하는 appsettings.json 방법을 보여 있습니다.

appsettings.json을 사용하여 사용자 지정 인증서 지정

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "Kestrel": {
    "Certificates": {
      "Default": {
        "Path": "cert.pem",
        "KeyPath": "key.pem"
      }
    }
  }
}

구성을 통해 사용자 지정 인증서 지정

var builder = WebApplication.CreateBuilder(args);

// Configure the cert and the key
builder.Configuration["Kestrel:Certificates:Default:Path"] = "cert.pem";
builder.Configuration["Kestrel:Certificates:Default:KeyPath"] = "key.pem";

var app = builder.Build();

app.Urls.Add("https://localhost:3000");

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

app.Run();

인증서 API 사용

using System.Security.Cryptography.X509Certificates;

var builder = WebApplication.CreateBuilder(args);

builder.WebHost.ConfigureKestrel(options =>
{
    options.ConfigureHttpsDefaults(httpsOptions =>
    {
        var certPath = Path.Combine(builder.Environment.ContentRootPath, "cert.pem");
        var keyPath = Path.Combine(builder.Environment.ContentRootPath, "key.pem");

        httpsOptions.ServerCertificate = X509Certificate2.CreateFromPemFile(certPath, 
                                         keyPath);
    });
});

var app = builder.Build();

app.Urls.Add("https://localhost:3000");

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

app.Run();

환경 읽기

var app = WebApplication.Create(args);

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/oops");
}

app.MapGet("/", () => "Hello World");
app.MapGet("/oops", () => "Oops! An error happened.");

app.Run();

환경 사용에 대한 자세한 내용은 ASP.NET Core에서 여러 환경 사용을 참조하세요.

구성

다음 코드는 구성 시스템에서 읽습니다.

var app = WebApplication.Create(args);

var message = app.Configuration["HelloKey"] ?? "Config failed!";

app.MapGet("/", () => message);

app.Run();

자세한 내용은 ASP.NET Core의 구성을 참조하세요.

로깅

다음 코드는 로그온 애플리케이션 시작에 메시지를 씁니다.

var app = WebApplication.Create(args);

app.Logger.LogInformation("The app started");

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

app.Run();

자세한 내용은 .NET Core 및 ASP.NET Core의 로깅을 참조하세요.

DI(종속성 주입) 컨테이너 액세스

다음 코드에서는 애플리케이션을 시작하는 동안 DI 컨테이너에서 서비스를 가져오는 방법을 보여줍니다.


var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();
builder.Services.AddScoped<SampleService>();

var app = builder.Build();

app.MapControllers();

using (var scope = app.Services.CreateScope())
{
    var sampleService = scope.ServiceProvider.GetRequiredService<SampleService>();
    sampleService.DoSomething();
}

app.Run();

다음 코드에서는 특성을 사용하여 DI 컨테이너에서 키에 액세스하는 [FromKeyedServices] 방법을 보여 있습니다.

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddKeyedSingleton<ICache, BigCache>("big");
builder.Services.AddKeyedSingleton<ICache, SmallCache>("small");

var app = builder.Build();

app.MapGet("/big", ([FromKeyedServices("big")] ICache bigCache) => bigCache.Get("date"));

app.MapGet("/small", ([FromKeyedServices("small")] ICache smallCache) => smallCache.Get("date"));

app.Run();

public interface ICache
{
    object Get(string key);
}
public class BigCache : ICache
{
    public object Get(string key) => $"Resolving {key} from big cache.";
}

public class SmallCache : ICache
{
    public object Get(string key) => $"Resolving {key} from small cache.";
}

DI에 대한 자세한 내용은 ASP.NET Core의 종속성 주입을 참조 하세요.

WebApplicationBuilder

이 섹션에는 WebApplicationBuilder를 사용하는 샘플 코드가 포함되어 있습니다.

콘텐츠 루트, 애플리케이션 이름, 환경 변경

다음 코드는 콘텐츠 루트, 애플리케이션 이름, 환경을 설정합니다.

var builder = WebApplication.CreateBuilder(new WebApplicationOptions
{
    Args = args,
    ApplicationName = typeof(Program).Assembly.FullName,
    ContentRootPath = Directory.GetCurrentDirectory(),
    EnvironmentName = Environments.Staging,
    WebRootPath = "customwwwroot"
});

Console.WriteLine($"Application Name: {builder.Environment.ApplicationName}");
Console.WriteLine($"Environment Name: {builder.Environment.EnvironmentName}");
Console.WriteLine($"ContentRoot Path: {builder.Environment.ContentRootPath}");
Console.WriteLine($"WebRootPath: {builder.Environment.WebRootPath}");

var app = builder.Build();

WebApplication.CreateBuilder는 미리 구성된 기본값을 사용하여 WebApplicationBuilder 클래스의 새 인스턴스를 초기화합니다.

자세한 내용은 ASP.NET Core 기본 사항 개요를 참조하세요

환경 변수 또는 명령줄을 사용하여 콘텐츠 루트, 앱 이름 및 환경 변경

다음 표는 콘텐츠 루트, 앱 이름, 환경 변경에 사용되는 환경 변수와 명령줄 인수를 보여 줍니다.

기능 환경 변수 명령줄 인수
애플리케이션 이름 ASPNETCORE_APPLICATIONNAME --applicationName
환경 이름 ASPNETCORE_ENVIRONMENT --environment
콘텐츠 루트 ASPNETCORE_CONTENTROOT --contentRoot

구성 공급자 추가

다음 샘플은 INI 구성 공급자를 추가합니다.

var builder = WebApplication.CreateBuilder(args);

builder.Configuration.AddIniFile("appsettings.ini");

var app = builder.Build();

자세한 내용은 ASP.NET Core의 구성에서 파일 구성 공급자를 참조하세요.

구성 읽기

기본적으로 WebApplicationBuilder는 다음을 포함하여 여러 원본에서 구성을 읽습니다.

  • appSettings.jsonappSettings.{environment}.json
  • 환경 변수
  • 명령줄

읽은 구성 원본의 전체 목록은 ASP.NET Core의 구성에서 기본 구성을 참조하세요.

다음 코드는 구성에서 HelloKey를 읽고 / 엔드포인트에 값을 표시합니다. 구성 값이 null이면 "Hello"가 message에 할당됩니다.

var builder = WebApplication.CreateBuilder(args);

var message = builder.Configuration["HelloKey"] ?? "Hello";

var app = builder.Build();

app.MapGet("/", () => message);

app.Run();

환경 읽기

var builder = WebApplication.CreateBuilder(args);

if (builder.Environment.IsDevelopment())
{
    Console.WriteLine($"Running in development.");
}

var app = builder.Build();

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

app.Run();

로깅 공급자 추가

var builder = WebApplication.CreateBuilder(args);

// Configure JSON logging to the console.
builder.Logging.AddJsonConsole();

var app = builder.Build();

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

app.Run();

서비스 추가

var builder = WebApplication.CreateBuilder(args);

// Add the memory cache services.
builder.Services.AddMemoryCache();

// Add a custom scoped service.
builder.Services.AddScoped<ITodoRepository, TodoRepository>();
var app = builder.Build();

IHostBuilder 사용자 지정

IHostBuilder의 기존 확장 메서드는 호스트 속성을 사용하여 액세스할 수 있습니다.

var builder = WebApplication.CreateBuilder(args);

// Wait 30 seconds for graceful shutdown.
builder.Host.ConfigureHostOptions(o => o.ShutdownTimeout = TimeSpan.FromSeconds(30));

var app = builder.Build();

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

app.Run();

IWebHostBuilder 사용자 지정

IWebHostBuilder의 확장 메서드는 WebApplicationBuilder.WebHost 속성을 사용하여 액세스할 수 있습니다.

var builder = WebApplication.CreateBuilder(args);

// Change the HTTP server implemenation to be HTTP.sys based
builder.WebHost.UseHttpSys();

var app = builder.Build();

app.MapGet("/", () => "Hello HTTP.sys");

app.Run();

웹 루트 변경

기본적으로 웹 루트는 wwwroot 폴더의 콘텐츠 루트를 기준으로 합니다. 웹 루트는 정적 파일 미들웨어가 정적 파일을 찾는 위치입니다. 웹 루트는 WebHostOptions, 명령줄 또는 UseWebRoot 메서드를 사용하여 변경할 수 있습니다.

var builder = WebApplication.CreateBuilder(new WebApplicationOptions
{
    Args = args,
    // Look for static files in webroot
    WebRootPath = "webroot"
});

var app = builder.Build();

app.Run();

사용자 지정 DI(종속성 주입) 컨테이너

다음 예제에서는 Autofac을 사용합니다.

var builder = WebApplication.CreateBuilder(args);

builder.Host.UseServiceProviderFactory(new AutofacServiceProviderFactory());

// Register services directly with Autofac here. Don't
// call builder.Populate(), that happens in AutofacServiceProviderFactory.
builder.Host.ConfigureContainer<ContainerBuilder>(builder => builder.RegisterModule(new MyApplicationModule()));

var app = builder.Build();

미들웨어 추가

기존 ASP.NET Core 미들웨어는 WebApplication에서 구성할 수 있습니다.

var app = WebApplication.Create(args);

// Setup the file server to serve static files.
app.UseFileServer();

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

app.Run();

자세한 내용은 ASP.NET Core 미들웨어를 참조하세요.

개발자 예외 페이지

WebApplication.CreateBuilder는 미리 구성된 기본값을 사용하여 WebApplicationBuilder 클래스의 새 인스턴스를 초기화합니다. 개발자 예외 페이지는 미리 구성된 기본값에서 사용하도록 설정됩니다. 개발 환경에서 다음 코드가 실행되는 경우 /로 이동하면 예외를 보여 주는 친숙한 페이지가 렌더링됩니다.

var builder = WebApplication.CreateBuilder(args);

var app = builder.Build();

app.MapGet("/", () =>
{
    throw new InvalidOperationException("Oops, the '/' route has thrown an exception.");
});

app.Run();

ASP.NET Core 미들웨어 기본 사항

다음 표에는 최소 API와 함께 자주 사용되는 미들웨어 일부가 나와 있습니다.

미들웨어 설명 API
인증 인증 지원을 제공합니다. UseAuthentication
Authorization 권한 부여 지원을 제공합니다. UseAuthorization
CORS 원본 간 리소스 공유를 구성합니다. UseCors
예외 처리기 미들웨어 파이프라인이 throw하는 예외를 전역으로 처리합니다. UseExceptionHandler
전달된 헤더 프록시된 헤더를 현재 요청에 전달합니다. UseForwardedHeaders
HTTPS 리디렉션 모든 HTTP 요청을 HTTPS로 리디렉션합니다. UseHttpsRedirection
HSTS(HTTP 엄격한 전송 보안) 특별한 응답 헤더를 추가하는 보안 향상 미들웨어입니다. UseHsts
요청 로깅 HTTP 요청 및 응답 로깅을 지원합니다. UseHttpLogging
요청 시간 제한 요청 시간 제한, 전역 기본값 및 엔드포인트당 구성을 지원합니다. UseRequestTimeouts
W3C 요청 로깅 W3C 형식의 HTTP 요청 및 응답 로깅을 지원합니다. UseW3CLogging
응답 캐싱 응답 캐시에 대한 지원을 제공합니다. UseResponseCaching
응답 압축 응답 압축에 대한 지원을 제공합니다. UseResponseCompression
세션 사용자 세션 관리에 대한 지원을 제공합니다. UseSession
정적 파일 정적 파일 및 디렉터리 검색 처리에 대한 지원을 제공합니다. UseStaticFiles, UseFileServer
WebSocket WebSocket 프로토콜을 활성화합니다. UseWebSockets

다음 섹션에서는 요청 처리에 대해 설명합니다. 라우팅, 매개 변수 바인딩 및 응답.

라우팅

구성된 WebApplicationMap{Verb}MapMethods를 지원하며 여기서 {Verb}Get, Post, Put 또는 Delete 등의 카멜 대/소문자 HTTP 메서드입니다.

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

app.MapGet("/", () => "This is a GET");
app.MapPost("/", () => "This is a POST");
app.MapPut("/", () => "This is a PUT");
app.MapDelete("/", () => "This is a DELETE");

app.MapMethods("/options-or-head", new[] { "OPTIONS", "HEAD" }, 
                          () => "This is an options or head request ");

app.Run();

이러한 메서드에 전달된 Delegate 인수를 "경로 처리기"라고 합니다.

경로 처리기

경로 처리기는 경로가 일치할 때 실행되는 메서드입니다. 경로 처리기는 람다 식, 로컬 함수, 인스턴스 메서드 또는 정적 메서드일 수 있습니다. 경로 처리기는 동기 또는 비동기일 수 있습니다.

람다 식

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

app.MapGet("/inline", () => "This is an inline lambda");

var handler = () => "This is a lambda variable";

app.MapGet("/", handler);

app.Run();

로컬 함수

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

string LocalFunction() => "This is local function";

app.MapGet("/", LocalFunction);

app.Run();

인스턴스 메서드

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

var handler = new HelloHandler();

app.MapGet("/", handler.Hello);

app.Run();

class HelloHandler
{
    public string Hello()
    {
        return "Hello Instance method";
    }
}

정적 메서드

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

app.MapGet("/", HelloHandler.Hello);

app.Run();

class HelloHandler
{
    public static string Hello()
    {
        return "Hello static method";
    }
}

외부에 정의된 엔드포인트 Program.cs

최소 API는 에 Program.cs위치할 필요가 없습니다.

Program.cs

using MinAPISeparateFile;

var builder = WebApplication.CreateSlimBuilder(args);

var app = builder.Build();

TodoEndpoints.Map(app);

app.Run();

TodoEndpoints.cs

namespace MinAPISeparateFile;

public static class TodoEndpoints
{
    public static void Map(WebApplication app)
    {
        app.MapGet("/", async context =>
        {
            // Get all todo items
            await context.Response.WriteAsJsonAsync(new { Message = "All todo items" });
        });

        app.MapGet("/{id}", async context =>
        {
            // Get one todo item
            await context.Response.WriteAsJsonAsync(new { Message = "One todo item" });
        });
    }
}

이 문서의 뒷부분에 있는 경로 그룹참조하세요.

경로의 URL을 생성하기 위해 경로에 이름을 지정할 수 있습니다. 이름이 지정된 경로를 사용하면 앱에서 경로를 하드 코드로 작성할 필요가 없습니다.

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

app.MapGet("/hello", () => "Hello named route")
   .WithName("hi");

app.MapGet("/", (LinkGenerator linker) => 
        $"The link to the hello route is {linker.GetPathByName("hi", values: null)}");

app.Run();

이전 코드는 / 엔드포인트에서 The link to the hello endpoint is /hello를 표시합니다.

참고: 엔드포인트 이름은 대/소문자를 구분합니다.

엔드포인트 이름:

  • 전역적으로 고유해야 합니다.
  • OpenAPI 지원을 사용할 때 OpenAPI 작업 ID로 사용됩니다. 자세한 내용은 OpenAPI를 참조하세요.

경로 매개 변수

경로 패턴 정의의 일부로 경로 매개 변수를 캡처할 수 있습니다.

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

app.MapGet("/users/{userId}/books/{bookId}", 
    (int userId, int bookId) => $"The user id is {userId} and book id is {bookId}");

app.Run();

이전 코드는 /users/3/books/7 URI에서 The user id is 3 and book id is 7을 반환합니다.

경로 처리기는 캡처할 매개 변수를 선언할 수 있습니다. 캡처하도록 선언된 매개 변수가 있는 경로에 대한 요청이 이루어지면 매개 변수가 구문 분석되어 처리기에 전달됩니다. 이렇게 하면 형식이 안전한 방식으로 값을 쉽게 캡처할 수 있습니다. 이전 코드에서 userIdbookId는 모두 int입니다.

이전 코드에서 경로 값 중 하나를 int로 변환할 수 없는 경우 예외가 throw됩니다. GET 요청 /users/hello/books/3는 다음 예외를 throw합니다.

BadHttpRequestException: Failed to bind parameter "int userId" from "hello".

와일드카드 및 catch all 경로

다음 catch all 경로는 `/posts/hello` 엔드포인트에서 Routing to hello를 반환합니다.

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

app.MapGet("/posts/{*rest}", (string rest) => $"Routing to {rest}");

app.Run();

경로 제약 조건

경로 제약 조건은 경로의 일치 동작을 제한합니다.

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

app.MapGet("/todos/{id:int}", (int id) => db.Todos.Find(id));
app.MapGet("/todos/{text}", (string text) => db.Todos.Where(t => t.Text.Contains(text));
app.MapGet("/posts/{slug:regex(^[a-z0-9_-]+$)}", (string slug) => $"Post {slug}");

app.Run();

다음 표는 이전 경로 템플릿과 동작을 보여 줍니다.

경로 템플릿 URI 일치 예제
/todos/{id:int} /todos/1
/todos/{text} /todos/something
/posts/{slug:regex(^[a-z0-9_-]+$)} /posts/mypost

자세한 내용은 ASP.NET Core의 라우팅에서 경로 제약 조건 참조를 참조하세요.

경로 그룹

MapGroup확장 메서드는 공통 접두사를 사용하여 엔드포인트 그룹을 구성하는 데 도움이 됩니다. 반복 코드를 줄이고 엔드포인트 메타데이터를 추가하는 RequireAuthorization이나 WithMetadata와 같은 메서드로서 단일 호출로 엔드포인트 전체 그룹을 사용자 지정할 수 있게 됩니다.

예를 들어 다음 코드에서는 두 개의 유사한 엔드포인트 그룹을 만듭니다.

app.MapGroup("/public/todos")
    .MapTodosApi()
    .WithTags("Public");

app.MapGroup("/private/todos")
    .MapTodosApi()
    .WithTags("Private")
    .AddEndpointFilterFactory(QueryPrivateTodos)
    .RequireAuthorization();


EndpointFilterDelegate QueryPrivateTodos(EndpointFilterFactoryContext factoryContext, EndpointFilterDelegate next)
{
    var dbContextIndex = -1;

    foreach (var argument in factoryContext.MethodInfo.GetParameters())
    {
        if (argument.ParameterType == typeof(TodoDb))
        {
            dbContextIndex = argument.Position;
            break;
        }
    }

    // Skip filter if the method doesn't have a TodoDb parameter.
    if (dbContextIndex < 0)
    {
        return next;
    }

    return async invocationContext =>
    {
        var dbContext = invocationContext.GetArgument<TodoDb>(dbContextIndex);
        dbContext.IsPrivate = true;

        try
        {
            return await next(invocationContext);
        }
        finally
        {
            // This should only be relevant if you're pooling or otherwise reusing the DbContext instance.
            dbContext.IsPrivate = false;
        }
    };
}
public static RouteGroupBuilder MapTodosApi(this RouteGroupBuilder group)
{
    group.MapGet("/", GetAllTodos);
    group.MapGet("/{id}", GetTodo);
    group.MapPost("/", CreateTodo);
    group.MapPut("/{id}", UpdateTodo);
    group.MapDelete("/{id}", DeleteTodo);

    return group;
}

이 시나리오에서는 201 Created 결과에서 Location 헤더에 대한 상대 주소를 사용할 수 있습니다.

public static async Task<Created<Todo>> CreateTodo(Todo todo, TodoDb database)
{
    await database.AddAsync(todo);
    await database.SaveChangesAsync();

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

첫 번째 엔드포인트 그룹은 접두사로 지정된 요청만 /public/todos 매칭하게 되며 인증 없이 액세스할 수 있습니다. 두 번째 엔드포인트 그룹은 접두사 /private/todos를 가지고 인증이 필요한 요청만 일치합니다.

QueryPrivateTodos엔드포인트 필터 팩터리는 경로 처리기의 TodoDb 매개 변수를 수정하여 프라이빗 할일 데이터에 접근하고 보관할 수 있도록 수정하는 로컬 함수입니다.

경로 그룹은 경로 매개 변수 및 제약 조건이 있는 중첩 그룹 및 복잡한 접두사 유형도 지원합니다. 다음 예제에서 그룹에 매핑된 user 경로 처리기는 외부 그룹 접두사에 정의된 {org}{group} 경로 매개 변수를 캡처할 수 있습니다.

접두사는 비어 있을 수도 있습니다. 이 기능은 경로 패턴을 변경하지 않고 엔드포인트 메타데이터 또는 필터를 엔드포인트 그룹에 추가하는 데 유용할 수 있습니다.

var all = app.MapGroup("").WithOpenApi();
var org = all.MapGroup("{org}");
var user = org.MapGroup("{user}");
user.MapGet("", (string org, string user) => $"{org}/{user}");

그룹에 필터 또는 메타데이터를 추가하면 내부 그룹 또는 특정 엔드포인트에 추가될 수 있는 추가 필터 또는 메타데이터를 추가하기 전에 각 엔드포인트에 개별적으로 추가하는 것과 동일한 결과가 됩니다.

var outer = app.MapGroup("/outer");
var inner = outer.MapGroup("/inner");

inner.AddEndpointFilter((context, next) =>
{
    app.Logger.LogInformation("/inner group filter");
    return next(context);
});

outer.AddEndpointFilter((context, next) =>
{
    app.Logger.LogInformation("/outer group filter");
    return next(context);
});

inner.MapGet("/", () => "Hi!").AddEndpointFilter((context, next) =>
{
    app.Logger.LogInformation("MapGet filter");
    return next(context);
});

위의 예제에서 외부 필터는 두 번째로 추가된 경우에도 내부 필터 앞에 들어오는 요청을 기록하게 됩니다. 필터가 서로 다른 그룹에 적용되었기 때문에 필터가 추가된 상대적 순서는 중요하지 않습니다. 동일한 그룹 또는 특정 엔드포인트에 적용되는 경우 주문 필터가 추가됩니다.

/outer/inner/에 대한 요청은 다음을 기록합니다.

/outer group filter
/inner group filter
MapGet filter

매개 변수 바인딩

매개 변수 바인딩은 경로 처리기가 표현하는 강력한 형식의 매개 변수로 요청 데이터를 변환하는 프로세스입니다. 바인딩 소스는 매개 변수가 바인딩되는 위치를 결정합니다. 바인딩 소스는 HTTP 메서드 및 매개 변수 형식에 따라 명시적이거나 유추될 수 있습니다.

지원되는 바인딩 소스:

  • 경로 값
  • 쿼리 문자열
  • 헤더
  • 본문(JSON 형식)
  • 양식 값
  • 종속성 주입에서 제공하는 서비스
  • 사용자 지정

다음 GET 경로 처리기는 이러한 매개 변수 바인딩 소스 중 일부를 사용합니다.

var builder = WebApplication.CreateBuilder(args);

// Added as service
builder.Services.AddSingleton<Service>();

var app = builder.Build();

app.MapGet("/{id}", (int id,
                     int page,
                     [FromHeader(Name = "X-CUSTOM-HEADER")] string customHeader,
                     Service service) => { });

class Service { }

다음 표에서는 이전 예제에서 사용된 매개 변수와 연결된 바인딩 소스 간의 관계를 보여 줍니다.

매개 변수 바인딩 소스
id 경로 값
page 쿼리 문자열
customHeader 헤더
service 종속성 주입에서 제공

HTTP 메서드 GET, HEAD, OPTIONS, DELETE는 본문에서 암시적으로 바인딩되지 않습니다. 이러한 HTTP 메서드에 대해 본문(JSON 형식)에서 바인딩하려면 [FromBody]명시적으로 바인딩하거나 HttpRequest에서 읽습니다.

다음 예제 POST 경로 처리기는 person 매개 변수에 대해 본문(JSON 형식)의 바인딩 소스를 사용합니다.

var builder = WebApplication.CreateBuilder(args);

var app = builder.Build();

app.MapPost("/", (Person person) => { });

record Person(string Name, int Age);

이전 예제의 매개 변수는 모두 요청 데이터에서 자동으로 바인딩됩니다. 매개 변수 바인딩이 제공하는 편의를 설명하기 위해 다음 경로 처리기는 요청에서 직접 요청 데이터를 읽는 방법을 보여줍니다.

app.MapGet("/{id}", (HttpRequest request) =>
{
    var id = request.RouteValues["id"];
    var page = request.Query["page"];
    var customHeader = request.Headers["X-CUSTOM-HEADER"];

    // ...
});

app.MapPost("/", async (HttpRequest request) =>
{
    var person = await request.ReadFromJsonAsync<Person>();

    // ...
});

명시적 매개 변수 바인딩

특성을 사용하여 매개 변수가 바인딩되는 위치를 명시적으로 선언할 수 있습니다.

using Microsoft.AspNetCore.Mvc;

var builder = WebApplication.CreateBuilder(args);

// Added as service
builder.Services.AddSingleton<Service>();

var app = builder.Build();


app.MapGet("/{id}", ([FromRoute] int id,
                     [FromQuery(Name = "p")] int page,
                     [FromServices] Service service,
                     [FromHeader(Name = "Content-Type")] string contentType) 
                     => {});

class Service { }

record Person(string Name, int Age);
매개 변수 바인딩 소스
id 이름이 id인 경로 값
page 이름이 "p"인 쿼리 문자열
service 종속성 주입에서 제공
contentType 이름이 "Content-Type"인 헤더

양식 값에서 명시적 바인딩

특성은 [FromForm] 양식 값을 바인딩합니다.

app.MapPost("/todos", async ([FromForm] string name,
    [FromForm] Visibility visibility, IFormFile? attachment, TodoDb db) =>
{
    var todo = new Todo
    {
        Name = name,
        Visibility = visibility
    };

    if (attachment is not null)
    {
        var attachmentName = Path.GetRandomFileName();

        using var stream = File.Create(Path.Combine("wwwroot", attachmentName));
        await attachment.CopyToAsync(stream);
    }

    db.Todos.Add(todo);
    await db.SaveChangesAsync();

    return Results.Ok();
});

// Remaining code removed for brevity.

다른 방법은 속성에 주석이 [AsParameters] 추가된 [FromForm]사용자 지정 형식으로 특성을 사용하는 것입니다. 예를 들어 다음 코드는 폼 값에서 레코드 구조체의 속성으로 NewTodoRequest 바인딩합니다.

app.MapPost("/ap/todos", async ([AsParameters] NewTodoRequest request, TodoDb db) =>
{
    var todo = new Todo
    {
        Name = request.Name,
        Visibility = request.Visibility
    };

    if (request.Attachment is not null)
    {
        var attachmentName = Path.GetRandomFileName();

        using var stream = File.Create(Path.Combine("wwwroot", attachmentName));
        await request.Attachment.CopyToAsync(stream);

        todo.Attachment = attachmentName;
    }

    db.Todos.Add(todo);
    await db.SaveChangesAsync();

    return Results.Ok();
});

// Remaining code removed for brevity.
public record struct NewTodoRequest([FromForm] string Name,
    [FromForm] Visibility Visibility, IFormFile? Attachment);

자세한 내용은 이 문서의 뒷부분에 있는 AsParameters 섹션을 참조하세요.

전체 샘플 코드AspNetCore.Docs.Samples 리포지토리에 있습니다.

IFormFile 및 IFormFileCollection에서 바인딩 보호

복합 양식 바인딩은 다음을 [FromForm]사용하고 사용할 IFormFileIFormFileCollection 수 있습니다.

using Microsoft.AspNetCore.Antiforgery;
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.AspNetCore.Mvc;

var builder = WebApplication.CreateBuilder();

builder.Services.AddAntiforgery();

var app = builder.Build();
app.UseAntiforgery();

// Generate a form with an anti-forgery token and an /upload endpoint.
app.MapGet("/", (HttpContext context, IAntiforgery antiforgery) =>
{
    var token = antiforgery.GetAndStoreTokens(context);
    var html = MyUtils.GenerateHtmlForm(token.FormFieldName, token.RequestToken!);
    return Results.Content(html, "text/html");
});

app.MapPost("/upload", async Task<Results<Ok<string>, BadRequest<string>>>
    ([FromForm] FileUploadForm fileUploadForm, HttpContext context,
                                                IAntiforgery antiforgery) =>
{
    await MyUtils.SaveFileWithName(fileUploadForm.FileDocument!,
              fileUploadForm.Name!, app.Environment.ContentRootPath);
    return TypedResults.Ok($"Your file with the description:" +
        $" {fileUploadForm.Description} has been uploaded successfully");
});

app.Run();

위조 방지 토큰을 포함하는 요청에 [FromForm] 바인딩된 매개 변수입니다. 위조 방지 토큰은 요청이 처리될 때 유효성을 검사합니다. 자세한 내용은 최소 API를 사용한 위조 방지를 참조 하세요.

자세한 내용은 최소 API의 양식 바인딩을 참조 하세요.

전체 샘플 코드AspNetCore.Docs.Samples 리포지토리에 있습니다.

종속성 주입을 사용하는 매개 변수 바인딩

최소 API에 대한 매개 변수 바인딩은 형식이 서비스로 구성된 경우 종속성 주입을 통해 매개 변수를 바인딩합니다. 매개 변수에 [FromServices] 특성을 명시적으로 적용할 필요는 없습니다. 다음 코드에서는 두 작업 모두 시간을 반환합니다.

using Microsoft.AspNetCore.Mvc;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton<IDateTime, SystemDateTime>();

var app = builder.Build();

app.MapGet("/",   (               IDateTime dateTime) => dateTime.Now);
app.MapGet("/fs", ([FromServices] IDateTime dateTime) => dateTime.Now);
app.Run();

선택적 매개 변수

경로 처리기에서 선언된 매개 변수는 필수로 처리됩니다.

  • 요청이 경로와 일치하는 경우 요청에 모든 필수 매개 변수가 제공되는 경우에만 경로 처리기가 실행됩니다.
  • 모든 필수 매개 변수를 제공하지 못하면 오류가 발생합니다.
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/products", (int pageNumber) => $"Requesting page {pageNumber}");

app.Run();
URI result
/products?pageNumber=3 3 반환됨
/products BadHttpRequestException: 쿼리 문자열에서 필수 매개 변수 "int pageNumber"가 제공되지 않았습니다.
/products/1 HTTP 404 오류, 일치하는 경로 없음

pageNumber를 선택 사항으로 지정하려면 형식을 선택 사항으로 정의하거나 기본값을 제공합니다.

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

app.MapGet("/products", (int? pageNumber) => $"Requesting page {pageNumber ?? 1}");

string ListProducts(int pageNumber = 1) => $"Requesting page {pageNumber}";

app.MapGet("/products2", ListProducts);

app.Run();
URI result
/products?pageNumber=3 3 반환됨
/products 1 반환됨
/products2 1 반환됨

이전 null 허용 값과 기본값은 모든 원본에 적용됩니다.

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

app.MapPost("/products", (Product? product) => { });

app.Run();

이전 코드는 요청 본문을 보내지 않은 경우 null 곱을 사용하여 메서드를 호출합니다.

참고: 잘못된 데이터를 제공하고 매개 변수가 null을 허용하면 경로 처리기가 실행되지 않습니다.

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

app.MapGet("/products", (int? pageNumber) => $"Requesting page {pageNumber ?? 1}");

app.Run();
URI result
/products?pageNumber=3 3 반환됨
/products 1 반환됨
/products?pageNumber=two BadHttpRequestException: "2"에서 "Nullable<int> pageNumber" 매개 변수를 바인딩하지 못했습니다.
/products/two HTTP 404 오류, 일치하는 경로 없음

자세한 내용은 바인딩 실패 섹션을 참조하세요.

특수 형식

다음 형식은 명시적 특성 없이 바인딩됩니다.

  • HttpContext: 현재 HTTP 요청 또는 응답에 대한 모든 정보를 포함하는 컨텍스트입니다.

    app.MapGet("/", (HttpContext context) => context.Response.WriteAsync("Hello World"));
    
  • HttpRequestHttpResponse: HTTP 요청 및 HTTP 응답:

    app.MapGet("/", (HttpRequest request, HttpResponse response) =>
        response.WriteAsync($"Hello World {request.Query["name"]}"));
    
  • CancellationToken: 현재 HTTP 요청에 연결된 취소 토큰:

    app.MapGet("/", async (CancellationToken cancellationToken) => 
        await MakeLongRunningRequestAsync(cancellationToken));
    
  • ClaimsPrincipal: HttpContext.User에서 바인딩된 요청에 연결된 사용자:

    app.MapGet("/", (ClaimsPrincipal user) => user.Identity.Name);
    

요청 본문을 Stream 또는 PipeReader로 바인딩

요청 본문을 Stream 또는 PipeReader로 바인딩하여 사용자가 데이터를 처리하여 다음 작업을 수행해야 하는 시나리오를 효율적으로 지원할 수 있습니다.

  • 데이터를 Blob 스토리지에 저장하거나 큐 공급자의 큐에 추가합니다.
  • 작업자 프로세스 또는 클라우드 함수를 사용하여 저장된 데이터를 처리합니다.

예를 들어 데이터는 Azure Queue Storage의 큐에 추가하거나 Azure Blob Storage에 저장할 수 있습니다.

다음 코드에서는 백그라운드 큐를 구현합니다.

using System.Text.Json;
using System.Threading.Channels;

namespace BackgroundQueueService;

class BackgroundQueue : BackgroundService
{
    private readonly Channel<ReadOnlyMemory<byte>> _queue;
    private readonly ILogger<BackgroundQueue> _logger;

    public BackgroundQueue(Channel<ReadOnlyMemory<byte>> queue,
                               ILogger<BackgroundQueue> logger)
    {
        _queue = queue;
        _logger = logger;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        await foreach (var dataStream in _queue.Reader.ReadAllAsync(stoppingToken))
        {
            try
            {
                var person = JsonSerializer.Deserialize<Person>(dataStream.Span)!;
                _logger.LogInformation($"{person.Name} is {person.Age} " +
                                       $"years and from {person.Country}");
            }
            catch (Exception ex)
            {
                _logger.LogError(ex.Message);
            }
        }
    }
}

class Person
{
    public string Name { get; set; } = String.Empty;
    public int Age { get; set; }
    public string Country { get; set; } = String.Empty;
}

다음 코드에서는 요청 본문을 Stream에 바인딩합니다.

app.MapPost("/register", async (HttpRequest req, Stream body,
                                 Channel<ReadOnlyMemory<byte>> queue) =>
{
    if (req.ContentLength is not null && req.ContentLength > maxMessageSize)
    {
        return Results.BadRequest();
    }

    // We're not above the message size and we have a content length, or
    // we're a chunked request and we're going to read up to the maxMessageSize + 1. 
    // We add one to the message size so that we can detect when a chunked request body
    // is bigger than our configured max.
    var readSize = (int?)req.ContentLength ?? (maxMessageSize + 1);

    var buffer = new byte[readSize];

    // Read at least that many bytes from the body.
    var read = await body.ReadAtLeastAsync(buffer, readSize, throwOnEndOfStream: false);

    // We read more than the max, so this is a bad request.
    if (read > maxMessageSize)
    {
        return Results.BadRequest();
    }

    // Attempt to send the buffer to the background queue.
    if (queue.Writer.TryWrite(buffer.AsMemory(0..read)))
    {
        return Results.Accepted();
    }

    // We couldn't accept the message since we're overloaded.
    return Results.StatusCode(StatusCodes.Status429TooManyRequests);
});

다음 코드에서는 전체 Program.cs 파일을 보여 줍니다.

using System.Threading.Channels;
using BackgroundQueueService;

var builder = WebApplication.CreateBuilder(args);
// The max memory to use for the upload endpoint on this instance.
var maxMemory = 500 * 1024 * 1024;

// The max size of a single message, staying below the default LOH size of 85K.
var maxMessageSize = 80 * 1024;

// The max size of the queue based on those restrictions
var maxQueueSize = maxMemory / maxMessageSize;

// Create a channel to send data to the background queue.
builder.Services.AddSingleton<Channel<ReadOnlyMemory<byte>>>((_) =>
                     Channel.CreateBounded<ReadOnlyMemory<byte>>(maxQueueSize));

// Create a background queue service.
builder.Services.AddHostedService<BackgroundQueue>();
var app = builder.Build();

// curl --request POST 'https://localhost:<port>/register' --header 'Content-Type: application/json' --data-raw '{ "Name":"Samson", "Age": 23, "Country":"Nigeria" }'
// curl --request POST "https://localhost:<port>/register" --header "Content-Type: application/json" --data-raw "{ \"Name\":\"Samson\", \"Age\": 23, \"Country\":\"Nigeria\" }"
app.MapPost("/register", async (HttpRequest req, Stream body,
                                 Channel<ReadOnlyMemory<byte>> queue) =>
{
    if (req.ContentLength is not null && req.ContentLength > maxMessageSize)
    {
        return Results.BadRequest();
    }

    // We're not above the message size and we have a content length, or
    // we're a chunked request and we're going to read up to the maxMessageSize + 1. 
    // We add one to the message size so that we can detect when a chunked request body
    // is bigger than our configured max.
    var readSize = (int?)req.ContentLength ?? (maxMessageSize + 1);

    var buffer = new byte[readSize];

    // Read at least that many bytes from the body.
    var read = await body.ReadAtLeastAsync(buffer, readSize, throwOnEndOfStream: false);

    // We read more than the max, so this is a bad request.
    if (read > maxMessageSize)
    {
        return Results.BadRequest();
    }

    // Attempt to send the buffer to the background queue.
    if (queue.Writer.TryWrite(buffer.AsMemory(0..read)))
    {
        return Results.Accepted();
    }

    // We couldn't accept the message since we're overloaded.
    return Results.StatusCode(StatusCodes.Status429TooManyRequests);
});

app.Run();
  • 데이터를 읽을 때 StreamHttpRequest.Body와 동일한 개체입니다.
  • 요청 본문은 기본적으로 버퍼링되지 않습니다. 본문을 읽은 후에는 되감을 수 없습니다. 스트림은 여러 번 읽을 수 없습니다.
  • StreamPipeReader는 최소 작업 처리기 외부에서 사용할 수 없습니다. 기본 버퍼가 삭제되거나 다시 사용되기 때문입니다.

IFormFile 및 IFormFileCollection을 사용하여 파일 업로드

다음 코드는 IFormFileIFormFileCollection을 사용하여 파일을 업로드합니다.

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

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

app.MapPost("/upload", async (IFormFile file) =>
{
    var tempFile = Path.GetTempFileName();
    app.Logger.LogInformation(tempFile);
    using var stream = File.OpenWrite(tempFile);
    await file.CopyToAsync(stream);
});

app.MapPost("/upload_many", async (IFormFileCollection myFiles) =>
{
    foreach (var file in myFiles)
    {
        var tempFile = Path.GetTempFileName();
        app.Logger.LogInformation(tempFile);
        using var stream = File.OpenWrite(tempFile);
        await file.CopyToAsync(stream);
    }
});

app.Run();

인증된 파일 업로드 요청은 인증 헤더, 클라이언트 인증서 또는 cookie 헤더를 사용하여 지원됩니다.

IFormCollection, IFormFile 및 IFormFileCollection을 사용하여 양식에 바인딩

IFormFile사용하여 IFormCollection양식 기반 매개 변수에서 바인딩하며 IFormFileCollection 지원됩니다. OpenAPI 메타데이터는 Swagger UI와의 통합을 지원하기 위해 양식 매개 변수에 대해 유추됩니다.

다음 코드는 형식에서 유추된 바인딩을 사용하여 파일을 업로드합니다 IFormFile .

using Microsoft.AspNetCore.Antiforgery;
using Microsoft.AspNetCore.Http.HttpResults;

var builder = WebApplication.CreateBuilder();

builder.Services.AddAntiforgery();

var app = builder.Build();
app.UseAntiforgery();

string GetOrCreateFilePath(string fileName, string filesDirectory = "uploadFiles")
{
    var directoryPath = Path.Combine(app.Environment.ContentRootPath, filesDirectory);
    Directory.CreateDirectory(directoryPath);
    return Path.Combine(directoryPath, fileName);
}

async Task UploadFileWithName(IFormFile file, string fileSaveName)
{
    var filePath = GetOrCreateFilePath(fileSaveName);
    await using var fileStream = new FileStream(filePath, FileMode.Create);
    await file.CopyToAsync(fileStream);
}

app.MapGet("/", (HttpContext context, IAntiforgery antiforgery) =>
{
    var token = antiforgery.GetAndStoreTokens(context);
    var html = $"""
      <html>
        <body>
          <form action="/upload" method="POST" enctype="multipart/form-data">
            <input name="{token.FormFieldName}" type="hidden" value="{token.RequestToken}"/>
            <input type="file" name="file" placeholder="Upload an image..." accept=".jpg, 
                                                                            .jpeg, .png" />
            <input type="submit" />
          </form> 
        </body>
      </html>
    """;

    return Results.Content(html, "text/html");
});

app.MapPost("/upload", async Task<Results<Ok<string>,
   BadRequest<string>>> (IFormFile file, HttpContext context, IAntiforgery antiforgery) =>
{
    var fileSaveName = Guid.NewGuid().ToString("N") + Path.GetExtension(file.FileName);
    await UploadFileWithName(file, fileSaveName);
    return TypedResults.Ok("File uploaded successfully!");
});

app.Run();

경고: 양식을 구현할 때 앱은 XSRF/CSRF(교차 사이트 요청 위조) 공격을 방지해야 합니다. 위의 코드 IAntiforgery 에서 서비스는 위조 방지 토큰을 생성하고 유효성을 검사하여 XSRF 공격을 방지하는 데 사용됩니다.

using Microsoft.AspNetCore.Antiforgery;
using Microsoft.AspNetCore.Http.HttpResults;

var builder = WebApplication.CreateBuilder();

builder.Services.AddAntiforgery();

var app = builder.Build();
app.UseAntiforgery();

string GetOrCreateFilePath(string fileName, string filesDirectory = "uploadFiles")
{
    var directoryPath = Path.Combine(app.Environment.ContentRootPath, filesDirectory);
    Directory.CreateDirectory(directoryPath);
    return Path.Combine(directoryPath, fileName);
}

async Task UploadFileWithName(IFormFile file, string fileSaveName)
{
    var filePath = GetOrCreateFilePath(fileSaveName);
    await using var fileStream = new FileStream(filePath, FileMode.Create);
    await file.CopyToAsync(fileStream);
}

app.MapGet("/", (HttpContext context, IAntiforgery antiforgery) =>
{
    var token = antiforgery.GetAndStoreTokens(context);
    var html = $"""
      <html>
        <body>
          <form action="/upload" method="POST" enctype="multipart/form-data">
            <input name="{token.FormFieldName}" type="hidden" value="{token.RequestToken}"/>
            <input type="file" name="file" placeholder="Upload an image..." accept=".jpg, 
                                                                            .jpeg, .png" />
            <input type="submit" />
          </form> 
        </body>
      </html>
    """;

    return Results.Content(html, "text/html");
});

app.MapPost("/upload", async Task<Results<Ok<string>,
   BadRequest<string>>> (IFormFile file, HttpContext context, IAntiforgery antiforgery) =>
{
    var fileSaveName = Guid.NewGuid().ToString("N") + Path.GetExtension(file.FileName);
    await UploadFileWithName(file, fileSaveName);
    return TypedResults.Ok("File uploaded successfully!");
});

app.Run();

XSRF 공격에 대한 자세한 내용은 최소 API를 사용한 위조 방지를 참조 하세요.

자세한 내용은 최소 API의 양식 바인딩을 참조 하세요.

양식에서 컬렉션 및 복합 형식에 바인딩

바인딩은 다음에서 지원됩니다.

  • 컬렉션(예: 목록사전)
  • 복합 형식(예: TodoProject

코드 내용은 다음과 같습니다.

  • 여러 부분으로 구성된 양식 입력을 복합 개체에 바인딩하는 최소 엔드포인트입니다.
  • 위조 방지 서비스를 사용하여 위조 방지 토큰의 생성 및 유효성 검사를 지원하는 방법입니다.
using Microsoft.AspNetCore.Antiforgery;
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.AspNetCore.Mvc;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddAntiforgery();

var app = builder.Build();

app.UseAntiforgery();

app.MapGet("/", (HttpContext context, IAntiforgery antiforgery) =>
{
    var token = antiforgery.GetAndStoreTokens(context);
    var html = $"""
        <html><body>
           <form action="/todo" method="POST" enctype="multipart/form-data">
               <input name="{token.FormFieldName}" 
                                type="hidden" value="{token.RequestToken}" />
               <input type="text" name="name" />
               <input type="date" name="dueDate" />
               <input type="checkbox" name="isCompleted" value="true" />
               <input type="submit" />
               <input name="isCompleted" type="hidden" value="false" /> 
           </form>
        </body></html>
    """;
    return Results.Content(html, "text/html");
});

app.MapPost("/todo", async Task<Results<Ok<Todo>, BadRequest<string>>> 
               ([FromForm] Todo todo, HttpContext context, IAntiforgery antiforgery) =>
{
    try
    {
        await antiforgery.ValidateRequestAsync(context);
        return TypedResults.Ok(todo);
    }
    catch (AntiforgeryValidationException e)
    {
        return TypedResults.BadRequest("Invalid anti-forgery token");
    }
});

app.Run();

class Todo
{
    public string Name { get; set; } = string.Empty;
    public bool IsCompleted { get; set; } = false;
    public DateTime DueDate { get; set; } = DateTime.Now.Add(TimeSpan.FromDays(1));
}

위의 코드에서

  • ON 본문에서 읽어야 하는 매개 변수를 구분하려면 대상 매개 변수에 특성을 주석 [FromForm] 으로 추가해야 합니다.JS
  • 요청 대리자 생성기를 사용하여 컴파일되는 최소 API에는 복합 또는 컬렉션 형식 의 바인딩이 지원되지 않습니다 .
  • 태그는 이름과 isCompletedfalse이 있는 추가 숨겨진 입력을 표시합니다. 양식이 isCompleted 제출될 때 검사box가 검사 경우 두 값 모두 값 truefalse 으로 제출됩니다. 검사box가 un검사이면 숨겨진 입력 값 false 만 제출됩니다. ASP.NET Core 모델 바인딩 프로세스는 bool 값에 바인딩할 때 첫 번째 값만 읽으므로 선택된 확인란에 대해 true 및 선택하지 않은 확인란에 대해 false로 나타납니다.

이전 엔드포인트에 제출된 양식 데이터의 예는 다음과 같습니다.

__RequestVerificationToken: CfDJ8Bveip67DklJm5vI2PF2VOUZ594RC8kcGWpTnVV17zCLZi1yrs-CSz426ZRRrQnEJ0gybB0AD7hTU-0EGJXDU-OaJaktgAtWLIaaEWMOWCkoxYYm-9U9eLV7INSUrQ6yBHqdMEE_aJpD4AI72gYiCqc
name: Walk the dog
dueDate: 2024-04-06
isCompleted: true
isCompleted: false

헤더 및 쿼리 문자열에서 배열 및 문자열 값 바인딩

다음 코드는 기본 형식의 배열, 문자열 배열 및 StringValues에 쿼리 문자열을 바인딩하는 방법을 보여 줍니다.

// Bind query string values to a primitive type array.
// GET  /tags?q=1&q=2&q=3
app.MapGet("/tags", (int[] q) =>
                      $"tag1: {q[0]} , tag2: {q[1]}, tag3: {q[2]}");

// Bind to a string array.
// GET /tags2?names=john&names=jack&names=jane
app.MapGet("/tags2", (string[] names) =>
            $"tag1: {names[0]} , tag2: {names[1]}, tag3: {names[2]}");

// Bind to StringValues.
// GET /tags3?names=john&names=jack&names=jane
app.MapGet("/tags3", (StringValues names) =>
            $"tag1: {names[0]} , tag2: {names[1]}, tag3: {names[2]}");

복합 형식의 배열에 쿼리 문자열 또는 헤더 값 바인딩은 형식에 TryParse가 구현된 경우에 지원됩니다. 다음 코드는 문자열 배열에 바인딩되며 지정된 태그가 있는 모든 항목을 반환합니다.

// GET /todoitems/tags?tags=home&tags=work
app.MapGet("/todoitems/tags", async (Tag[] tags, TodoDb db) =>
{
    return await db.Todos
        .Where(t => tags.Select(i => i.Name).Contains(t.Tag.Name))
        .ToListAsync();
});

다음 코드는 모델 및 필요한 TryParse 구현을 보여 줍니다.

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

    // This is an owned entity. 
    public Tag Tag { get; set; } = new();
}

[Owned]
public class Tag
{
    public string? Name { get; set; } = "n/a";

    public static bool TryParse(string? name, out Tag tag)
    {
        if (name is null)
        {
            tag = default!;
            return false;
        }

        tag = new Tag { Name = name };
        return true;
    }
}

다음 코드는 int 배열에 바인딩됩니다.

// GET /todoitems/query-string-ids?ids=1&ids=3
app.MapGet("/todoitems/query-string-ids", async (int[] ids, TodoDb db) =>
{
    return await db.Todos
        .Where(t => ids.Contains(t.Id))
        .ToListAsync();
});

위의 코드를 테스트하려면 다음 엔드포인트를 추가하여 데이터베이스를 Todo 항목으로 채웁니다.

// POST /todoitems/batch
app.MapPost("/todoitems/batch", async (Todo[] todos, TodoDb db) =>
{
    await db.Todos.AddRangeAsync(todos);
    await db.SaveChangesAsync();

    return Results.Ok(todos);
});

다음과 같은 HttpRepl 도구를 사용하여 이전 엔드포인트에 다음 데이터를 전달합니다.

[
    {
        "id": 1,
        "name": "Have Breakfast",
        "isComplete": true,
        "tag": {
            "name": "home"
        }
    },
    {
        "id": 2,
        "name": "Have Lunch",
        "isComplete": true,
        "tag": {
            "name": "work"
        }
    },
    {
        "id": 3,
        "name": "Have Supper",
        "isComplete": true,
        "tag": {
            "name": "home"
        }
    },
    {
        "id": 4,
        "name": "Have Snacks",
        "isComplete": true,
        "tag": {
            "name": "N/A"
        }
    }
]

다음 코드는 헤더 키 X-Todo-Id에 바인딩되고 Id 값이 일치하는 Todo 항목을 반환합니다.

// GET /todoitems/header-ids
// The keys of the headers should all be X-Todo-Id with different values
app.MapGet("/todoitems/header-ids", async ([FromHeader(Name = "X-Todo-Id")] int[] ids, TodoDb db) =>
{
    return await db.Todos
        .Where(t => ids.Contains(t.Id))
        .ToListAsync();
});

참고 항목

쿼리 문자열에서 바인딩 string[] 할 때 일치하는 쿼리 문자열 값이 없으면 null 값 대신 빈 배열이 생성됩니다.

[AsParameters]를 사용하여 인수 목록에 대한 매개 변수 바인딩

AsParametersAttribute는 유형에 대한 매개 변수 바인딩을 활성화하며 복합 또는 재귀 모델 바인딩은 활성화하지 않습니다.

다음 코드를 생각해 봅시다.

using Microsoft.EntityFrameworkCore;
using TodoApi.Models;

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());
// Remaining code removed for brevity.

다음 GET 엔드포인트를 살펴보세요.

app.MapGet("/todoitems/{id}",
                             async (int Id, TodoDb Db) =>
    await Db.Todos.FindAsync(Id)
        is Todo todo
            ? Results.Ok(new TodoItemDTO(todo))
            : Results.NotFound());

다음 struct는 강조 표시된 위 매개 변수를 바꾸는 데 사용할 수 있습니다.

struct TodoItemRequest
{
    public int Id { get; set; }
    public TodoDb Db { get; set; }
}

리팩터링된 GET 엔드포인트는 위의 structAsParameters 특성과 함께 사용합니다.

app.MapGet("/ap/todoitems/{id}",
                                async ([AsParameters] TodoItemRequest request) =>
    await request.Db.Todos.FindAsync(request.Id)
        is Todo todo
            ? Results.Ok(new TodoItemDTO(todo))
            : Results.NotFound());

다음 코드는 앱의 추가 엔드포인트를 보여 줍니다.

app.MapPost("/todoitems", async (TodoItemDTO Dto, TodoDb Db) =>
{
    var todoItem = new Todo
    {
        IsComplete = Dto.IsComplete,
        Name = Dto.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 Dto, TodoDb Db) =>
{
    var todo = await Db.Todos.FindAsync(Id);

    if (todo is null) return Results.NotFound();

    todo.Name = Dto.Name;
    todo.IsComplete = Dto.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.Ok(new TodoItemDTO(todo));
    }

    return Results.NotFound();
});

다음 클래스는 매개 변수 목록을 리팩터링하는 데 사용합니다.

class CreateTodoItemRequest
{
    public TodoItemDTO Dto { get; set; } = default!;
    public TodoDb Db { get; set; } = default!;
}

class EditTodoItemRequest
{
    public int Id { get; set; }
    public TodoItemDTO Dto { get; set; } = default!;
    public TodoDb Db { get; set; } = default!;
}

다음 코드는 AsParameters를 사용하는 리팩터링된 엔드포인트와 위의 struct 및 클래스를 보여 줍니다.

app.MapPost("/ap/todoitems", async ([AsParameters] CreateTodoItemRequest request) =>
{
    var todoItem = new Todo
    {
        IsComplete = request.Dto.IsComplete,
        Name = request.Dto.Name
    };

    request.Db.Todos.Add(todoItem);
    await request.Db.SaveChangesAsync();

    return Results.Created($"/todoitems/{todoItem.Id}", new TodoItemDTO(todoItem));
});

app.MapPut("/ap/todoitems/{id}", async ([AsParameters] EditTodoItemRequest request) =>
{
    var todo = await request.Db.Todos.FindAsync(request.Id);

    if (todo is null) return Results.NotFound();

    todo.Name = request.Dto.Name;
    todo.IsComplete = request.Dto.IsComplete;

    await request.Db.SaveChangesAsync();

    return Results.NoContent();
});

app.MapDelete("/ap/todoitems/{id}", async ([AsParameters] TodoItemRequest request) =>
{
    if (await request.Db.Todos.FindAsync(request.Id) is Todo todo)
    {
        request.Db.Todos.Remove(todo);
        await request.Db.SaveChangesAsync();
        return Results.Ok(new TodoItemDTO(todo));
    }

    return Results.NotFound();
});

다음 record 형식을 사용하면 위의 매개 변수를 바꿀 수 있습니다.

record TodoItemRequest(int Id, TodoDb Db);
record CreateTodoItemRequest(TodoItemDTO Dto, TodoDb Db);
record EditTodoItemRequest(int Id, TodoItemDTO Dto, TodoDb Db);

structAsParameters와 함께 사용하면 record 유형보다 성능이 개선될 수 있습니다.

완전한 샘플 코드AspNetCore.Docs.Samples 리포지토리에 있습니다.

사용자 지정 바인딩

매개 변수 바인딩을 사용자 지정하는 방법에는 다음 두 가지가 있습니다.

  1. 경로, 쿼리, 헤더 바인딩 소스의 경우 형식의 정적 TryParse 메서드를 추가하여 사용자 지정 형식을 바인딩합니다.
  2. 형식에서 BindAsync 메서드를 구현하여 바인딩 프로세스를 제어합니다.

TryParse

TryParse에는 다음 두 가지 API가 있습니다.

public static bool TryParse(string value, out T result);
public static bool TryParse(string value, IFormatProvider provider, out T result);

다음 코드는 URI가 /map?Point=12.3,10.1Point: 12.3, 10.1을 표시합니다.

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

// GET /map?Point=12.3,10.1
app.MapGet("/map", (Point point) => $"Point: {point.X}, {point.Y}");

app.Run();

public class Point
{
    public double X { get; set; }
    public double Y { get; set; }

    public static bool TryParse(string? value, IFormatProvider? provider,
                                out Point? point)
    {
        // Format is "(12.3,10.1)"
        var trimmedValue = value?.TrimStart('(').TrimEnd(')');
        var segments = trimmedValue?.Split(',',
                StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
        if (segments?.Length == 2
            && double.TryParse(segments[0], out var x)
            && double.TryParse(segments[1], out var y))
        {
            point = new Point { X = x, Y = y };
            return true;
        }

        point = null;
        return false;
    }
}

BindAsync

BindAsync에는 다음 API가 있습니다.

public static ValueTask<T?> BindAsync(HttpContext context, ParameterInfo parameter);
public static ValueTask<T?> BindAsync(HttpContext context);

다음 코드는 URI가 /products?SortBy=xyz&SortDir=Desc&Page=99SortBy:xyz, SortDirection:Desc, CurrentPage:99을 표시합니다.

using System.Reflection;

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

// GET /products?SortBy=xyz&SortDir=Desc&Page=99
app.MapGet("/products", (PagingData pageData) => $"SortBy:{pageData.SortBy}, " +
       $"SortDirection:{pageData.SortDirection}, CurrentPage:{pageData.CurrentPage}");

app.Run();

public class PagingData
{
    public string? SortBy { get; init; }
    public SortDirection SortDirection { get; init; }
    public int CurrentPage { get; init; } = 1;

    public static ValueTask<PagingData?> BindAsync(HttpContext context,
                                                   ParameterInfo parameter)
    {
        const string sortByKey = "sortBy";
        const string sortDirectionKey = "sortDir";
        const string currentPageKey = "page";

        Enum.TryParse<SortDirection>(context.Request.Query[sortDirectionKey],
                                     ignoreCase: true, out var sortDirection);
        int.TryParse(context.Request.Query[currentPageKey], out var page);
        page = page == 0 ? 1 : page;

        var result = new PagingData
        {
            SortBy = context.Request.Query[sortByKey],
            SortDirection = sortDirection,
            CurrentPage = page
        };

        return ValueTask.FromResult<PagingData?>(result);
    }
}

public enum SortDirection
{
    Default,
    Asc,
    Desc
}

바인딩 실패

바인딩이 실패할 경우 프레임워크는 디버그 메시지를 로그하고 실패 모드에 따라 다양한 상태 코드를 클라이언트에 반환합니다.

실패 모드 null 허용 매개 변수 형식 바인딩 소스 상태 코드
{ParameterType}.TryParsefalse를 반환합니다 route/query/header 400
{ParameterType}.BindAsyncnull를 반환합니다 custom 400
{ParameterType}.BindAsync가 throw 상관없다 custom 500
JSON 본문을 역직렬화하지 못함 상관없다 본문 400
잘못된 콘텐츠 형식(application/json이 아님) 상관없다 본문 415

바인딩 우선 순위

매개 변수에서 바인딩 소스를 결정하는 규칙:

  1. 매개 변수(From* 특성)에 다음 순서로 정의된 명시적 특성:
    1. 경로 값: [FromRoute]
    2. 쿼리 문자열: [FromQuery]
    3. 헤더: [FromHeader]
    4. 본문: [FromBody]
    5. 양식: [FromForm]
    6. 서비스: [FromServices]
    7. 매개 변수 값: [AsParameters]
  2. 특수 형식
    1. HttpContext
    2. HttpRequest (HttpContext.Request)
    3. HttpResponse (HttpContext.Response)
    4. ClaimsPrincipal (HttpContext.User)
    5. CancellationToken (HttpContext.RequestAborted)
    6. IFormCollection (HttpContext.Request.Form)
    7. IFormFileCollection (HttpContext.Request.Form.Files)
    8. IFormFile (HttpContext.Request.Form.Files[paramName])
    9. Stream (HttpContext.Request.Body)
    10. PipeReader (HttpContext.Request.BodyReader)
  3. 매개 변수 형식에 유효한 정적 BindAsync 메서드가 있습니다.
  4. 매개 변수 형식이 문자열이거나 매개 변수 형식에 유효한 정적 TryParse 메서드가 있습니다.
    1. 예를 들어 app.Map("/todo/{id}", (int id) => {});매개 변수 이름이 경로 템플릿에 있는 경우 경로에서 바인딩됩니다.
    2. 쿼리 문자열에서 바인딩됩니다.
  5. 매개 변수 형식이 종속성 주입에서 제공되는 서비스인 경우 해당 서비스를 원본으로 사용합니다.
  6. 매개 변수가 본문의 매개 변수입니다.

본문 바인딩에 대한 ON 역직렬화 옵션 구성 JS

본문 바인딩 소스는 역직렬화에 사용합니다 System.Text.Json . 이 기본값은 변경할 수 없지만 JSON serialization 및 deserialization 옵션을 구성할 수 있습니다.

전역적으로 ON 역직렬화 옵션 구성 JS

앱에 대해 전역적으로 적용되는 옵션은 호출하여 ConfigureHttpJsonOptions구성할 수 있습니다. 다음 예제에는 공용 필드 및 ON 출력 형식이 JS포함됩니다.

var builder = WebApplication.CreateBuilder(args);

builder.Services.ConfigureHttpJsonOptions(options => {
    options.SerializerOptions.WriteIndented = true;
    options.SerializerOptions.IncludeFields = true;
});

var app = builder.Build();

app.MapPost("/", (Todo todo) => {
    if (todo is not null) {
        todo.Name = todo.NameField;
    }
    return todo;
});

app.Run();

class Todo {
    public string? Name { get; set; }
    public string? NameField;
    public bool IsComplete { get; set; }
}
// If the request body contains the following JSON:
//
// {"nameField":"Walk dog", "isComplete":false}
//
// The endpoint returns the following JSON:
//
// {
//    "name":"Walk dog",
//    "nameField":"Walk dog",
//    "isComplete":false
// }

샘플 코드는 serialization 및 deserialization을 모두 구성하므로 출력 JSON에서 읽고 NameField 포함 NameField 할 수 있습니다.

엔드포인트에 대한 ON 역직렬화 옵션 구성 JS

ReadFromJsonAsync 에는 개체를 허용하는 오버로드가 있습니다 JsonSerializerOptions . 다음 예제에는 공용 필드 및 ON 출력 형식이 JS포함됩니다.

using System.Text.Json;

var app = WebApplication.Create();

var options = new JsonSerializerOptions(JsonSerializerDefaults.Web) { 
    IncludeFields = true, 
    WriteIndented = true
};

app.MapPost("/", async (HttpContext context) => {
    if (context.Request.HasJsonContentType()) {
        var todo = await context.Request.ReadFromJsonAsync<Todo>(options);
        if (todo is not null) {
            todo.Name = todo.NameField;
        }
        return Results.Ok(todo);
    }
    else {
        return Results.BadRequest();
    }
});

app.Run();

class Todo
{
    public string? Name { get; set; }
    public string? NameField;
    public bool IsComplete { get; set; }
}
// If the request body contains the following JSON:
//
// {"nameField":"Walk dog", "isComplete":false}
//
// The endpoint returns the following JSON:
//
// {
//    "name":"Walk dog",
//    "isComplete":false
// }

앞의 코드는 역직렬화에만 사용자 지정된 옵션을 적용하므로 출력 JSON은 제외됩니다 NameField.

요청 본문 읽기

HttpContext 또는 HttpRequest 매개 변수를 사용하여 직접 요청 본문을 읽습니다.

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

app.MapPost("/uploadstream", async (IConfiguration config, HttpRequest request) =>
{
    var filePath = Path.Combine(config["StoredFilesPath"], Path.GetRandomFileName());

    await using var writeStream = File.Create(filePath);
    await request.BodyReader.CopyToAsync(writeStream);
});

app.Run();

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

  • HttpRequest.BodyReader를 사용하여 요청 본문에 액세스합니다.
  • 요청 본문을 로컬 파일에 복사합니다.

응답

경로 처리기는 다음과 같은 형식의 반환 값을 지원합니다.

  1. IResult 기반 - 여기에는 Task<IResult>ValueTask<IResult>가 포함됩니다.
  2. string - 여기에는 Task<string>ValueTask<string>이 포함됩니다.
  3. T(다른 모든 형식) - 여기에는 Task<T>ValueTask<T>가 포함됩니다.
반환 값 동작 콘텐츠-형식
IResult 프레임워크가 IResult.ExecuteAsync를 호출합니다. IResult 구현에 의해 결정됩니다.
string 프레임워크가 응답에 직접 문자열을 씁니다. text/plain
T(기타 형식) 프레임워크 JS는 응답을 ON 직렬화합니다. application/json

처리기 반환 값을 라우팅하는 자세한 가이드는 최소 API 애플리케이션에서 응답 만들기를 참조하세요.

예시 반환 값

문자열 반환 값

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

JSON 반환 값

app.MapGet("/hello", () => new { Message = "Hello World" });

TypedResults 반환

다음 코드는 다음을 반환합니다.TypedResults

app.MapGet("/hello", () => TypedResults.Ok(new Message() {  Text = "Hello World!" }));

반환은 TypedResults 반환하는 Results것이 좋습니다. 자세한 내용은 TypedResults 및 결과를 참조하세요.

IResult 반환 값

app.MapGet("/hello", () => Results.Ok(new { Message = "Hello World" }));

다음 예는 기본 제공 결과 형식을 사용하여 응답을 사용자 지정합니다.

app.MapGet("/api/todoitems/{id}", async (int id, TodoDb db) =>
         await db.Todos.FindAsync(id) 
         is Todo todo
         ? Results.Ok(todo) 
         : Results.NotFound())
   .Produces<Todo>(StatusCodes.Status200OK)
   .Produces(StatusCodes.Status404NotFound);

JS에

app.MapGet("/hello", () => Results.Json(new { Message = "Hello World" }));

사용자 지정 상태 코드

app.MapGet("/405", () => Results.StatusCode(405));

Text

app.MapGet("/text", () => Results.Text("This is some text"));

스트림

var proxyClient = new HttpClient();
app.MapGet("/pokemon", async () => 
{
    var stream = await proxyClient.GetStreamAsync("http://contoso/pokedex.json");
    // Proxy the response as JSON
    return Results.Stream(stream, "application/json");
});

자세한 예제는 최소 API 애플리케이션에서 응답 만들기 를 참조하세요.

리디렉션

app.MapGet("/old-path", () => Results.Redirect("/new-path"));

파일

app.MapGet("/download", () => Results.File("myfile.text"));

기본 제공 결과

일반적인 결과 도우미는 정적 클래스 및 TypedResults 정적 클래스에 Results 있습니다. 반환은 TypedResults 반환하는 Results것이 좋습니다. 자세한 내용은 TypedResults 및 결과를 참조하세요.

결과 사용자 지정

애플리케이션은 사용자 지정 IResult 형식을 구현하여 응답을 제어할 수 있습니다. 다음 코드는 HTML 결과 형식의 예입니다.

using System.Net.Mime;
using System.Text;
static class ResultsExtensions
{
    public static IResult Html(this IResultExtensions resultExtensions, string html)
    {
        ArgumentNullException.ThrowIfNull(resultExtensions);

        return new HtmlResult(html);
    }
}

class HtmlResult : IResult
{
    private readonly string _html;

    public HtmlResult(string html)
    {
        _html = html;
    }

    public Task ExecuteAsync(HttpContext httpContext)
    {
        httpContext.Response.ContentType = MediaTypeNames.Text.Html;
        httpContext.Response.ContentLength = Encoding.UTF8.GetByteCount(_html);
        return httpContext.Response.WriteAsync(_html);
    }
}

이러한 사용자 지정 결과를 더 검색 가능하게 만들려면 Microsoft.AspNetCore.Http.IResultExtensions에 확장 메서드를 추가하는 것이 좋습니다.

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

app.MapGet("/html", () => Results.Extensions.Html(@$"<!doctype html>
<html>
    <head><title>miniHTML</title></head>
    <body>
        <h1>Hello World</h1>
        <p>The time on the server is {DateTime.Now:O}</p>
    </body>
</html>"));

app.Run();

형식화된 결과

IResult 인터페이스는 반환된 개체를 HTTP 응답으로 직렬화하는 JSON에 대한 암시적 지원을 활용하지 않는 최소 API에서 반환된 값을 나타낼 수 있습니다. 정적 Results 클래스는 다양한 형식의 응답을 나타내는 다양한 IResult 개체를 만드는 데 사용됩니다. 예를 들어 응답 상태 코드를 설정하거나 다른 URL로 리디렉션합니다.

IResult를 구현하는 형식은 공용이므로 테스트할 때 형식 어설션을 허용합니다. 예시:

[TestClass()]
public class WeatherApiTests
{
    [TestMethod()]
    public void MapWeatherApiTest()
    {
        var result = WeatherApi.GetAllWeathers();
        Assert.IsInstanceOfType(result, typeof(Ok<WeatherForecast[]>));
    }      
}

정적 TypedResults 클래스에서 해당 메서드의 반환 형식을 확인하여 캐스팅할 올바른 퍼블릭 IResult 형식을 찾을 수 있습니다.

자세한 예제는 최소 API 애플리케이션에서 응답 만들기 를 참조하세요.

필터

참조

권한 부여

권한 부여 정책을 사용하여 경로를 보호할 수 있습니다. 권한 부여 정책은 [Authorize] 특성을 통해 또는 RequireAuthorization 메서드를 사용하여 선언할 수 있습니다.

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using WebRPauth.Data;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthorization(o => o.AddPolicy("AdminsOnly", 
                                  b => b.RequireClaim("admin", "true")));

var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();

builder.Services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
    .AddEntityFrameworkStores<ApplicationDbContext>();

var app = builder.Build();

app.UseAuthorization();

app.MapGet("/auth", [Authorize] () => "This endpoint requires authorization.");
app.MapGet("/", () => "This endpoint doesn't require authorization.");
app.MapGet("/Identity/Account/Login", () => "Sign in page at this endpoint.");

app.Run();

이전 코드는 RequireAuthorization을 사용하여 작성할 수 있습니다.

app.MapGet("/auth", () => "This endpoint requires authorization")
   .RequireAuthorization();

다음 샘플은 정책 기반 권한 부여를 사용합니다.

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using WebRPauth.Data;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthorization(o => o.AddPolicy("AdminsOnly", 
                                  b => b.RequireClaim("admin", "true")));

var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();

builder.Services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
    .AddEntityFrameworkStores<ApplicationDbContext>();

var app = builder.Build();

app.UseAuthorization();

app.MapGet("/admin", [Authorize("AdminsOnly")] () => 
                             "The /admin endpoint is for admins only.");

app.MapGet("/admin2", () => "The /admin2 endpoint is for admins only.")
   .RequireAuthorization("AdminsOnly");

app.MapGet("/", () => "This endpoint doesn't require authorization.");
app.MapGet("/Identity/Account/Login", () => "Sign in page at this endpoint.");

app.Run();

인증되지 않은 사용자가 엔드포인트에 액세스할 수 있도록 허용

[AllowAnonymous]는 인증되지 않은 사용자가 엔드포인트에 액세스할 수 있도록 허용합니다.

app.MapGet("/login", [AllowAnonymous] () => "This endpoint is for all roles.");


app.MapGet("/login2", () => "This endpoint also for all roles.")
   .AllowAnonymous();

CORS

경로는 CORS 정책을 사용하여 CORS를 사용할 수 있습니다. CORS는 [EnableCors] 특성을 통해 또는 RequireCors 메서드를 사용하여 선언할 수 있습니다. 다음 샘플은 CORS를 사용하도록 설정합니다.

const string MyAllowSpecificOrigins = "_myAllowSpecificOrigins";

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddCors(options =>
{
    options.AddPolicy(name: MyAllowSpecificOrigins,
                      builder =>
                      {
                          builder.WithOrigins("http://example.com",
                                              "http://www.contoso.com");
                      });
});

var app = builder.Build();
app.UseCors();

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

app.Run();
using Microsoft.AspNetCore.Cors;

const string MyAllowSpecificOrigins = "_myAllowSpecificOrigins";

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddCors(options =>
{
    options.AddPolicy(name: MyAllowSpecificOrigins,
                      builder =>
                      {
                          builder.WithOrigins("http://example.com",
                                              "http://www.contoso.com");
                      });
});

var app = builder.Build();
app.UseCors();

app.MapGet("/cors", [EnableCors(MyAllowSpecificOrigins)] () => 
                           "This endpoint allows cross origin requests!");
app.MapGet("/cors2", () => "This endpoint allows cross origin requests!")
                     .RequireCors(MyAllowSpecificOrigins);

app.Run();

자세한 내용은 ASP.NET Core에서 CORS(원본 간 요청) 사용을 참조하세요.

ValidateScopes 및 ValidateOnBuild

ValidateScopes개발 ValidateOnBuild 환경에서는 기본적으로 사용하도록 설정되지만 다른 환경에서는 사용하지 않도록 설정됩니다.

이 경우 ValidateOnBuild DI 컨테이너는 true빌드 시 서비스 구성의 유효성을 검사합니다. 서비스 구성이 잘못된 경우 서비스가 요청된 런타임이 아닌 앱 시작 시 빌드가 실패합니다.

이 경우 ValidateScopes DI 컨테이너는 true범위가 지정된 서비스가 루트 범위에서 확인되지 않는지 확인합니다. 루트 범위에서 범위가 지정된 서비스를 해결하면 서비스가 요청 범위보다 더 오래 메모리에 유지되므로 메모리 누수가 발생할 수 있습니다.

ValidateScopes 성능 ValidateOnBuild 상의 이유로 비개발 모드에서는 기본적으로 false입니다.

다음 코드는 ValidateScopes 기본적으로 개발 모드에서 사용하도록 설정되어 있지만 릴리스 모드에서는 사용하지 않도록 설정되어 있습니다.

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddScoped<MyScopedService>();

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    Console.WriteLine("Development environment");
}
else
{
    Console.WriteLine("Release environment");
}

app.MapGet("/", context =>
{
    // Intentionally getting service provider from app, not from the request
    // This causes an exception from attempting to resolve a scoped service
    // outside of a scope.
    // Throws System.InvalidOperationException:
    // 'Cannot resolve scoped service 'MyScopedService' from root provider.'
    var service = app.Services.GetRequiredService<MyScopedService>();
    return context.Response.WriteAsync("Service resolved");
});

app.Run();

public class MyScopedService { }

다음 코드는 ValidateOnBuild 기본적으로 개발 모드에서 사용하도록 설정되어 있지만 릴리스 모드에서는 사용하지 않도록 설정되어 있습니다.

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddScoped<MyScopedService>();
builder.Services.AddScoped<AnotherService>();

// System.AggregateException: 'Some services are not able to be constructed (Error
// while validating the service descriptor 'ServiceType: AnotherService Lifetime:
// Scoped ImplementationType: AnotherService': Unable to resolve service for type
// 'BrokenService' while attempting to activate 'AnotherService'.)'
var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    Console.WriteLine("Development environment");
}
else
{
    Console.WriteLine("Release environment");
}

app.MapGet("/", context =>
{
    var service = context.RequestServices.GetRequiredService<MyScopedService>();
    return context.Response.WriteAsync("Service resolved correctly!");
});

app.Run();

public class MyScopedService { }

public class AnotherService
{
    public AnotherService(BrokenService brokenService) { }
}

public class BrokenService { }

다음 코드는 사용 안 함으로 설정됩니다 ValidateOnBuildDevelopment.ValidateScopes

var builder = WebApplication.CreateBuilder(args);

if (builder.Environment.IsDevelopment())
{
    Console.WriteLine("Development environment");
    // Doesn't detect the validation problems because ValidateScopes is false.
    builder.Host.UseDefaultServiceProvider(options =>
    {
        options.ValidateScopes = false;
        options.ValidateOnBuild = false;
    });
}

참고 항목

이 문서:

최소 API는 다음으로 구성됩니다.

WebApplication

다음 코드는 ASP.NET Core 템플릿에서 생성됩니다.

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

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

app.Run();

위의 코드는 명령줄에서 dotnet new web을 통해 만들거나 Visual Studio에서 빈 웹 템플릿을 선택하여 만들 수 있습니다.

다음 코드는 WebApplicationBuilder를 명시적으로 만들지 않고 WebApplication(app)을 만듭니다.

var app = WebApplication.Create(args);

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

app.Run();

WebApplication.Create는 미리 구성된 기본값을 사용하여 WebApplication 클래스의 새 인스턴스를 초기화합니다.

WebApplication 는 특정 조건에 따라 다음 미들웨어 Minimal API applications 를 자동으로 추가합니다.

  • HostingEnvironment"Development"일 때 UseDeveloperExceptionPage가 먼저 추가됩니다.
  • 사용자 코드가 아직 UseRouting를 호출하지 않은 경우 및 구성된 엔드포인트가 있는 경우(예: app.MapGet), UseRouting가 두 번째로 추가됩니다.
  • UseEndpoints는 엔드포인트가 구성된 경우 미들웨어 파이프라인의 끝에 추가됩니다.
  • UseAuthentication은 사용자 코드가 아직 호출 UseAuthentication 되지 않은 경우와 서비스 공급자에서 검색할 수 있는 경우 IAuthenticationSchemeProvider 즉시 UseRouting 추가됩니다. IAuthenticationSchemeProviderAddAuthentication를 사용할 때 기본적으로 추가되고 IServiceProviderIsService를 사용하여 서비스가 검색됩니다.
  • UseAuthorization 는 사용자 코드가 아직 호출 UseAuthorization 되지 않았고 서비스 공급자에서 검색할 수 있는 경우 IAuthorizationHandlerProvider 다음에 추가됩니다. IAuthorizationHandlerProviderAddAuthorization를 사용할 때 기본적으로 추가되고 IServiceProviderIsService를 사용하여 서비스가 검색됩니다.
  • 사용자 구성 미들웨어 및 엔드포인트는 UseRoutingUseEndpoints 사이에 추가됩니다.

다음 코드는 앱에 추가되는 자동 미들웨어가 생성하는 것입니다.

if (isDevelopment)
{
    app.UseDeveloperExceptionPage();
}

app.UseRouting();

if (isAuthenticationConfigured)
{
    app.UseAuthentication();
}

if (isAuthorizationConfigured)
{
    app.UseAuthorization();
}

// user middleware/endpoints
app.CustomMiddleware(...);
app.MapGet("/", () => "hello world");
// end user middleware/endpoints

app.UseEndpoints(e => {});

경우에 따라 앱에 대한 기본 미들웨어 구성이 올바르지 않으며 수정이 필요합니다. 예를 들어, UseCorsUseAuthenticationUseAuthorization 전에 호출되어야 합니다. 앱은 호출 UseAuthentication 해야 하며 UseAuthorization 호출되는 경우 UseCors :

app.UseCors();
app.UseAuthentication();
app.UseAuthorization();

경로 일치가 발생하기 전에 미들웨어를 실행해야 하는 경우, UseRouting를 호출해야 하며 UseRouting에 대한 호출 전에 미들웨어를 배치해야 합니다. UseEndpoints 는 앞에서 설명한 대로 자동으로 추가되므로 이 경우에는 필요하지 않습니다.

app.Use((context, next) =>
{
    return next(context);
});

app.UseRouting();

// other middleware and endpoints

터미널 미들웨어를 추가하는 경우:

  • 미들웨어는 UseEndpoints 다음에 추가해야 합니다.
  • 터미널 미들웨어를 올바른 위치에 배치할 수 있도록 앱은 UseRoutingUseEndpoints를 호출해야 합니다.
app.UseRouting();

app.MapGet("/", () => "hello world");

app.UseEndpoints(e => {});

app.Run(context =>
{
    context.Response.StatusCode = 404;
    return Task.CompletedTask;
});

터미널 미들웨어는 요청을 처리하는 엔드포인트가 없는 경우 실행되는 미들웨어입니다.

포트 작업

Visual Studio 또는 dotnet new를 사용하여 웹앱을 만들면 앱이 응답하는 포트를 지정하는 Properties/launchSettings.json 파일이 만들어집니다. 다음에 오는 포트 설정 샘플에서는 Visual Studio에서 앱을 실행하면 Unable to connect to web server 'AppName' 오류 대화 상자가 반환됩니다. Visual Studio는 Properties/launchSettings.json에 지정된 포트가 예상되기 때문에 오류를 반환하지만 앱은 app.Run("http://localhost:3000")에서 지정한 포트를 사용하고 있습니다. 명령줄에서 다음 포트 변경 샘플을 실행합니다.

다음 섹션에서는 앱이 응답하는 포트를 설정합니다.

var app = WebApplication.Create(args);

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

app.Run("http://localhost:3000");

위의 코드에서 앱은 포트 3000에 응답합니다.

여러 포트

위의 코드에서 앱은 포트 30004000에 응답합니다.

var app = WebApplication.Create(args);

app.Urls.Add("http://localhost:3000");
app.Urls.Add("http://localhost:4000");

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

app.Run();

명령줄에서 포트 설정

다음 명령은 앱이 포트 7777에 응답하도록 합니다.

dotnet run --urls="https://localhost:7777"

Kestrel 엔드포인트가 appsettings.json 파일에도 구성된 경우 appsettings.json 파일에서 지정된 URL이 사용됩니다. 자세한 내용은 Kestrel 엔드포인트 구성을 참조하세요.

환경에서 포트 읽기

다음 코드는 환경에서 포트를 읽습니다.

var app = WebApplication.Create(args);

var port = Environment.GetEnvironmentVariable("PORT") ?? "3000";

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

app.Run($"http://localhost:{port}");

환경에서 포트를 설정하는 기본 방법은 다음 섹션에 나오는 ASPNETCORE_URLS 환경 변수를 사용하는 것입니다.

ASPNETCORE_URLS 환경 변수를 통해 포트 설정

ASPNETCORE_URLS 환경 변수를 사용하여 포트를 설정할 수 있습니다.

ASPNETCORE_URLS=http://localhost:3000

ASPNETCORE_URLS는 여러 URL을 지원합니다.

ASPNETCORE_URLS=http://localhost:3000;https://localhost:5000

환경 사용에 대한 자세한 내용은 ASP.NET Core에서 여러 환경 사용을 참조하세요.

모든 인터페이스에서 수신 대기

다음 샘플은 모든 인터페이스에서의 수신 대기를 보여 줍니다.

http://*:3000

var app = WebApplication.Create(args);

app.Urls.Add("http://*:3000");

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

app.Run();

http://+:3000

var app = WebApplication.Create(args);

app.Urls.Add("http://+:3000");

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

app.Run();

http://0.0.0.0:3000

var app = WebApplication.Create(args);

app.Urls.Add("http://0.0.0.0:3000");

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

app.Run();

ASPNETCORE_URLS를 사용하여 모든 인터페이스에서 수신 대기

이전 샘플은 ASPNETCORE_URLS를 사용할 수 있습니다.

ASPNETCORE_URLS=http://*:3000;https://+:5000;http://0.0.0.0:5005

개발 인증서로 HTTPS 지정

var app = WebApplication.Create(args);

app.Urls.Add("https://localhost:3000");

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

app.Run();

개발 인증서에 대한 자세한 내용은 Windows 및 macOS에서 ASP.NET Core HTTPS 개발 인증서 신뢰를 참조하세요.

사용자 지정 인증서를 사용하여 HTTPS 지정

다음 섹션에서는 파일 및 구성을 통해 사용자 지정 인증서를 지정하는 appsettings.json 방법을 보여 있습니다.

appsettings.json을 사용하여 사용자 지정 인증서 지정

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "Kestrel": {
    "Certificates": {
      "Default": {
        "Path": "cert.pem",
        "KeyPath": "key.pem"
      }
    }
  }
}

구성을 통해 사용자 지정 인증서 지정

var builder = WebApplication.CreateBuilder(args);

// Configure the cert and the key
builder.Configuration["Kestrel:Certificates:Default:Path"] = "cert.pem";
builder.Configuration["Kestrel:Certificates:Default:KeyPath"] = "key.pem";

var app = builder.Build();

app.Urls.Add("https://localhost:3000");

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

app.Run();

인증서 API 사용

using System.Security.Cryptography.X509Certificates;

var builder = WebApplication.CreateBuilder(args);

builder.WebHost.ConfigureKestrel(options =>
{
    options.ConfigureHttpsDefaults(httpsOptions =>
    {
        var certPath = Path.Combine(builder.Environment.ContentRootPath, "cert.pem");
        var keyPath = Path.Combine(builder.Environment.ContentRootPath, "key.pem");

        httpsOptions.ServerCertificate = X509Certificate2.CreateFromPemFile(certPath, 
                                         keyPath);
    });
});

var app = builder.Build();

app.Urls.Add("https://localhost:3000");

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

app.Run();

구성

다음 코드는 구성 시스템에서 읽습니다.

var app = WebApplication.Create(args);

var message = app.Configuration["HelloKey"] ?? "Config failed!";

app.MapGet("/", () => message);

app.Run();

자세한 내용은 ASP.NET Core의 구성을 참조하세요.

로깅

다음 코드는 로그온 애플리케이션 시작에 메시지를 씁니다.

var app = WebApplication.Create(args);

app.Logger.LogInformation("The app started");

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

app.Run();

자세한 내용은 .NET Core 및 ASP.NET Core의 로깅을 참조하세요.

DI(종속성 주입) 컨테이너 액세스

다음 코드에서는 애플리케이션을 시작하는 동안 DI 컨테이너에서 서비스를 가져오는 방법을 보여줍니다.


var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();
builder.Services.AddScoped<SampleService>();

var app = builder.Build();

app.MapControllers();

using (var scope = app.Services.CreateScope())
{
    var sampleService = scope.ServiceProvider.GetRequiredService<SampleService>();
    sampleService.DoSomething();
}

app.Run();

자세한 내용은 ASP.NET Core에서 종속성 주입을 참조하세요.

WebApplicationBuilder

이 섹션에는 WebApplicationBuilder를 사용하는 샘플 코드가 포함되어 있습니다.

콘텐츠 루트, 애플리케이션 이름, 환경 변경

다음 코드는 콘텐츠 루트, 애플리케이션 이름, 환경을 설정합니다.

var builder = WebApplication.CreateBuilder(new WebApplicationOptions
{
    Args = args,
    ApplicationName = typeof(Program).Assembly.FullName,
    ContentRootPath = Directory.GetCurrentDirectory(),
    EnvironmentName = Environments.Staging,
    WebRootPath = "customwwwroot"
});

Console.WriteLine($"Application Name: {builder.Environment.ApplicationName}");
Console.WriteLine($"Environment Name: {builder.Environment.EnvironmentName}");
Console.WriteLine($"ContentRoot Path: {builder.Environment.ContentRootPath}");
Console.WriteLine($"WebRootPath: {builder.Environment.WebRootPath}");

var app = builder.Build();

WebApplication.CreateBuilder는 미리 구성된 기본값을 사용하여 WebApplicationBuilder 클래스의 새 인스턴스를 초기화합니다.

자세한 내용은 ASP.NET Core 기본 사항 개요를 참조하세요

환경 변수 또는 명령줄을 통해 콘텐츠 루트, 앱 이름, 환경 변경

다음 표는 콘텐츠 루트, 앱 이름, 환경 변경에 사용되는 환경 변수와 명령줄 인수를 보여 줍니다.

기능 환경 변수 명령줄 인수
애플리케이션 이름 ASPNETCORE_APPLICATIONNAME --applicationName
환경 이름 ASPNETCORE_ENVIRONMENT --environment
콘텐츠 루트 ASPNETCORE_CONTENTROOT --contentRoot

구성 공급자 추가

다음 샘플은 INI 구성 공급자를 추가합니다.

var builder = WebApplication.CreateBuilder(args);

builder.Configuration.AddIniFile("appsettings.ini");

var app = builder.Build();

자세한 내용은 ASP.NET Core의 구성에서 파일 구성 공급자를 참조하세요.

구성 읽기

기본적으로 WebApplicationBuilder는 다음을 포함하여 여러 원본에서 구성을 읽습니다.

  • appSettings.jsonappSettings.{environment}.json
  • 환경 변수
  • 명령줄

다음 코드는 구성에서 HelloKey를 읽고 / 엔드포인트에 값을 표시합니다. 구성 값이 null이면 "Hello"가 message에 할당됩니다.

var builder = WebApplication.CreateBuilder(args);

var message = builder.Configuration["HelloKey"] ?? "Hello";

var app = builder.Build();

app.MapGet("/", () => message);

app.Run();

구성 소스 읽기의 전체 목록은 ASP.NET Core의 구성에서 기본 구성을 참조하세요.

로깅 공급자 추가

var builder = WebApplication.CreateBuilder(args);

// Configure JSON logging to the console.
builder.Logging.AddJsonConsole();

var app = builder.Build();

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

app.Run();

서비스 추가

var builder = WebApplication.CreateBuilder(args);

// Add the memory cache services.
builder.Services.AddMemoryCache();

// Add a custom scoped service.
builder.Services.AddScoped<ITodoRepository, TodoRepository>();
var app = builder.Build();

IHostBuilder 사용자 지정

IHostBuilder의 기존 확장 메서드는 호스트 속성을 사용하여 액세스할 수 있습니다.

var builder = WebApplication.CreateBuilder(args);

// Wait 30 seconds for graceful shutdown.
builder.Host.ConfigureHostOptions(o => o.ShutdownTimeout = TimeSpan.FromSeconds(30));

var app = builder.Build();

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

app.Run();

IWebHostBuilder 사용자 지정

IWebHostBuilder의 확장 메서드는 WebApplicationBuilder.WebHost 속성을 사용하여 액세스할 수 있습니다.

var builder = WebApplication.CreateBuilder(args);

// Change the HTTP server implemenation to be HTTP.sys based
builder.WebHost.UseHttpSys();

var app = builder.Build();

app.MapGet("/", () => "Hello HTTP.sys");

app.Run();

웹 루트 변경

기본적으로 웹 루트는 wwwroot 폴더의 콘텐츠 루트를 기준으로 합니다. 웹 루트는 정적 파일 미들웨어가 정적 파일을 찾는 위치입니다. 웹 루트는 WebHostOptions, 명령줄 또는 UseWebRoot 메서드를 사용하여 변경할 수 있습니다.

var builder = WebApplication.CreateBuilder(new WebApplicationOptions
{
    Args = args,
    // Look for static files in webroot
    WebRootPath = "webroot"
});

var app = builder.Build();

app.Run();

사용자 지정 DI(종속성 주입) 컨테이너

다음 예제에서는 Autofac을 사용합니다.

var builder = WebApplication.CreateBuilder(args);

builder.Host.UseServiceProviderFactory(new AutofacServiceProviderFactory());

// Register services directly with Autofac here. Don't
// call builder.Populate(), that happens in AutofacServiceProviderFactory.
builder.Host.ConfigureContainer<ContainerBuilder>(builder => builder.RegisterModule(new MyApplicationModule()));

var app = builder.Build();

미들웨어 추가

기존 ASP.NET Core 미들웨어는 WebApplication에서 구성할 수 있습니다.

var app = WebApplication.Create(args);

// Setup the file server to serve static files.
app.UseFileServer();

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

app.Run();

자세한 내용은 ASP.NET Core 미들웨어를 참조하세요.

개발자 예외 페이지

WebApplication.CreateBuilder는 미리 구성된 기본값을 사용하여 WebApplicationBuilder 클래스의 새 인스턴스를 초기화합니다. 개발자 예외 페이지는 미리 구성된 기본값에서 사용하도록 설정됩니다. 개발 환경에서 다음 코드가 실행되는 경우 /로 이동하면 예외를 보여 주는 친숙한 페이지가 렌더링됩니다.

var builder = WebApplication.CreateBuilder(args);

var app = builder.Build();

app.MapGet("/", () =>
{
    throw new InvalidOperationException("Oops, the '/' route has thrown an exception.");
});

app.Run();

ASP.NET Core 미들웨어 기본 사항

다음 표에는 최소 API와 함께 자주 사용되는 미들웨어 일부가 나와 있습니다.

미들웨어 설명 API
인증 인증 지원을 제공합니다. UseAuthentication
Authorization 권한 부여 지원을 제공합니다. UseAuthorization
CORS 원본 간 리소스 공유를 구성합니다. UseCors
예외 처리기 미들웨어 파이프라인이 throw하는 예외를 전역으로 처리합니다. UseExceptionHandler
전달된 헤더 프록시된 헤더를 현재 요청에 전달합니다. UseForwardedHeaders
HTTPS 리디렉션 모든 HTTP 요청을 HTTPS로 리디렉션합니다. UseHttpsRedirection
HSTS(HTTP 엄격한 전송 보안) 특별한 응답 헤더를 추가하는 보안 향상 미들웨어입니다. UseHsts
요청 로깅 HTTP 요청 및 응답 로깅을 지원합니다. UseHttpLogging
요청 시간 제한 요청 시간 제한, 전역 기본값 및 엔드포인트당 구성을 지원합니다. UseRequestTimeouts
W3C 요청 로깅 W3C 형식의 HTTP 요청 및 응답 로깅을 지원합니다. UseW3CLogging
응답 캐싱 응답 캐시에 대한 지원을 제공합니다. UseResponseCaching
응답 압축 응답 압축에 대한 지원을 제공합니다. UseResponseCompression
세션 사용자 세션 관리에 대한 지원을 제공합니다. UseSession
정적 파일 정적 파일 및 디렉터리 검색 처리에 대한 지원을 제공합니다. UseStaticFiles, UseFileServer
WebSocket WebSocket 프로토콜을 활성화합니다. UseWebSockets

다음 섹션에서는 요청 처리에 대해 설명합니다. 라우팅, 매개 변수 바인딩 및 응답.

라우팅

구성된 WebApplicationMap{Verb}MapMethods를 지원하며 여기서 {Verb}Get, Post, Put 또는 Delete 등의 카멜 대/소문자 HTTP 메서드입니다.

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

app.MapGet("/", () => "This is a GET");
app.MapPost("/", () => "This is a POST");
app.MapPut("/", () => "This is a PUT");
app.MapDelete("/", () => "This is a DELETE");

app.MapMethods("/options-or-head", new[] { "OPTIONS", "HEAD" }, 
                          () => "This is an options or head request ");

app.Run();

이러한 메서드에 전달된 Delegate 인수를 "경로 처리기"라고 합니다.

경로 처리기

경로 처리기는 경로가 일치할 때 실행되는 메서드입니다. 경로 처리기는 람다 식, 로컬 함수, 인스턴스 메서드 또는 정적 메서드일 수 있습니다. 경로 처리기는 동기 또는 비동기일 수 있습니다.

람다 식

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

app.MapGet("/inline", () => "This is an inline lambda");

var handler = () => "This is a lambda variable";

app.MapGet("/", handler);

app.Run();

로컬 함수

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

string LocalFunction() => "This is local function";

app.MapGet("/", LocalFunction);

app.Run();

인스턴스 메서드

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

var handler = new HelloHandler();

app.MapGet("/", handler.Hello);

app.Run();

class HelloHandler
{
    public string Hello()
    {
        return "Hello Instance method";
    }
}

정적 메서드

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

app.MapGet("/", HelloHandler.Hello);

app.Run();

class HelloHandler
{
    public static string Hello()
    {
        return "Hello static method";
    }
}

외부에 정의된 엔드포인트 Program.cs

최소 API는 에 Program.cs위치할 필요가 없습니다.

Program.cs

using MinAPISeparateFile;

var builder = WebApplication.CreateSlimBuilder(args);

var app = builder.Build();

TodoEndpoints.Map(app);

app.Run();

TodoEndpoints.cs

namespace MinAPISeparateFile;

public static class TodoEndpoints
{
    public static void Map(WebApplication app)
    {
        app.MapGet("/", async context =>
        {
            // Get all todo items
            await context.Response.WriteAsJsonAsync(new { Message = "All todo items" });
        });

        app.MapGet("/{id}", async context =>
        {
            // Get one todo item
            await context.Response.WriteAsJsonAsync(new { Message = "One todo item" });
        });
    }
}

이 문서의 뒷부분에 있는 경로 그룹참조하세요.

경로의 URL을 생성하기 위해 경로에 이름을 지정할 수 있습니다. 이름이 지정된 경로를 사용하면 앱에서 경로를 하드 코드로 작성할 필요가 없습니다.

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

app.MapGet("/hello", () => "Hello named route")
   .WithName("hi");

app.MapGet("/", (LinkGenerator linker) => 
        $"The link to the hello route is {linker.GetPathByName("hi", values: null)}");

app.Run();

이전 코드는 / 엔드포인트에서 The link to the hello endpoint is /hello를 표시합니다.

참고: 엔드포인트 이름은 대/소문자를 구분합니다.

엔드포인트 이름:

  • 전역적으로 고유해야 합니다.
  • OpenAPI 지원을 사용할 때 OpenAPI 작업 ID로 사용됩니다. 자세한 내용은 OpenAPI를 참조하세요.

경로 매개 변수

경로 패턴 정의의 일부로 경로 매개 변수를 캡처할 수 있습니다.

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

app.MapGet("/users/{userId}/books/{bookId}", 
    (int userId, int bookId) => $"The user id is {userId} and book id is {bookId}");

app.Run();

이전 코드는 /users/3/books/7 URI에서 The user id is 3 and book id is 7을 반환합니다.

경로 처리기는 캡처할 매개 변수를 선언할 수 있습니다. 캡처하도록 선언된 매개 변수가 있는 경로에 대한 요청이 이루어지면 매개 변수가 구문 분석되어 처리기에 전달됩니다. 이렇게 하면 형식이 안전한 방식으로 값을 쉽게 캡처할 수 있습니다. 이전 코드에서 userIdbookId는 모두 int입니다.

이전 코드에서 경로 값 중 하나를 int로 변환할 수 없는 경우 예외가 throw됩니다. GET 요청 /users/hello/books/3는 다음 예외를 throw합니다.

BadHttpRequestException: Failed to bind parameter "int userId" from "hello".

와일드카드 및 catch all 경로

다음 catch all 경로는 `/posts/hello` 엔드포인트에서 Routing to hello를 반환합니다.

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

app.MapGet("/posts/{*rest}", (string rest) => $"Routing to {rest}");

app.Run();

경로 제약 조건

경로 제약 조건은 경로의 일치 동작을 제한합니다.

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

app.MapGet("/todos/{id:int}", (int id) => db.Todos.Find(id));
app.MapGet("/todos/{text}", (string text) => db.Todos.Where(t => t.Text.Contains(text));
app.MapGet("/posts/{slug:regex(^[a-z0-9_-]+$)}", (string slug) => $"Post {slug}");

app.Run();

다음 표는 이전 경로 템플릿과 동작을 보여 줍니다.

경로 템플릿 URI 일치 예제
/todos/{id:int} /todos/1
/todos/{text} /todos/something
/posts/{slug:regex(^[a-z0-9_-]+$)} /posts/mypost

자세한 내용은 ASP.NET Core의 라우팅에서 경로 제약 조건 참조를 참조하세요.

경로 그룹

MapGroup확장 메서드는 공통 접두사를 사용하여 엔드포인트 그룹을 구성하는 데 도움이 됩니다. 반복 코드를 줄이고 엔드포인트 메타데이터를 추가하는 RequireAuthorization이나 WithMetadata와 같은 메서드로서 단일 호출로 엔드포인트 전체 그룹을 사용자 지정할 수 있게 됩니다.

예를 들어 다음 코드에서는 두 개의 유사한 엔드포인트 그룹을 만듭니다.

app.MapGroup("/public/todos")
    .MapTodosApi()
    .WithTags("Public");

app.MapGroup("/private/todos")
    .MapTodosApi()
    .WithTags("Private")
    .AddEndpointFilterFactory(QueryPrivateTodos)
    .RequireAuthorization();


EndpointFilterDelegate QueryPrivateTodos(EndpointFilterFactoryContext factoryContext, EndpointFilterDelegate next)
{
    var dbContextIndex = -1;

    foreach (var argument in factoryContext.MethodInfo.GetParameters())
    {
        if (argument.ParameterType == typeof(TodoDb))
        {
            dbContextIndex = argument.Position;
            break;
        }
    }

    // Skip filter if the method doesn't have a TodoDb parameter.
    if (dbContextIndex < 0)
    {
        return next;
    }

    return async invocationContext =>
    {
        var dbContext = invocationContext.GetArgument<TodoDb>(dbContextIndex);
        dbContext.IsPrivate = true;

        try
        {
            return await next(invocationContext);
        }
        finally
        {
            // This should only be relevant if you're pooling or otherwise reusing the DbContext instance.
            dbContext.IsPrivate = false;
        }
    };
}
public static RouteGroupBuilder MapTodosApi(this RouteGroupBuilder group)
{
    group.MapGet("/", GetAllTodos);
    group.MapGet("/{id}", GetTodo);
    group.MapPost("/", CreateTodo);
    group.MapPut("/{id}", UpdateTodo);
    group.MapDelete("/{id}", DeleteTodo);

    return group;
}

이 시나리오에서는 201 Created 결과에서 Location 헤더에 대한 상대 주소를 사용할 수 있습니다.

public static async Task<Created<Todo>> CreateTodo(Todo todo, TodoDb database)
{
    await database.AddAsync(todo);
    await database.SaveChangesAsync();

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

첫 번째 엔드포인트 그룹은 접두사로 지정된 요청만 /public/todos 매칭하게 되며 인증 없이 액세스할 수 있습니다. 두 번째 엔드포인트 그룹은 접두사 /private/todos를 가지고 인증이 필요한 요청만 일치합니다.

QueryPrivateTodos엔드포인트 필터 팩터리는 경로 처리기의 TodoDb 매개 변수를 수정하여 프라이빗 할일 데이터에 접근하고 보관할 수 있도록 수정하는 로컬 함수입니다.

경로 그룹은 경로 매개 변수 및 제약 조건이 있는 중첩 그룹 및 복잡한 접두사 유형도 지원합니다. 다음 예제에서 그룹에 매핑된 user 경로 처리기는 외부 그룹 접두사에 정의된 {org}{group} 경로 매개 변수를 캡처할 수 있습니다.

접두사는 비어 있을 수도 있습니다. 이 기능은 경로 패턴을 변경하지 않고 엔드포인트 메타데이터 또는 필터를 엔드포인트 그룹에 추가하는 데 유용할 수 있습니다.

var all = app.MapGroup("").WithOpenApi();
var org = all.MapGroup("{org}");
var user = org.MapGroup("{user}");
user.MapGet("", (string org, string user) => $"{org}/{user}");

그룹에 필터 또는 메타데이터를 추가하면 내부 그룹 또는 특정 엔드포인트에 추가될 수 있는 추가 필터 또는 메타데이터를 추가하기 전에 각 엔드포인트에 개별적으로 추가하는 것과 동일한 결과가 됩니다.

var outer = app.MapGroup("/outer");
var inner = outer.MapGroup("/inner");

inner.AddEndpointFilter((context, next) =>
{
    app.Logger.LogInformation("/inner group filter");
    return next(context);
});

outer.AddEndpointFilter((context, next) =>
{
    app.Logger.LogInformation("/outer group filter");
    return next(context);
});

inner.MapGet("/", () => "Hi!").AddEndpointFilter((context, next) =>
{
    app.Logger.LogInformation("MapGet filter");
    return next(context);
});

위의 예제에서 외부 필터는 두 번째로 추가된 경우에도 내부 필터 앞에 들어오는 요청을 기록하게 됩니다. 필터가 서로 다른 그룹에 적용되었기 때문에 필터가 추가된 상대적 순서는 중요하지 않습니다. 동일한 그룹 또는 특정 엔드포인트에 적용되는 경우 주문 필터가 추가됩니다.

/outer/inner/에 대한 요청은 다음을 기록합니다.

/outer group filter
/inner group filter
MapGet filter

매개 변수 바인딩

매개 변수 바인딩은 경로 처리기가 표현하는 강력한 형식의 매개 변수로 요청 데이터를 변환하는 프로세스입니다. 바인딩 소스는 매개 변수가 바인딩되는 위치를 결정합니다. 바인딩 소스는 HTTP 메서드 및 매개 변수 형식에 따라 명시적이거나 유추될 수 있습니다.

지원되는 바인딩 소스:

  • 경로 값
  • 쿼리 문자열
  • 헤더
  • 본문(JSON 형식)
  • 종속성 주입에서 제공하는 서비스
  • 사용자 지정

양식 값의 바인딩은 .NET 6 및 7에서 기본적으로 지원되지 않습니다.

다음 GET 경로 처리기는 이러한 매개 변수 바인딩 소스 중 일부를 사용합니다.

var builder = WebApplication.CreateBuilder(args);

// Added as service
builder.Services.AddSingleton<Service>();

var app = builder.Build();

app.MapGet("/{id}", (int id,
                     int page,
                     [FromHeader(Name = "X-CUSTOM-HEADER")] string customHeader,
                     Service service) => { });

class Service { }

다음 표에서는 이전 예제에서 사용된 매개 변수와 연결된 바인딩 소스 간의 관계를 보여 줍니다.

매개 변수 바인딩 소스
id 경로 값
page 쿼리 문자열
customHeader 헤더
service 종속성 주입에서 제공

HTTP 메서드 GET, HEAD, OPTIONS, DELETE는 본문에서 암시적으로 바인딩되지 않습니다. 이러한 HTTP 메서드에 대해 본문(JSON 형식)에서 바인딩하려면 [FromBody]명시적으로 바인딩하거나 HttpRequest에서 읽습니다.

다음 예제 POST 경로 처리기는 person 매개 변수에 대해 본문(JSON 형식)의 바인딩 소스를 사용합니다.

var builder = WebApplication.CreateBuilder(args);

var app = builder.Build();

app.MapPost("/", (Person person) => { });

record Person(string Name, int Age);

이전 예제의 매개 변수는 모두 요청 데이터에서 자동으로 바인딩됩니다. 매개 변수 바인딩이 제공하는 편의를 설명하기 위해 다음 경로 처리기는 요청에서 직접 요청 데이터를 읽는 방법을 보여줍니다.

app.MapGet("/{id}", (HttpRequest request) =>
{
    var id = request.RouteValues["id"];
    var page = request.Query["page"];
    var customHeader = request.Headers["X-CUSTOM-HEADER"];

    // ...
});

app.MapPost("/", async (HttpRequest request) =>
{
    var person = await request.ReadFromJsonAsync<Person>();

    // ...
});

명시적 매개 변수 바인딩

특성을 사용하여 매개 변수가 바인딩되는 위치를 명시적으로 선언할 수 있습니다.

using Microsoft.AspNetCore.Mvc;

var builder = WebApplication.CreateBuilder(args);

// Added as service
builder.Services.AddSingleton<Service>();

var app = builder.Build();


app.MapGet("/{id}", ([FromRoute] int id,
                     [FromQuery(Name = "p")] int page,
                     [FromServices] Service service,
                     [FromHeader(Name = "Content-Type")] string contentType) 
                     => {});

class Service { }

record Person(string Name, int Age);
매개 변수 바인딩 소스
id 이름이 id인 경로 값
page 이름이 "p"인 쿼리 문자열
service 종속성 주입에서 제공
contentType 이름이 "Content-Type"인 헤더

참고 항목

양식 값의 바인딩은 .NET 6 및 7에서 기본적으로 지원되지 않습니다.

종속성 주입을 사용하는 매개 변수 바인딩

최소 API에 대한 매개 변수 바인딩은 형식이 서비스로 구성된 경우 종속성 주입을 통해 매개 변수를 바인딩합니다. 매개 변수에 [FromServices] 특성을 명시적으로 적용할 필요는 없습니다. 다음 코드에서는 두 작업 모두 시간을 반환합니다.

using Microsoft.AspNetCore.Mvc;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton<IDateTime, SystemDateTime>();

var app = builder.Build();

app.MapGet("/",   (               IDateTime dateTime) => dateTime.Now);
app.MapGet("/fs", ([FromServices] IDateTime dateTime) => dateTime.Now);
app.Run();

선택적 매개 변수

경로 처리기에서 선언된 매개 변수는 필수로 처리됩니다.

  • 요청이 경로와 일치하는 경우 요청에 모든 필수 매개 변수가 제공되는 경우에만 경로 처리기가 실행됩니다.
  • 모든 필수 매개 변수를 제공하지 못하면 오류가 발생합니다.
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/products", (int pageNumber) => $"Requesting page {pageNumber}");

app.Run();
URI result
/products?pageNumber=3 3 반환됨
/products BadHttpRequestException: 필수 매개 변수 "int pageNumber"가 쿼리 문자열에서 제공되지 않았습니다.
/products/1 HTTP 404 오류, 일치하는 경로 없음

pageNumber를 선택 사항으로 지정하려면 형식을 선택 사항으로 정의하거나 기본값을 제공합니다.

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

app.MapGet("/products", (int? pageNumber) => $"Requesting page {pageNumber ?? 1}");

string ListProducts(int pageNumber = 1) => $"Requesting page {pageNumber}";

app.MapGet("/products2", ListProducts);

app.Run();
URI result
/products?pageNumber=3 3 반환됨
/products 1 반환됨
/products2 1 반환됨

이전 null 허용 값과 기본값은 모든 원본에 적용됩니다.

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

app.MapPost("/products", (Product? product) => { });

app.Run();

이전 코드는 요청 본문을 보내지 않은 경우 null 곱을 사용하여 메서드를 호출합니다.

참고: 잘못된 데이터를 제공하고 매개 변수가 null을 허용하면 경로 처리기가 실행되지 않습니다.

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

app.MapGet("/products", (int? pageNumber) => $"Requesting page {pageNumber ?? 1}");

app.Run();
URI result
/products?pageNumber=3 3 반환됨
/products 1 반환됨
/products?pageNumber=two BadHttpRequestException: "2"에서 "Nullable<int> pageNumber" 매개 변수를 바인딩하지 못했습니다.
/products/two HTTP 404 오류, 일치하는 경로 없음

자세한 내용은 바인딩 실패 섹션을 참조하세요.

특수 형식

다음 형식은 명시적 특성 없이 바인딩됩니다.

  • HttpContext: 현재 HTTP 요청 또는 응답에 대한 모든 정보를 포함하는 컨텍스트입니다.

    app.MapGet("/", (HttpContext context) => context.Response.WriteAsync("Hello World"));
    
  • HttpRequestHttpResponse: HTTP 요청 및 HTTP 응답:

    app.MapGet("/", (HttpRequest request, HttpResponse response) =>
        response.WriteAsync($"Hello World {request.Query["name"]}"));
    
  • CancellationToken: 현재 HTTP 요청에 연결된 취소 토큰:

    app.MapGet("/", async (CancellationToken cancellationToken) => 
        await MakeLongRunningRequestAsync(cancellationToken));
    
  • ClaimsPrincipal: HttpContext.User에서 바인딩된 요청에 연결된 사용자:

    app.MapGet("/", (ClaimsPrincipal user) => user.Identity.Name);
    

요청 본문을 Stream 또는 PipeReader로 바인딩

요청 본문을 Stream 또는 PipeReader로 바인딩하여 사용자가 데이터를 처리하여 다음 작업을 수행해야 하는 시나리오를 효율적으로 지원할 수 있습니다.

  • 데이터를 Blob 스토리지에 저장하거나 큐 공급자의 큐에 추가합니다.
  • 작업자 프로세스 또는 클라우드 함수를 사용하여 저장된 데이터를 처리합니다.

예를 들어 데이터는 Azure Queue Storage의 큐에 추가하거나 Azure Blob Storage에 저장할 수 있습니다.

다음 코드에서는 백그라운드 큐를 구현합니다.

using System.Text.Json;
using System.Threading.Channels;

namespace BackgroundQueueService;

class BackgroundQueue : BackgroundService
{
    private readonly Channel<ReadOnlyMemory<byte>> _queue;
    private readonly ILogger<BackgroundQueue> _logger;

    public BackgroundQueue(Channel<ReadOnlyMemory<byte>> queue,
                               ILogger<BackgroundQueue> logger)
    {
        _queue = queue;
        _logger = logger;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        await foreach (var dataStream in _queue.Reader.ReadAllAsync(stoppingToken))
        {
            try
            {
                var person = JsonSerializer.Deserialize<Person>(dataStream.Span)!;
                _logger.LogInformation($"{person.Name} is {person.Age} " +
                                       $"years and from {person.Country}");
            }
            catch (Exception ex)
            {
                _logger.LogError(ex.Message);
            }
        }
    }
}

class Person
{
    public string Name { get; set; } = String.Empty;
    public int Age { get; set; }
    public string Country { get; set; } = String.Empty;
}

다음 코드에서는 요청 본문을 Stream에 바인딩합니다.

app.MapPost("/register", async (HttpRequest req, Stream body,
                                 Channel<ReadOnlyMemory<byte>> queue) =>
{
    if (req.ContentLength is not null && req.ContentLength > maxMessageSize)
    {
        return Results.BadRequest();
    }

    // We're not above the message size and we have a content length, or
    // we're a chunked request and we're going to read up to the maxMessageSize + 1. 
    // We add one to the message size so that we can detect when a chunked request body
    // is bigger than our configured max.
    var readSize = (int?)req.ContentLength ?? (maxMessageSize + 1);

    var buffer = new byte[readSize];

    // Read at least that many bytes from the body.
    var read = await body.ReadAtLeastAsync(buffer, readSize, throwOnEndOfStream: false);

    // We read more than the max, so this is a bad request.
    if (read > maxMessageSize)
    {
        return Results.BadRequest();
    }

    // Attempt to send the buffer to the background queue.
    if (queue.Writer.TryWrite(buffer.AsMemory(0..read)))
    {
        return Results.Accepted();
    }

    // We couldn't accept the message since we're overloaded.
    return Results.StatusCode(StatusCodes.Status429TooManyRequests);
});

다음 코드에서는 전체 Program.cs 파일을 보여 줍니다.

using System.Threading.Channels;
using BackgroundQueueService;

var builder = WebApplication.CreateBuilder(args);
// The max memory to use for the upload endpoint on this instance.
var maxMemory = 500 * 1024 * 1024;

// The max size of a single message, staying below the default LOH size of 85K.
var maxMessageSize = 80 * 1024;

// The max size of the queue based on those restrictions
var maxQueueSize = maxMemory / maxMessageSize;

// Create a channel to send data to the background queue.
builder.Services.AddSingleton<Channel<ReadOnlyMemory<byte>>>((_) =>
                     Channel.CreateBounded<ReadOnlyMemory<byte>>(maxQueueSize));

// Create a background queue service.
builder.Services.AddHostedService<BackgroundQueue>();
var app = builder.Build();

// curl --request POST 'https://localhost:<port>/register' --header 'Content-Type: application/json' --data-raw '{ "Name":"Samson", "Age": 23, "Country":"Nigeria" }'
// curl --request POST "https://localhost:<port>/register" --header "Content-Type: application/json" --data-raw "{ \"Name\":\"Samson\", \"Age\": 23, \"Country\":\"Nigeria\" }"
app.MapPost("/register", async (HttpRequest req, Stream body,
                                 Channel<ReadOnlyMemory<byte>> queue) =>
{
    if (req.ContentLength is not null && req.ContentLength > maxMessageSize)
    {
        return Results.BadRequest();
    }

    // We're not above the message size and we have a content length, or
    // we're a chunked request and we're going to read up to the maxMessageSize + 1. 
    // We add one to the message size so that we can detect when a chunked request body
    // is bigger than our configured max.
    var readSize = (int?)req.ContentLength ?? (maxMessageSize + 1);

    var buffer = new byte[readSize];

    // Read at least that many bytes from the body.
    var read = await body.ReadAtLeastAsync(buffer, readSize, throwOnEndOfStream: false);

    // We read more than the max, so this is a bad request.
    if (read > maxMessageSize)
    {
        return Results.BadRequest();
    }

    // Attempt to send the buffer to the background queue.
    if (queue.Writer.TryWrite(buffer.AsMemory(0..read)))
    {
        return Results.Accepted();
    }

    // We couldn't accept the message since we're overloaded.
    return Results.StatusCode(StatusCodes.Status429TooManyRequests);
});

app.Run();
  • 데이터를 읽을 때 StreamHttpRequest.Body와 동일한 개체입니다.
  • 요청 본문은 기본적으로 버퍼링되지 않습니다. 본문을 읽은 후에는 되감을 수 없습니다. 스트림은 여러 번 읽을 수 없습니다.
  • StreamPipeReader는 최소 작업 처리기 외부에서 사용할 수 없습니다. 기본 버퍼가 삭제되거나 다시 사용되기 때문입니다.

IFormFile 및 IFormFileCollection을 사용하여 파일 업로드

다음 코드는 IFormFileIFormFileCollection을 사용하여 파일을 업로드합니다.

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

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

app.MapPost("/upload", async (IFormFile file) =>
{
    var tempFile = Path.GetTempFileName();
    app.Logger.LogInformation(tempFile);
    using var stream = File.OpenWrite(tempFile);
    await file.CopyToAsync(stream);
});

app.MapPost("/upload_many", async (IFormFileCollection myFiles) =>
{
    foreach (var file in myFiles)
    {
        var tempFile = Path.GetTempFileName();
        app.Logger.LogInformation(tempFile);
        using var stream = File.OpenWrite(tempFile);
        await file.CopyToAsync(stream);
    }
});

app.Run();

인증된 파일 업로드 요청은 인증 헤더, 클라이언트 인증서 또는 cookie 헤더를 사용하여 지원됩니다.

ASP.NET Core 7.0에서는 위조 방지 기능이 기본적으로 지원되지 않습니다. 위조 방지는 ASP.NET Core 8.0 이상에서 사용할 수 있습니다. 그러나 IAntiforgery 서비스를 사용하여 구현할 수 있습니다.

헤더 및 쿼리 문자열에서 배열 및 문자열 값 바인딩

다음 코드는 기본 형식의 배열, 문자열 배열 및 StringValues에 쿼리 문자열을 바인딩하는 방법을 보여 줍니다.

// Bind query string values to a primitive type array.
// GET  /tags?q=1&q=2&q=3
app.MapGet("/tags", (int[] q) =>
                      $"tag1: {q[0]} , tag2: {q[1]}, tag3: {q[2]}");

// Bind to a string array.
// GET /tags2?names=john&names=jack&names=jane
app.MapGet("/tags2", (string[] names) =>
            $"tag1: {names[0]} , tag2: {names[1]}, tag3: {names[2]}");

// Bind to StringValues.
// GET /tags3?names=john&names=jack&names=jane
app.MapGet("/tags3", (StringValues names) =>
            $"tag1: {names[0]} , tag2: {names[1]}, tag3: {names[2]}");

복합 형식의 배열에 쿼리 문자열 또는 헤더 값 바인딩은 형식에 TryParse가 구현된 경우에 지원됩니다. 다음 코드는 문자열 배열에 바인딩되며 지정된 태그가 있는 모든 항목을 반환합니다.

// GET /todoitems/tags?tags=home&tags=work
app.MapGet("/todoitems/tags", async (Tag[] tags, TodoDb db) =>
{
    return await db.Todos
        .Where(t => tags.Select(i => i.Name).Contains(t.Tag.Name))
        .ToListAsync();
});

다음 코드는 모델 및 필요한 TryParse 구현을 보여 줍니다.

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

    // This is an owned entity. 
    public Tag Tag { get; set; } = new();
}

[Owned]
public class Tag
{
    public string? Name { get; set; } = "n/a";

    public static bool TryParse(string? name, out Tag tag)
    {
        if (name is null)
        {
            tag = default!;
            return false;
        }

        tag = new Tag { Name = name };
        return true;
    }
}

다음 코드는 int 배열에 바인딩됩니다.

// GET /todoitems/query-string-ids?ids=1&ids=3
app.MapGet("/todoitems/query-string-ids", async (int[] ids, TodoDb db) =>
{
    return await db.Todos
        .Where(t => ids.Contains(t.Id))
        .ToListAsync();
});

위의 코드를 테스트하려면 다음 엔드포인트를 추가하여 데이터베이스를 Todo 항목으로 채웁니다.

// POST /todoitems/batch
app.MapPost("/todoitems/batch", async (Todo[] todos, TodoDb db) =>
{
    await db.Todos.AddRangeAsync(todos);
    await db.SaveChangesAsync();

    return Results.Ok(todos);
});

API 테스트 도구를 HttpRepl 사용하여 이전 엔드포인트에 다음 데이터를 전달합니다.

[
    {
        "id": 1,
        "name": "Have Breakfast",
        "isComplete": true,
        "tag": {
            "name": "home"
        }
    },
    {
        "id": 2,
        "name": "Have Lunch",
        "isComplete": true,
        "tag": {
            "name": "work"
        }
    },
    {
        "id": 3,
        "name": "Have Supper",
        "isComplete": true,
        "tag": {
            "name": "home"
        }
    },
    {
        "id": 4,
        "name": "Have Snacks",
        "isComplete": true,
        "tag": {
            "name": "N/A"
        }
    }
]

다음 코드는 헤더 키 X-Todo-Id에 바인딩되고 Id 값이 일치하는 Todo 항목을 반환합니다.

// GET /todoitems/header-ids
// The keys of the headers should all be X-Todo-Id with different values
app.MapGet("/todoitems/header-ids", async ([FromHeader(Name = "X-Todo-Id")] int[] ids, TodoDb db) =>
{
    return await db.Todos
        .Where(t => ids.Contains(t.Id))
        .ToListAsync();
});

참고 항목

쿼리 문자열에서 바인딩 string[] 할 때 일치하는 쿼리 문자열 값이 없으면 null 값 대신 빈 배열이 생성됩니다.

[AsParameters]를 사용하여 인수 목록에 대한 매개 변수 바인딩

AsParametersAttribute는 유형에 대한 매개 변수 바인딩을 활성화하며 복합 또는 재귀 모델 바인딩은 활성화하지 않습니다.

다음 코드를 생각해 봅시다.

using Microsoft.EntityFrameworkCore;
using TodoApi.Models;

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());
// Remaining code removed for brevity.

다음 GET 엔드포인트를 살펴보세요.

app.MapGet("/todoitems/{id}",
                             async (int Id, TodoDb Db) =>
    await Db.Todos.FindAsync(Id)
        is Todo todo
            ? Results.Ok(new TodoItemDTO(todo))
            : Results.NotFound());

다음 struct는 강조 표시된 위 매개 변수를 바꾸는 데 사용할 수 있습니다.

struct TodoItemRequest
{
    public int Id { get; set; }
    public TodoDb Db { get; set; }
}

리팩터링된 GET 엔드포인트는 위의 structAsParameters 특성과 함께 사용합니다.

app.MapGet("/ap/todoitems/{id}",
                                async ([AsParameters] TodoItemRequest request) =>
    await request.Db.Todos.FindAsync(request.Id)
        is Todo todo
            ? Results.Ok(new TodoItemDTO(todo))
            : Results.NotFound());

다음 코드는 앱의 추가 엔드포인트를 보여 줍니다.

app.MapPost("/todoitems", async (TodoItemDTO Dto, TodoDb Db) =>
{
    var todoItem = new Todo
    {
        IsComplete = Dto.IsComplete,
        Name = Dto.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 Dto, TodoDb Db) =>
{
    var todo = await Db.Todos.FindAsync(Id);

    if (todo is null) return Results.NotFound();

    todo.Name = Dto.Name;
    todo.IsComplete = Dto.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.Ok(new TodoItemDTO(todo));
    }

    return Results.NotFound();
});

다음 클래스는 매개 변수 목록을 리팩터링하는 데 사용합니다.

class CreateTodoItemRequest
{
    public TodoItemDTO Dto { get; set; } = default!;
    public TodoDb Db { get; set; } = default!;
}

class EditTodoItemRequest
{
    public int Id { get; set; }
    public TodoItemDTO Dto { get; set; } = default!;
    public TodoDb Db { get; set; } = default!;
}

다음 코드는 AsParameters를 사용하는 리팩터링된 엔드포인트와 위의 struct 및 클래스를 보여 줍니다.

app.MapPost("/ap/todoitems", async ([AsParameters] CreateTodoItemRequest request) =>
{
    var todoItem = new Todo
    {
        IsComplete = request.Dto.IsComplete,
        Name = request.Dto.Name
    };

    request.Db.Todos.Add(todoItem);
    await request.Db.SaveChangesAsync();

    return Results.Created($"/todoitems/{todoItem.Id}", new TodoItemDTO(todoItem));
});

app.MapPut("/ap/todoitems/{id}", async ([AsParameters] EditTodoItemRequest request) =>
{
    var todo = await request.Db.Todos.FindAsync(request.Id);

    if (todo is null) return Results.NotFound();

    todo.Name = request.Dto.Name;
    todo.IsComplete = request.Dto.IsComplete;

    await request.Db.SaveChangesAsync();

    return Results.NoContent();
});

app.MapDelete("/ap/todoitems/{id}", async ([AsParameters] TodoItemRequest request) =>
{
    if (await request.Db.Todos.FindAsync(request.Id) is Todo todo)
    {
        request.Db.Todos.Remove(todo);
        await request.Db.SaveChangesAsync();
        return Results.Ok(new TodoItemDTO(todo));
    }

    return Results.NotFound();
});

다음 record 형식을 사용하면 위의 매개 변수를 바꿀 수 있습니다.

record TodoItemRequest(int Id, TodoDb Db);
record CreateTodoItemRequest(TodoItemDTO Dto, TodoDb Db);
record EditTodoItemRequest(int Id, TodoItemDTO Dto, TodoDb Db);

structAsParameters와 함께 사용하면 record 유형보다 성능이 개선될 수 있습니다.

완전한 샘플 코드AspNetCore.Docs.Samples 리포지토리에 있습니다.

사용자 지정 바인딩

매개 변수 바인딩을 사용자 지정하는 방법에는 다음 두 가지가 있습니다.

  1. 경로, 쿼리, 헤더 바인딩 소스의 경우 형식의 정적 TryParse 메서드를 추가하여 사용자 지정 형식을 바인딩합니다.
  2. 형식에서 BindAsync 메서드를 구현하여 바인딩 프로세스를 제어합니다.

TryParse

TryParse에는 다음 두 가지 API가 있습니다.

public static bool TryParse(string value, out T result);
public static bool TryParse(string value, IFormatProvider provider, out T result);

다음 코드는 URI가 /map?Point=12.3,10.1Point: 12.3, 10.1을 표시합니다.

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

// GET /map?Point=12.3,10.1
app.MapGet("/map", (Point point) => $"Point: {point.X}, {point.Y}");

app.Run();

public class Point
{
    public double X { get; set; }
    public double Y { get; set; }

    public static bool TryParse(string? value, IFormatProvider? provider,
                                out Point? point)
    {
        // Format is "(12.3,10.1)"
        var trimmedValue = value?.TrimStart('(').TrimEnd(')');
        var segments = trimmedValue?.Split(',',
                StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
        if (segments?.Length == 2
            && double.TryParse(segments[0], out var x)
            && double.TryParse(segments[1], out var y))
        {
            point = new Point { X = x, Y = y };
            return true;
        }

        point = null;
        return false;
    }
}

BindAsync

BindAsync에는 다음 API가 있습니다.

public static ValueTask<T?> BindAsync(HttpContext context, ParameterInfo parameter);
public static ValueTask<T?> BindAsync(HttpContext context);

다음 코드는 URI가 /products?SortBy=xyz&SortDir=Desc&Page=99SortBy:xyz, SortDirection:Desc, CurrentPage:99을 표시합니다.

using System.Reflection;

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

// GET /products?SortBy=xyz&SortDir=Desc&Page=99
app.MapGet("/products", (PagingData pageData) => $"SortBy:{pageData.SortBy}, " +
       $"SortDirection:{pageData.SortDirection}, CurrentPage:{pageData.CurrentPage}");

app.Run();

public class PagingData
{
    public string? SortBy { get; init; }
    public SortDirection SortDirection { get; init; }
    public int CurrentPage { get; init; } = 1;

    public static ValueTask<PagingData?> BindAsync(HttpContext context,
                                                   ParameterInfo parameter)
    {
        const string sortByKey = "sortBy";
        const string sortDirectionKey = "sortDir";
        const string currentPageKey = "page";

        Enum.TryParse<SortDirection>(context.Request.Query[sortDirectionKey],
                                     ignoreCase: true, out var sortDirection);
        int.TryParse(context.Request.Query[currentPageKey], out var page);
        page = page == 0 ? 1 : page;

        var result = new PagingData
        {
            SortBy = context.Request.Query[sortByKey],
            SortDirection = sortDirection,
            CurrentPage = page
        };

        return ValueTask.FromResult<PagingData?>(result);
    }
}

public enum SortDirection
{
    Default,
    Asc,
    Desc
}

바인딩 실패

바인딩이 실패할 경우 프레임워크는 디버그 메시지를 로그하고 실패 모드에 따라 다양한 상태 코드를 클라이언트에 반환합니다.

실패 모드 null 허용 매개 변수 형식 바인딩 소스 상태 코드
{ParameterType}.TryParsefalse를 반환합니다 route/query/header 400
{ParameterType}.BindAsyncnull를 반환합니다 custom 400
{ParameterType}.BindAsync가 throw 중요하지 않음 custom 500
JSON 본문을 역직렬화하지 못함 중요하지 않음 본문 400
잘못된 콘텐츠 형식(application/json이 아님) 중요하지 않음 본문 415

바인딩 우선 순위

매개 변수에서 바인딩 소스를 결정하는 규칙:

  1. 매개 변수(From* 특성)에 다음 순서로 정의된 명시적 특성:
    1. 경로 값: [FromRoute]
    2. 쿼리 문자열: [FromQuery]
    3. 헤더: [FromHeader]
    4. 본문: [FromBody]
    5. 서비스: [FromServices]
    6. 매개 변수 값: [AsParameters]
  2. 특수 형식
    1. HttpContext
    2. HttpRequest (HttpContext.Request)
    3. HttpResponse (HttpContext.Response)
    4. ClaimsPrincipal (HttpContext.User)
    5. CancellationToken (HttpContext.RequestAborted)
    6. IFormFileCollection (HttpContext.Request.Form.Files)
    7. IFormFile (HttpContext.Request.Form.Files[paramName])
    8. Stream (HttpContext.Request.Body)
    9. PipeReader (HttpContext.Request.BodyReader)
  3. 매개 변수 형식에 유효한 정적 BindAsync 메서드가 있습니다.
  4. 매개 변수 형식이 문자열이거나 매개 변수 형식에 유효한 정적 TryParse 메서드가 있습니다.
    1. 매개 변수 이름이 경로 템플릿(예: app.Map("/todo/{id}", (int id) => {});)에 있는 경우 경로에서 바인딩됩니다.
    2. 쿼리 문자열에서 바인딩됩니다.
  5. 매개 변수 형식이 종속성 주입에서 제공되는 서비스인 경우 해당 서비스를 원본으로 사용합니다.
  6. 매개 변수가 본문의 매개 변수입니다.

본문 바인딩에 대한 ON 역직렬화 옵션 구성 JS

본문 바인딩 소스는 역직렬화에 사용합니다 System.Text.Json . 이 기본값은 변경할 수 없지만 JSON serialization 및 deserialization 옵션을 구성할 수 있습니다.

전역적으로 ON 역직렬화 옵션 구성 JS

앱에 대해 전역적으로 적용되는 옵션은 호출하여 ConfigureHttpJsonOptions구성할 수 있습니다. 다음 예제에는 공용 필드 및 ON 출력 형식이 JS포함됩니다.

var builder = WebApplication.CreateBuilder(args);

builder.Services.ConfigureHttpJsonOptions(options => {
    options.SerializerOptions.WriteIndented = true;
    options.SerializerOptions.IncludeFields = true;
});

var app = builder.Build();

app.MapPost("/", (Todo todo) => {
    if (todo is not null) {
        todo.Name = todo.NameField;
    }
    return todo;
});

app.Run();

class Todo {
    public string? Name { get; set; }
    public string? NameField;
    public bool IsComplete { get; set; }
}
// If the request body contains the following JSON:
//
// {"nameField":"Walk dog", "isComplete":false}
//
// The endpoint returns the following JSON:
//
// {
//    "name":"Walk dog",
//    "nameField":"Walk dog",
//    "isComplete":false
// }

샘플 코드는 serialization 및 deserialization을 모두 구성하므로 출력 JSON에서 읽고 NameField 포함 NameField 할 수 있습니다.

엔드포인트에 대한 ON 역직렬화 옵션 구성 JS

ReadFromJsonAsync 에는 개체를 허용하는 오버로드가 있습니다 JsonSerializerOptions . 다음 예제에는 공용 필드 및 ON 출력 형식이 JS포함됩니다.

using System.Text.Json;

var app = WebApplication.Create();

var options = new JsonSerializerOptions(JsonSerializerDefaults.Web) { 
    IncludeFields = true, 
    WriteIndented = true
};

app.MapPost("/", async (HttpContext context) => {
    if (context.Request.HasJsonContentType()) {
        var todo = await context.Request.ReadFromJsonAsync<Todo>(options);
        if (todo is not null) {
            todo.Name = todo.NameField;
        }
        return Results.Ok(todo);
    }
    else {
        return Results.BadRequest();
    }
});

app.Run();

class Todo
{
    public string? Name { get; set; }
    public string? NameField;
    public bool IsComplete { get; set; }
}
// If the request body contains the following JSON:
//
// {"nameField":"Walk dog", "isComplete":false}
//
// The endpoint returns the following JSON:
//
// {
//    "name":"Walk dog",
//    "isComplete":false
// }

앞의 코드는 역직렬화에만 사용자 지정된 옵션을 적용하므로 출력 JSON은 제외됩니다 NameField.

요청 본문 읽기

HttpContext 또는 HttpRequest 매개 변수를 사용하여 직접 요청 본문을 읽습니다.

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

app.MapPost("/uploadstream", async (IConfiguration config, HttpRequest request) =>
{
    var filePath = Path.Combine(config["StoredFilesPath"], Path.GetRandomFileName());

    await using var writeStream = File.Create(filePath);
    await request.BodyReader.CopyToAsync(writeStream);
});

app.Run();

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

  • HttpRequest.BodyReader를 사용하여 요청 본문에 액세스합니다.
  • 요청 본문을 로컬 파일에 복사합니다.

응답

경로 처리기는 다음과 같은 형식의 반환 값을 지원합니다.

  1. IResult 기반 - 여기에는 Task<IResult>ValueTask<IResult>가 포함됩니다.
  2. string - 여기에는 Task<string>ValueTask<string>이 포함됩니다.
  3. T(다른 모든 형식) - 여기에는 Task<T>ValueTask<T>가 포함됩니다.
반환 값 동작 콘텐츠-형식
IResult 프레임워크가 IResult.ExecuteAsync를 호출합니다. IResult 구현에 의해 결정됩니다.
string 프레임워크가 응답에 직접 문자열을 씁니다. text/plain
T(기타 형식) 프레임워크 JS는 응답을 ON 직렬화합니다. application/json

처리기 반환 값을 라우팅하는 자세한 가이드는 최소 API 애플리케이션에서 응답 만들기를 참조하세요.

예시 반환 값

문자열 반환 값

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

JSON 반환 값

app.MapGet("/hello", () => new { Message = "Hello World" });

TypedResults 반환

다음 코드는 다음을 반환합니다.TypedResults

app.MapGet("/hello", () => TypedResults.Ok(new Message() {  Text = "Hello World!" }));

반환은 TypedResults 반환하는 Results것이 좋습니다. 자세한 내용은 TypedResults 및 결과를 참조하세요.

IResult 반환 값

app.MapGet("/hello", () => Results.Ok(new { Message = "Hello World" }));

다음 예는 기본 제공 결과 형식을 사용하여 응답을 사용자 지정합니다.

app.MapGet("/api/todoitems/{id}", async (int id, TodoDb db) =>
         await db.Todos.FindAsync(id) 
         is Todo todo
         ? Results.Ok(todo) 
         : Results.NotFound())
   .Produces<Todo>(StatusCodes.Status200OK)
   .Produces(StatusCodes.Status404NotFound);

JS에

app.MapGet("/hello", () => Results.Json(new { Message = "Hello World" }));

사용자 지정 상태 코드

app.MapGet("/405", () => Results.StatusCode(405));

Text

app.MapGet("/text", () => Results.Text("This is some text"));

스트림

var proxyClient = new HttpClient();
app.MapGet("/pokemon", async () => 
{
    var stream = await proxyClient.GetStreamAsync("http://contoso/pokedex.json");
    // Proxy the response as JSON
    return Results.Stream(stream, "application/json");
});

자세한 예제는 최소 API 애플리케이션에서 응답 만들기 를 참조하세요.

리디렉션

app.MapGet("/old-path", () => Results.Redirect("/new-path"));

파일

app.MapGet("/download", () => Results.File("myfile.text"));

기본 제공 결과

일반적인 결과 도우미는 정적 클래스 및 TypedResults 정적 클래스에 Results 있습니다. 반환은 TypedResults 반환하는 Results것이 좋습니다. 자세한 내용은 TypedResults 및 결과를 참조하세요.

결과 사용자 지정

애플리케이션은 사용자 지정 IResult 형식을 구현하여 응답을 제어할 수 있습니다. 다음 코드는 HTML 결과 형식의 예입니다.

using System.Net.Mime;
using System.Text;
static class ResultsExtensions
{
    public static IResult Html(this IResultExtensions resultExtensions, string html)
    {
        ArgumentNullException.ThrowIfNull(resultExtensions);

        return new HtmlResult(html);
    }
}

class HtmlResult : IResult
{
    private readonly string _html;

    public HtmlResult(string html)
    {
        _html = html;
    }

    public Task ExecuteAsync(HttpContext httpContext)
    {
        httpContext.Response.ContentType = MediaTypeNames.Text.Html;
        httpContext.Response.ContentLength = Encoding.UTF8.GetByteCount(_html);
        return httpContext.Response.WriteAsync(_html);
    }
}

이러한 사용자 지정 결과를 더 검색 가능하게 만들려면 Microsoft.AspNetCore.Http.IResultExtensions에 확장 메서드를 추가하는 것이 좋습니다.

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

app.MapGet("/html", () => Results.Extensions.Html(@$"<!doctype html>
<html>
    <head><title>miniHTML</title></head>
    <body>
        <h1>Hello World</h1>
        <p>The time on the server is {DateTime.Now:O}</p>
    </body>
</html>"));

app.Run();

형식화된 결과

IResult 인터페이스는 반환된 개체를 HTTP 응답으로 직렬화하는 JSON에 대한 암시적 지원을 활용하지 않는 최소 API에서 반환된 값을 나타낼 수 있습니다. 정적 Results 클래스는 다양한 형식의 응답을 나타내는 다양한 IResult 개체를 만드는 데 사용됩니다. 예를 들어 응답 상태 코드를 설정하거나 다른 URL로 리디렉션합니다.

IResult를 구현하는 형식은 공용이므로 테스트할 때 형식 어설션을 허용합니다. 예시:

[TestClass()]
public class WeatherApiTests
{
    [TestMethod()]
    public void MapWeatherApiTest()
    {
        var result = WeatherApi.GetAllWeathers();
        Assert.IsInstanceOfType(result, typeof(Ok<WeatherForecast[]>));
    }      
}

정적 TypedResults 클래스에서 해당 메서드의 반환 형식을 확인하여 캐스팅할 올바른 퍼블릭 IResult 형식을 찾을 수 있습니다.

자세한 예제는 최소 API 애플리케이션에서 응답 만들기 를 참조하세요.

필터

최소 API 앱의 필터를 참조하세요.

권한 부여

권한 부여 정책을 사용하여 경로를 보호할 수 있습니다. 권한 부여 정책은 [Authorize] 특성을 통해 또는 RequireAuthorization 메서드를 사용하여 선언할 수 있습니다.

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using WebRPauth.Data;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthorization(o => o.AddPolicy("AdminsOnly", 
                                  b => b.RequireClaim("admin", "true")));

var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();

builder.Services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
    .AddEntityFrameworkStores<ApplicationDbContext>();

var app = builder.Build();

app.UseAuthorization();

app.MapGet("/auth", [Authorize] () => "This endpoint requires authorization.");
app.MapGet("/", () => "This endpoint doesn't require authorization.");
app.MapGet("/Identity/Account/Login", () => "Sign in page at this endpoint.");

app.Run();

이전 코드는 RequireAuthorization을 사용하여 작성할 수 있습니다.

app.MapGet("/auth", () => "This endpoint requires authorization")
   .RequireAuthorization();

다음 샘플은 정책 기반 권한 부여를 사용합니다.

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using WebRPauth.Data;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthorization(o => o.AddPolicy("AdminsOnly", 
                                  b => b.RequireClaim("admin", "true")));

var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();

builder.Services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
    .AddEntityFrameworkStores<ApplicationDbContext>();

var app = builder.Build();

app.UseAuthorization();

app.MapGet("/admin", [Authorize("AdminsOnly")] () => 
                             "The /admin endpoint is for admins only.");

app.MapGet("/admin2", () => "The /admin2 endpoint is for admins only.")
   .RequireAuthorization("AdminsOnly");

app.MapGet("/", () => "This endpoint doesn't require authorization.");
app.MapGet("/Identity/Account/Login", () => "Sign in page at this endpoint.");

app.Run();

인증되지 않은 사용자가 엔드포인트에 액세스할 수 있도록 허용

[AllowAnonymous]는 인증되지 않은 사용자가 엔드포인트에 액세스할 수 있도록 허용합니다.

app.MapGet("/login", [AllowAnonymous] () => "This endpoint is for all roles.");


app.MapGet("/login2", () => "This endpoint also for all roles.")
   .AllowAnonymous();

CORS

경로는 CORS 정책을 사용하여 CORS를 사용할 수 있습니다. CORS는 [EnableCors] 특성을 통해 또는 RequireCors 메서드를 사용하여 선언할 수 있습니다. 다음 샘플은 CORS를 사용하도록 설정합니다.

const string MyAllowSpecificOrigins = "_myAllowSpecificOrigins";

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddCors(options =>
{
    options.AddPolicy(name: MyAllowSpecificOrigins,
                      builder =>
                      {
                          builder.WithOrigins("http://example.com",
                                              "http://www.contoso.com");
                      });
});

var app = builder.Build();
app.UseCors();

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

app.Run();
using Microsoft.AspNetCore.Cors;

const string MyAllowSpecificOrigins = "_myAllowSpecificOrigins";

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddCors(options =>
{
    options.AddPolicy(name: MyAllowSpecificOrigins,
                      builder =>
                      {
                          builder.WithOrigins("http://example.com",
                                              "http://www.contoso.com");
                      });
});

var app = builder.Build();
app.UseCors();

app.MapGet("/cors", [EnableCors(MyAllowSpecificOrigins)] () => 
                           "This endpoint allows cross origin requests!");
app.MapGet("/cors2", () => "This endpoint allows cross origin requests!")
                     .RequireCors(MyAllowSpecificOrigins);

app.Run();

자세한 내용은 ASP.NET Core에서 CORS(원본 간 요청) 사용을 참조하세요.

참고 항목

이 문서:

최소 API는 다음으로 구성됩니다.

WebApplication

다음 코드는 ASP.NET Core 템플릿에서 생성됩니다.

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

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

app.Run();

위의 코드는 명령줄에서 dotnet new web을 통해 만들거나 Visual Studio에서 빈 웹 템플릿을 선택하여 만들 수 있습니다.

다음 코드는 WebApplicationBuilder를 명시적으로 만들지 않고 WebApplication(app)을 만듭니다.

var app = WebApplication.Create(args);

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

app.Run();

WebApplication.Create는 미리 구성된 기본값을 사용하여 WebApplication 클래스의 새 인스턴스를 초기화합니다.

포트 작업

Visual Studio 또는 dotnet new를 사용하여 웹앱을 만들면 앱이 응답하는 포트를 지정하는 Properties/launchSettings.json 파일이 만들어집니다. 다음에 오는 포트 설정 샘플에서는 Visual Studio에서 앱을 실행하면 Unable to connect to web server 'AppName' 오류 대화 상자가 반환됩니다. 명령줄에서 다음 포트 변경 샘플을 실행합니다.

다음 섹션에서는 앱이 응답하는 포트를 설정합니다.

var app = WebApplication.Create(args);

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

app.Run("http://localhost:3000");

위의 코드에서 앱은 포트 3000에 응답합니다.

여러 포트

위의 코드에서 앱은 포트 30004000에 응답합니다.

var app = WebApplication.Create(args);

app.Urls.Add("http://localhost:3000");
app.Urls.Add("http://localhost:4000");

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

app.Run();

명령줄에서 포트 설정

다음 명령은 앱이 포트 7777에 응답하도록 합니다.

dotnet run --urls="https://localhost:7777"

Kestrel 엔드포인트가 appsettings.json 파일에도 구성된 경우 appsettings.json 파일에서 지정된 URL이 사용됩니다. 자세한 내용은 Kestrel 엔드포인트 구성을 참조하세요.

환경에서 포트 읽기

다음 코드는 환경에서 포트를 읽습니다.

var app = WebApplication.Create(args);

var port = Environment.GetEnvironmentVariable("PORT") ?? "3000";

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

app.Run($"http://localhost:{port}");

환경에서 포트를 설정하는 기본 방법은 다음 섹션에 나오는 ASPNETCORE_URLS 환경 변수를 사용하는 것입니다.

ASPNETCORE_URLS 환경 변수를 통해 포트 설정

ASPNETCORE_URLS 환경 변수를 사용하여 포트를 설정할 수 있습니다.

ASPNETCORE_URLS=http://localhost:3000

ASPNETCORE_URLS는 여러 URL을 지원합니다.

ASPNETCORE_URLS=http://localhost:3000;https://localhost:5000

모든 인터페이스에서 수신 대기

다음 샘플은 모든 인터페이스에서의 수신 대기를 보여 줍니다.

http://*:3000

var app = WebApplication.Create(args);

app.Urls.Add("http://*:3000");

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

app.Run();

http://+:3000

var app = WebApplication.Create(args);

app.Urls.Add("http://+:3000");

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

app.Run();

http://0.0.0.0:3000

var app = WebApplication.Create(args);

app.Urls.Add("http://0.0.0.0:3000");

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

app.Run();

ASPNETCORE_URLS를 사용하여 모든 인터페이스에서 수신 대기

이전 샘플은 ASPNETCORE_URLS를 사용할 수 있습니다.

ASPNETCORE_URLS=http://*:3000;https://+:5000;http://0.0.0.0:5005

개발 인증서로 HTTPS 지정

var app = WebApplication.Create(args);

app.Urls.Add("https://localhost:3000");

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

app.Run();

개발 인증서에 대한 자세한 내용은 Windows 및 macOS에서 ASP.NET Core HTTPS 개발 인증서 신뢰를 참조하세요.

사용자 지정 인증서를 사용하여 HTTPS 지정

다음 섹션에서는 파일 및 구성을 통해 사용자 지정 인증서를 지정하는 appsettings.json 방법을 보여 있습니다.

appsettings.json을 사용하여 사용자 지정 인증서 지정

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "Kestrel": {
    "Certificates": {
      "Default": {
        "Path": "cert.pem",
        "KeyPath": "key.pem"
      }
    }
  }
}

구성을 통해 사용자 지정 인증서 지정

var builder = WebApplication.CreateBuilder(args);

// Configure the cert and the key
builder.Configuration["Kestrel:Certificates:Default:Path"] = "cert.pem";
builder.Configuration["Kestrel:Certificates:Default:KeyPath"] = "key.pem";

var app = builder.Build();

app.Urls.Add("https://localhost:3000");

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

app.Run();

인증서 API 사용

using System.Security.Cryptography.X509Certificates;

var builder = WebApplication.CreateBuilder(args);

builder.WebHost.ConfigureKestrel(options =>
{
    options.ConfigureHttpsDefaults(httpsOptions =>
    {
        var certPath = Path.Combine(builder.Environment.ContentRootPath, "cert.pem");
        var keyPath = Path.Combine(builder.Environment.ContentRootPath, "key.pem");

        httpsOptions.ServerCertificate = X509Certificate2.CreateFromPemFile(certPath, 
                                         keyPath);
    });
});

var app = builder.Build();

app.Urls.Add("https://localhost:3000");

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

app.Run();

환경 읽기

var app = WebApplication.Create(args);

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/oops");
}

app.MapGet("/", () => "Hello World");
app.MapGet("/oops", () => "Oops! An error happened.");

app.Run();

환경 사용에 대한 자세한 내용은 ASP.NET Core에서 여러 환경 사용을 참조하세요.

구성

다음 코드는 구성 시스템에서 읽습니다.

var app = WebApplication.Create(args);

var message = app.Configuration["HelloKey"] ?? "Hello";

app.MapGet("/", () => message);

app.Run();

자세한 내용은 ASP.NET Core의 구성을 참조하세요.

로깅

다음 코드는 로그온 애플리케이션 시작에 메시지를 씁니다.

var app = WebApplication.Create(args);

app.Logger.LogInformation("The app started");

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

app.Run();

자세한 내용은 .NET Core 및 ASP.NET Core의 로깅을 참조하세요.

DI(종속성 주입) 컨테이너 액세스

다음 코드에서는 애플리케이션을 시작하는 동안 DI 컨테이너에서 서비스를 가져오는 방법을 보여줍니다.


var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();
builder.Services.AddScoped<SampleService>();

var app = builder.Build();

app.MapControllers();

using (var scope = app.Services.CreateScope())
{
    var sampleService = scope.ServiceProvider.GetRequiredService<SampleService>();
    sampleService.DoSomething();
}

app.Run();

자세한 내용은 ASP.NET Core에서 종속성 주입을 참조하세요.

WebApplicationBuilder

이 섹션에는 WebApplicationBuilder를 사용하는 샘플 코드가 포함되어 있습니다.

콘텐츠 루트, 애플리케이션 이름, 환경 변경

다음 코드는 콘텐츠 루트, 애플리케이션 이름, 환경을 설정합니다.

var builder = WebApplication.CreateBuilder(new WebApplicationOptions
{
    Args = args,
    ApplicationName = typeof(Program).Assembly.FullName,
    ContentRootPath = Directory.GetCurrentDirectory(),
    EnvironmentName = Environments.Staging,
    WebRootPath = "customwwwroot"
});

Console.WriteLine($"Application Name: {builder.Environment.ApplicationName}");
Console.WriteLine($"Environment Name: {builder.Environment.EnvironmentName}");
Console.WriteLine($"ContentRoot Path: {builder.Environment.ContentRootPath}");
Console.WriteLine($"WebRootPath: {builder.Environment.WebRootPath}");

var app = builder.Build();

WebApplication.CreateBuilder는 미리 구성된 기본값을 사용하여 WebApplicationBuilder 클래스의 새 인스턴스를 초기화합니다.

자세한 내용은 ASP.NET Core 기본 사항 개요를 참조하세요

환경 변수 또는 명령줄을 통해 콘텐츠 루트, 앱 이름, 환경 변경

다음 표는 콘텐츠 루트, 앱 이름, 환경 변경에 사용되는 환경 변수와 명령줄 인수를 보여 줍니다.

기능 환경 변수 명령줄 인수
애플리케이션 이름 ASPNETCORE_APPLICATIONNAME --applicationName
환경 이름 ASPNETCORE_ENVIRONMENT --environment
콘텐츠 루트 ASPNETCORE_CONTENTROOT --contentRoot

구성 공급자 추가

다음 샘플은 INI 구성 공급자를 추가합니다.

var builder = WebApplication.CreateBuilder(args);

builder.Configuration.AddIniFile("appsettings.ini");

var app = builder.Build();

자세한 내용은 ASP.NET Core의 구성에서 파일 구성 공급자를 참조하세요.

구성 읽기

기본적으로 WebApplicationBuilder는 다음을 포함하여 여러 원본에서 구성을 읽습니다.

  • appSettings.jsonappSettings.{environment}.json
  • 환경 변수
  • 명령줄

구성 소스 읽기의 전체 목록은 ASP.NET Core의 구성에서 기본 구성을 참조하세요.

다음 코드는 구성에서 HelloKey를 읽고 / 엔드포인트에 값을 표시합니다. 구성 값이 null이면 "Hello"가 message에 할당됩니다.

var builder = WebApplication.CreateBuilder(args);

var message = builder.Configuration["HelloKey"] ?? "Hello";

var app = builder.Build();

app.MapGet("/", () => message);

app.Run();

환경 읽기

var builder = WebApplication.CreateBuilder(args);

var message = builder.Configuration["HelloKey"] ?? "Hello";

var app = builder.Build();

app.MapGet("/", () => message);

app.Run();

로깅 공급자 추가

var builder = WebApplication.CreateBuilder(args);

// Configure JSON logging to the console.
builder.Logging.AddJsonConsole();

var app = builder.Build();

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

app.Run();

서비스 추가

var builder = WebApplication.CreateBuilder(args);

// Add the memory cache services.
builder.Services.AddMemoryCache();

// Add a custom scoped service.
builder.Services.AddScoped<ITodoRepository, TodoRepository>();
var app = builder.Build();

IHostBuilder 사용자 지정

IHostBuilder의 기존 확장 메서드는 호스트 속성을 사용하여 액세스할 수 있습니다.

var builder = WebApplication.CreateBuilder(args);

// Wait 30 seconds for graceful shutdown.
builder.Host.ConfigureHostOptions(o => o.ShutdownTimeout = TimeSpan.FromSeconds(30));

var app = builder.Build();

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

app.Run();

IWebHostBuilder 사용자 지정

IWebHostBuilder의 확장 메서드는 WebApplicationBuilder.WebHost 속성을 사용하여 액세스할 수 있습니다.

var builder = WebApplication.CreateBuilder(args);

// Change the HTTP server implemenation to be HTTP.sys based
builder.WebHost.UseHttpSys();

var app = builder.Build();

app.MapGet("/", () => "Hello HTTP.sys");

app.Run();

웹 루트 변경

기본적으로 웹 루트는 wwwroot 폴더의 콘텐츠 루트를 기준으로 합니다. 웹 루트는 정적 파일 미들웨어가 정적 파일을 찾는 위치입니다. 웹 루트는 WebHostOptions, 명령줄 또는 UseWebRoot 메서드를 사용하여 변경할 수 있습니다.

var builder = WebApplication.CreateBuilder(new WebApplicationOptions
{
    Args = args,
    // Look for static files in webroot
    WebRootPath = "webroot"
});

var app = builder.Build();

app.Run();

사용자 지정 DI(종속성 주입) 컨테이너

다음 예제에서는 Autofac을 사용합니다.

var builder = WebApplication.CreateBuilder(args);

builder.Host.UseServiceProviderFactory(new AutofacServiceProviderFactory());

// Register services directly with Autofac here. Don't
// call builder.Populate(), that happens in AutofacServiceProviderFactory.
builder.Host.ConfigureContainer<ContainerBuilder>(builder => builder.RegisterModule(new MyApplicationModule()));

var app = builder.Build();

미들웨어 추가

기존 ASP.NET Core 미들웨어는 WebApplication에서 구성할 수 있습니다.

var app = WebApplication.Create(args);

// Setup the file server to serve static files.
app.UseFileServer();

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

app.Run();

자세한 내용은 ASP.NET Core 미들웨어를 참조하세요.

개발자 예외 페이지

WebApplication.CreateBuilder는 미리 구성된 기본값을 사용하여 WebApplicationBuilder 클래스의 새 인스턴스를 초기화합니다. 개발자 예외 페이지는 미리 구성된 기본값에서 사용하도록 설정됩니다. 개발 환경에서 다음 코드가 실행되는 경우 /로 이동하면 예외를 보여 주는 친숙한 페이지가 렌더링됩니다.

var builder = WebApplication.CreateBuilder(args);

var app = builder.Build();

app.MapGet("/", () =>
{
    throw new InvalidOperationException("Oops, the '/' route has thrown an exception.");
});

app.Run();

ASP.NET Core 미들웨어 기본 사항

다음 표에는 최소 API와 함께 자주 사용되는 미들웨어 일부가 나와 있습니다.

미들웨어 설명 API
인증 인증 지원을 제공합니다. UseAuthentication
Authorization 권한 부여 지원을 제공합니다. UseAuthorization
CORS 원본 간 리소스 공유를 구성합니다. UseCors
예외 처리기 미들웨어 파이프라인이 throw하는 예외를 전역으로 처리합니다. UseExceptionHandler
전달된 헤더 프록시된 헤더를 현재 요청에 전달합니다. UseForwardedHeaders
HTTPS 리디렉션 모든 HTTP 요청을 HTTPS로 리디렉션합니다. UseHttpsRedirection
HSTS(HTTP 엄격한 전송 보안) 특별한 응답 헤더를 추가하는 보안 향상 미들웨어입니다. UseHsts
요청 로깅 HTTP 요청 및 응답 로깅을 지원합니다. UseHttpLogging
W3C 요청 로깅 W3C 형식의 HTTP 요청 및 응답 로깅을 지원합니다. UseW3CLogging
응답 캐싱 응답 캐시에 대한 지원을 제공합니다. UseResponseCaching
응답 압축 응답 압축에 대한 지원을 제공합니다. UseResponseCompression
세션 사용자 세션 관리에 대한 지원을 제공합니다. UseSession
정적 파일 정적 파일 및 디렉터리 검색 처리에 대한 지원을 제공합니다. UseStaticFiles, UseFileServer
WebSocket WebSocket 프로토콜을 활성화합니다. UseWebSockets

요청 처리

다음 섹션에서는 라우팅, 매개 변수 바인딩, 응답을 다룹니다.

라우팅

구성된 WebApplicationMap{Verb}MapMethods를 지원합니다.

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

app.MapGet("/", () => "This is a GET");
app.MapPost("/", () => "This is a POST");
app.MapPut("/", () => "This is a PUT");
app.MapDelete("/", () => "This is a DELETE");

app.MapMethods("/options-or-head", new[] { "OPTIONS", "HEAD" }, 
                          () => "This is an options or head request ");

app.Run();

경로 처리기

경로 처리기는 경로가 일치할 때 실행되는 메서드입니다. 경로 처리기는 동기 또는 비동기를 포함한 모든 형태의 함수일 수 있습니다. 경로 처리기는 람다 식, 로컬 함수, 인스턴스 메서드 또는 정적 메서드일 수 있습니다.

람다 식

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

app.MapGet("/inline", () => "This is an inline lambda");

var handler = () => "This is a lambda variable";

app.MapGet("/", handler);

app.Run();

로컬 함수

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

string LocalFunction() => "This is local function";

app.MapGet("/", LocalFunction);

app.Run();

인스턴스 메서드

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

var handler = new HelloHandler();

app.MapGet("/", handler.Hello);

app.Run();

class HelloHandler
{
    public string Hello()
    {
        return "Hello Instance method";
    }
}

정적 메서드

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

app.MapGet("/", HelloHandler.Hello);

app.Run();

class HelloHandler
{
    public static string Hello()
    {
        return "Hello static method";
    }
}

경로의 URL을 생성하기 위해 경로에 이름을 지정할 수 있습니다. 이름이 지정된 경로를 사용하면 앱에서 경로를 하드 코드로 작성할 필요가 없습니다.

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

app.MapGet("/hello", () => "Hello named route")
   .WithName("hi");

app.MapGet("/", (LinkGenerator linker) => 
        $"The link to the hello route is {linker.GetPathByName("hi", values: null)}");

app.Run();

이전 코드는 / 엔드포인트에서 The link to the hello endpoint is /hello를 표시합니다.

참고: 엔드포인트 이름은 대/소문자를 구분합니다.

엔드포인트 이름:

  • 전역적으로 고유해야 합니다.
  • OpenAPI 지원을 사용할 때 OpenAPI 작업 ID로 사용됩니다. 자세한 내용은 OpenAPI를 참조하세요.

경로 매개 변수

경로 패턴 정의의 일부로 경로 매개 변수를 캡처할 수 있습니다.

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

app.MapGet("/users/{userId}/books/{bookId}", 
    (int userId, int bookId) => $"The user id is {userId} and book id is {bookId}");

app.Run();

이전 코드는 /users/3/books/7 URI에서 The user id is 3 and book id is 7을 반환합니다.

경로 처리기는 캡처할 매개 변수를 선언할 수 있습니다. 캡처하도록 선언된 매개 변수와 함께 경로에 요청이 수행되면 매개 변수가 구문 분석되어 처리기에 전달됩니다. 이렇게 하면 형식이 안전한 방식으로 값을 쉽게 캡처할 수 있습니다. 이전 코드에서 userIdbookId는 모두 int입니다.

이전 코드에서 경로 값 중 하나를 int로 변환할 수 없는 경우 예외가 throw됩니다. GET 요청 /users/hello/books/3는 다음 예외를 throw합니다.

BadHttpRequestException: Failed to bind parameter "int userId" from "hello".

와일드카드 및 catch all 경로

다음 catch all 경로는 `/posts/hello` 엔드포인트에서 Routing to hello를 반환합니다.

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

app.MapGet("/posts/{*rest}", (string rest) => $"Routing to {rest}");

app.Run();

경로 제약 조건

경로 제약 조건은 경로의 일치 동작을 제한합니다.

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

app.MapGet("/todos/{id:int}", (int id) => db.Todos.Find(id));
app.MapGet("/todos/{text}", (string text) => db.Todos.Where(t => t.Text.Contains(text)));
app.MapGet("/posts/{slug:regex(^[a-z0-9_-]+$)}", (string slug) => $"Post {slug}");

app.Run();

다음 표는 이전 경로 템플릿과 동작을 보여 줍니다.

경로 템플릿 URI 일치 예제
/todos/{id:int} /todos/1
/todos/{text} /todos/something
/posts/{slug:regex(^[a-z0-9_-]+$)} /posts/mypost

자세한 내용은 ASP.NET Core의 라우팅에서 경로 제약 조건 참조를 참조하세요.

매개 변수 바인딩

매개 변수 바인딩은 경로 처리기가 표현하는 강력한 형식의 매개 변수로 요청 데이터를 변환하는 프로세스입니다. 바인딩 소스는 매개 변수가 바인딩되는 위치를 결정합니다. 바인딩 소스는 HTTP 메서드 및 매개 변수 형식에 따라 명시적이거나 유추될 수 있습니다.

지원되는 바인딩 소스:

  • 경로 값
  • 쿼리 문자열
  • 헤더
  • 본문(JSON 형식)
  • 종속성 주입에서 제공하는 서비스
  • 사용자 지정

참고 항목

양식 값에서 바인딩은 .NET에서 기본적으로 지원되지 않습니다.

다음 예제 GET 경로 처리기는 이러한 매개 변수 바인딩 소스 중 일부를 사용합니다.

var builder = WebApplication.CreateBuilder(args);

// Added as service
builder.Services.AddSingleton<Service>();

var app = builder.Build();

app.MapGet("/{id}", (int id,
                     int page,
                     [FromHeader(Name = "X-CUSTOM-HEADER")] string customHeader,
                     Service service) => { });

class Service { }

다음 표에서는 이전 예제에서 사용된 매개 변수와 연결된 바인딩 소스 간의 관계를 보여 줍니다.

매개 변수 바인딩 소스
id 경로 값
page 쿼리 문자열
customHeader 헤더
service 종속성 주입에서 제공

HTTP 메서드 GET, HEAD, OPTIONS, DELETE는 본문에서 암시적으로 바인딩되지 않습니다. 이러한 HTTP 메서드에 대해 본문(JSON 형식)에서 바인딩하려면 [FromBody]명시적으로 바인딩하거나 HttpRequest에서 읽습니다.

다음 예제 POST 경로 처리기는 person 매개 변수에 대해 본문(JSON 형식)의 바인딩 소스를 사용합니다.

var builder = WebApplication.CreateBuilder(args);

var app = builder.Build();

app.MapPost("/", (Person person) => { });

record Person(string Name, int Age);

이전 예제의 매개 변수는 모두 요청 데이터에서 자동으로 바인딩됩니다. 매개 변수 바인딩이 제공하는 편의를 설명하기 위해 다음 예제 경로 처리기는 요청에서 직접 요청 데이터를 읽는 방법을 보여 줍니다.

app.MapGet("/{id}", (HttpRequest request) =>
{
    var id = request.RouteValues["id"];
    var page = request.Query["page"];
    var customHeader = request.Headers["X-CUSTOM-HEADER"];

    // ...
});

app.MapPost("/", async (HttpRequest request) =>
{
    var person = await request.ReadFromJsonAsync<Person>();

    // ...
});

명시적 매개 변수 바인딩

특성을 사용하여 매개 변수가 바인딩되는 위치를 명시적으로 선언할 수 있습니다.

using Microsoft.AspNetCore.Mvc;

var builder = WebApplication.CreateBuilder(args);

// Added as service
builder.Services.AddSingleton<Service>();

var app = builder.Build();


app.MapGet("/{id}", ([FromRoute] int id,
                     [FromQuery(Name = "p")] int page,
                     [FromServices] Service service,
                     [FromHeader(Name = "Content-Type")] string contentType) 
                     => {});

class Service { }

record Person(string Name, int Age);
매개 변수 바인딩 소스
id 이름이 id인 경로 값
page 이름이 "p"인 쿼리 문자열
service 종속성 주입에서 제공
contentType 이름이 "Content-Type"인 헤더

참고 항목

양식 값에서 바인딩은 .NET에서 기본적으로 지원되지 않습니다.

DI를 사용한 매개 변수 바인딩

최소 API에 대한 매개 변수 바인딩은 형식이 서비스로 구성된 경우 종속성 주입을 통해 매개 변수를 바인딩합니다. 매개 변수에 [FromServices] 특성을 명시적으로 적용할 필요는 없습니다. 다음 코드에서는 두 작업 모두 시간을 반환합니다.

using Microsoft.AspNetCore.Mvc;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton<IDateTime, SystemDateTime>();

var app = builder.Build();

app.MapGet("/",   (               IDateTime dateTime) => dateTime.Now);
app.MapGet("/fs", ([FromServices] IDateTime dateTime) => dateTime.Now);
app.Run();

선택적 매개 변수

경로 처리기에서 선언된 매개 변수는 필수로 처리됩니다.

  • 요청이 경로와 일치하는 경우 요청에 모든 필수 매개 변수가 제공되는 경우에만 경로 처리기가 실행됩니다.
  • 모든 필수 매개 변수를 제공하지 못하면 오류가 발생합니다.
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/products", (int pageNumber) => $"Requesting page {pageNumber}");

app.Run();
URI result
/products?pageNumber=3 3 반환됨
/products BadHttpRequestException: 필수 매개 변수 "int pageNumber"가 쿼리 문자열에서 제공되지 않았습니다.
/products/1 HTTP 404 오류, 일치하는 경로 없음

pageNumber를 선택 사항으로 지정하려면 형식을 선택 사항으로 정의하거나 기본값을 제공합니다.

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

app.MapGet("/products", (int? pageNumber) => $"Requesting page {pageNumber ?? 1}");

string ListProducts(int pageNumber = 1) => $"Requesting page {pageNumber}";

app.MapGet("/products2", ListProducts);

app.Run();
URI result
/products?pageNumber=3 3 반환됨
/products 1 반환됨
/products2 1 반환됨

이전 null 허용 값과 기본값은 모든 원본에 적용됩니다.

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

app.MapPost("/products", (Product? product) => { });

app.Run();

이전 코드는 요청 본문을 보내지 않은 경우 null 곱을 사용하여 메서드를 호출합니다.

참고: 잘못된 데이터를 제공하고 매개 변수가 null을 허용하면 경로 처리기가 실행되지 않습니다.

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

app.MapGet("/products", (int? pageNumber) => $"Requesting page {pageNumber ?? 1}");

app.Run();
URI result
/products?pageNumber=3 3 반환됨
/products 1 반환됨
/products?pageNumber=two BadHttpRequestException: "2"에서 "Nullable<int> pageNumber" 매개 변수를 바인딩하지 못했습니다.
/products/two HTTP 404 오류, 일치하는 경로 없음

자세한 내용은 바인딩 실패 섹션을 참조하세요.

특수 형식

다음 형식은 명시적 특성 없이 바인딩됩니다.

  • HttpContext: 현재 HTTP 요청 또는 응답에 대한 모든 정보를 포함하는 컨텍스트입니다.

    app.MapGet("/", (HttpContext context) => context.Response.WriteAsync("Hello World"));
    
  • HttpRequestHttpResponse: HTTP 요청 및 HTTP 응답:

    app.MapGet("/", (HttpRequest request, HttpResponse response) =>
        response.WriteAsync($"Hello World {request.Query["name"]}"));
    
  • CancellationToken: 현재 HTTP 요청에 연결된 취소 토큰:

    app.MapGet("/", async (CancellationToken cancellationToken) => 
        await MakeLongRunningRequestAsync(cancellationToken));
    
  • ClaimsPrincipal: HttpContext.User에서 바인딩된 요청에 연결된 사용자:

    app.MapGet("/", (ClaimsPrincipal user) => user.Identity.Name);
    

사용자 지정 바인딩

매개 변수 바인딩을 사용자 지정하는 방법에는 다음 두 가지가 있습니다.

  1. 경로, 쿼리, 헤더 바인딩 소스의 경우 형식의 정적 TryParse 메서드를 추가하여 사용자 지정 형식을 바인딩합니다.
  2. 형식에서 BindAsync 메서드를 구현하여 바인딩 프로세스를 제어합니다.

TryParse

TryParse에는 다음 두 가지 API가 있습니다.

public static bool TryParse(string value, out T result);
public static bool TryParse(string value, IFormatProvider provider, out T result);

다음 코드는 URI가 /map?Point=12.3,10.1Point: 12.3, 10.1을 표시합니다.

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

// GET /map?Point=12.3,10.1
app.MapGet("/map", (Point point) => $"Point: {point.X}, {point.Y}");

app.Run();

public class Point
{
    public double X { get; set; }
    public double Y { get; set; }

    public static bool TryParse(string? value, IFormatProvider? provider,
                                out Point? point)
    {
        // Format is "(12.3,10.1)"
        var trimmedValue = value?.TrimStart('(').TrimEnd(')');
        var segments = trimmedValue?.Split(',',
                StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
        if (segments?.Length == 2
            && double.TryParse(segments[0], out var x)
            && double.TryParse(segments[1], out var y))
        {
            point = new Point { X = x, Y = y };
            return true;
        }

        point = null;
        return false;
    }
}

BindAsync

BindAsync에는 다음 API가 있습니다.

public static ValueTask<T?> BindAsync(HttpContext context, ParameterInfo parameter);
public static ValueTask<T?> BindAsync(HttpContext context);

다음 코드는 URI가 /products?SortBy=xyz&SortDir=Desc&Page=99SortBy:xyz, SortDirection:Desc, CurrentPage:99을 표시합니다.

using System.Reflection;

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

// GET /products?SortBy=xyz&SortDir=Desc&Page=99
app.MapGet("/products", (PagingData pageData) => $"SortBy:{pageData.SortBy}, " +
       $"SortDirection:{pageData.SortDirection}, CurrentPage:{pageData.CurrentPage}");

app.Run();

public class PagingData
{
    public string? SortBy { get; init; }
    public SortDirection SortDirection { get; init; }
    public int CurrentPage { get; init; } = 1;

    public static ValueTask<PagingData?> BindAsync(HttpContext context,
                                                   ParameterInfo parameter)
    {
        const string sortByKey = "sortBy";
        const string sortDirectionKey = "sortDir";
        const string currentPageKey = "page";

        Enum.TryParse<SortDirection>(context.Request.Query[sortDirectionKey],
                                     ignoreCase: true, out var sortDirection);
        int.TryParse(context.Request.Query[currentPageKey], out var page);
        page = page == 0 ? 1 : page;

        var result = new PagingData
        {
            SortBy = context.Request.Query[sortByKey],
            SortDirection = sortDirection,
            CurrentPage = page
        };

        return ValueTask.FromResult<PagingData?>(result);
    }
}

public enum SortDirection
{
    Default,
    Asc,
    Desc
}

바인딩 실패

바인딩이 실패할 경우 프레임워크는 디버그 메시지를 로그하고 실패 모드에 따라 다양한 상태 코드를 클라이언트에 반환합니다.

실패 모드 null 허용 매개 변수 형식 바인딩 소스 상태 코드
{ParameterType}.TryParsefalse를 반환합니다 route/query/header 400
{ParameterType}.BindAsyncnull를 반환합니다 custom 400
{ParameterType}.BindAsync가 throw 중요하지 않음 custom 500
JSON 본문을 역직렬화하지 못함 중요하지 않음 본문 400
잘못된 콘텐츠 형식(application/json이 아님) 중요하지 않음 본문 415

바인딩 우선 순위

매개 변수에서 바인딩 소스를 결정하는 규칙:

  1. 매개 변수(From* 특성)에 다음 순서로 정의된 명시적 특성:
    1. 경로 값: [FromRoute]
    2. 쿼리 문자열: [FromQuery]
    3. 헤더: [FromHeader]
    4. 본문: [FromBody]
    5. 서비스: [FromServices]
  2. 특수 형식
    1. HttpContext
    2. HttpRequest (HttpContext.Request)
    3. HttpResponse (HttpContext.Response)
    4. ClaimsPrincipal (HttpContext.User)
    5. CancellationToken (HttpContext.RequestAborted)
  3. 매개 변수 형식에 유효한 BindAsync 메서드가 있습니다.
  4. 매개 변수 형식이 문자열이거나 매개 변수 형식에 유효한 TryParse 메서드가 있습니다.
    1. 매개 변수 이름이 경로 템플릿(예: app.Map("/todo/{id}", (int id) => {});)에 있는 경우 경로에서 바인딩됩니다.
    2. 쿼리 문자열에서 바인딩됩니다.
  5. 매개 변수 형식이 종속성 주입에서 제공되는 서비스인 경우 해당 서비스를 원본으로 사용합니다.
  6. 매개 변수가 본문의 매개 변수입니다.

JSON 바인딩 사용자 지정

본문 바인딩 소스는 역직렬화에 System.Text.Json을 사용합니다. 이 기본값은 변경할 수 없지만 앞서 설명한 다른 방법을 사용하여 사용자 지정할 수 있습니다. JSON 직렬 변환기 옵션을 사용자 지정하려면 다음과 유사한 코드를 사용하세요.

using Microsoft.AspNetCore.Http.Json;

var builder = WebApplication.CreateBuilder(args);

// Configure JSON options.
builder.Services.Configure<JsonOptions>(options =>
{
    options.SerializerOptions.IncludeFields = true;
});

var app = builder.Build();

app.MapPost("/products", (Product product) => product);

app.Run();

class Product
{
    // These are public fields, not properties.
    public int Id;
    public string? Name;
}

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

  • 입력 및 출력 기본 JSON 옵션을 모두 구성합니다.
  • 다음 ON을 반환합니다.JS
    {
      "id": 1,
      "name": "Joe Smith"
    }
    
    게시할 때
    {
      "Id": 1,
      "Name": "Joe Smith"
    }
    

요청 본문 읽기

HttpContext 또는 HttpRequest 매개 변수를 사용하여 직접 요청 본문을 읽습니다.

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

app.MapPost("/uploadstream", async (IConfiguration config, HttpRequest request) =>
{
    var filePath = Path.Combine(config["StoredFilesPath"], Path.GetRandomFileName());

    await using var writeStream = File.Create(filePath);
    await request.BodyReader.CopyToAsync(writeStream);
});

app.Run();

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

  • HttpRequest.BodyReader를 사용하여 요청 본문에 액세스합니다.
  • 요청 본문을 로컬 파일에 복사합니다.

응답

경로 처리기는 다음과 같은 형식의 반환 값을 지원합니다.

  1. IResult 기반 - 여기에는 Task<IResult>ValueTask<IResult>가 포함됩니다.
  2. string - 여기에는 Task<string>ValueTask<string>이 포함됩니다.
  3. T(다른 모든 형식) - 여기에는 Task<T>ValueTask<T>가 포함됩니다.
반환 값 동작 콘텐츠-형식
IResult 프레임워크가 IResult.ExecuteAsync를 호출합니다. IResult 구현에 의해 결정됩니다.
string 프레임워크가 응답에 직접 문자열을 씁니다. text/plain
T(기타 형식) 프레임워크가 응답을 JSON 직렬화합니다. application/json

예시 반환 값

문자열 반환 값

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

JSON 반환 값

app.MapGet("/hello", () => new { Message = "Hello World" });

IResult 반환 값

app.MapGet("/hello", () => Results.Ok(new { Message = "Hello World" }));

다음 예는 기본 제공 결과 형식을 사용하여 응답을 사용자 지정합니다.

app.MapGet("/api/todoitems/{id}", async (int id, TodoDb db) =>
         await db.Todos.FindAsync(id) 
         is Todo todo
         ? Results.Ok(todo) 
         : Results.NotFound())
   .Produces<Todo>(StatusCodes.Status200OK)
   .Produces(StatusCodes.Status404NotFound);
JS에
app.MapGet("/hello", () => Results.Json(new { Message = "Hello World" }));
사용자 지정 상태 코드
app.MapGet("/405", () => Results.StatusCode(405));
Text
app.MapGet("/text", () => Results.Text("This is some text"));
스트림
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

var proxyClient = new HttpClient();
app.MapGet("/pokemon", async () => 
{
    var stream = await proxyClient.GetStreamAsync("http://contoso/pokedex.json");
    // Proxy the response as JSON
    return Results.Stream(stream, "application/json");
});

app.Run();
리디렉션
app.MapGet("/old-path", () => Results.Redirect("/new-path"));
파일
app.MapGet("/download", () => Results.File("myfile.text"));

기본 제공 결과

Microsoft.AspNetCore.Http.Results 정적 클래스에는 일반적인 결과 도우미가 있습니다.

설명 응답 형식 상태 코드 API
고급 옵션을 사용하여 JSON 응답 쓰기 application/json 200 Results.Json
JSON 응답 쓰기 application/json 200 Results.Ok
텍스트 응답 쓰기 text/plain(기본값), 구성 가능 200 Results.Text
응답을 바이트로 쓰기 application/octet-stream(기본값), 구성 가능 200 Results.Bytes
응답에 바이트 스트림 쓰기 application/octet-stream(기본값), 구성 가능 200 Results.Stream
콘텐츠 처리 헤더로 다운로드하기 위해 파일을 응답으로 스트리밍 application/octet-stream(기본값), 구성 가능 200 Results.File
선택적 JSON 응답을 사용하여 상태 코드를 404로 설정 해당 없음 404 Results.NotFound
상태 코드를 204로 설정 해당 없음 204 Results.NoContent
선택적 JSON 응답을 사용하여 상태 코드를 422로 설정 해당 없음 422 Results.UnprocessableEntity
선택적 JSON 응답을 사용하여 상태 코드를 400으로 설정 해당 없음 400 Results.BadRequest
선택적 JSON 응답을 사용하여 상태 코드를 409로 설정 해당 없음 409 Results.Conflict
응답에 문제 세부 정보 JSON 개체 쓰기 해당 없음 500(기본값), 구성 가능 Results.Problem
유효성 검사 오류와 함께 응답에 문제 세부 정보 JSON 개체 쓰기 해당 없음 N/A, 구성 가능 Results.ValidationProblem

결과 사용자 지정

애플리케이션은 사용자 지정 IResult 형식을 구현하여 응답을 제어할 수 있습니다. 다음 코드는 HTML 결과 형식의 예입니다.

using System.Net.Mime;
using System.Text;
static class ResultsExtensions
{
    public static IResult Html(this IResultExtensions resultExtensions, string html)
    {
        ArgumentNullException.ThrowIfNull(resultExtensions);

        return new HtmlResult(html);
    }
}

class HtmlResult : IResult
{
    private readonly string _html;

    public HtmlResult(string html)
    {
        _html = html;
    }

    public Task ExecuteAsync(HttpContext httpContext)
    {
        httpContext.Response.ContentType = MediaTypeNames.Text.Html;
        httpContext.Response.ContentLength = Encoding.UTF8.GetByteCount(_html);
        return httpContext.Response.WriteAsync(_html);
    }
}

이러한 사용자 지정 결과를 더 검색 가능하게 만들려면 Microsoft.AspNetCore.Http.IResultExtensions에 확장 메서드를 추가하는 것이 좋습니다.

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

app.MapGet("/html", () => Results.Extensions.Html(@$"<!doctype html>
<html>
    <head><title>miniHTML</title></head>
    <body>
        <h1>Hello World</h1>
        <p>The time on the server is {DateTime.Now:O}</p>
    </body>
</html>"));

app.Run();

권한 부여

권한 부여 정책을 사용하여 경로를 보호할 수 있습니다. 권한 부여 정책은 [Authorize] 특성을 통해 또는 RequireAuthorization 메서드를 사용하여 선언할 수 있습니다.

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using WebRPauth.Data;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthorization(o => o.AddPolicy("AdminsOnly", 
                                  b => b.RequireClaim("admin", "true")));

var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();

builder.Services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
    .AddEntityFrameworkStores<ApplicationDbContext>();

var app = builder.Build();

app.UseAuthorization();

app.MapGet("/auth", [Authorize] () => "This endpoint requires authorization.");
app.MapGet("/", () => "This endpoint doesn't require authorization.");
app.MapGet("/Identity/Account/Login", () => "Sign in page at this endpoint.");

app.Run();

이전 코드는 RequireAuthorization을 사용하여 작성할 수 있습니다.

app.MapGet("/auth", () => "This endpoint requires authorization")
   .RequireAuthorization();

다음 샘플은 정책 기반 권한 부여를 사용합니다.

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using WebRPauth.Data;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthorization(o => o.AddPolicy("AdminsOnly", 
                                  b => b.RequireClaim("admin", "true")));

var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();

builder.Services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
    .AddEntityFrameworkStores<ApplicationDbContext>();

var app = builder.Build();

app.UseAuthorization();

app.MapGet("/admin", [Authorize("AdminsOnly")] () => 
                             "The /admin endpoint is for admins only.");

app.MapGet("/admin2", () => "The /admin2 endpoint is for admins only.")
   .RequireAuthorization("AdminsOnly");

app.MapGet("/", () => "This endpoint doesn't require authorization.");
app.MapGet("/Identity/Account/Login", () => "Sign in page at this endpoint.");

app.Run();

인증되지 않은 사용자가 엔드포인트에 액세스할 수 있도록 허용

[AllowAnonymous]는 인증되지 않은 사용자가 엔드포인트에 액세스할 수 있도록 허용합니다.

app.MapGet("/login", [AllowAnonymous] () => "This endpoint is for all roles.");


app.MapGet("/login2", () => "This endpoint also for all roles.")
   .AllowAnonymous();

CORS

경로는 CORS 정책을 사용하여 CORS를 사용할 수 있습니다. CORS는 [EnableCors] 특성을 통해 또는 RequireCors 메서드를 사용하여 선언할 수 있습니다. 다음 샘플은 CORS를 사용하도록 설정합니다.

const string MyAllowSpecificOrigins = "_myAllowSpecificOrigins";

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddCors(options =>
{
    options.AddPolicy(name: MyAllowSpecificOrigins,
                      builder =>
                      {
                          builder.WithOrigins("http://example.com",
                                              "http://www.contoso.com");
                      });
});

var app = builder.Build();
app.UseCors();

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

app.Run();
using Microsoft.AspNetCore.Cors;

const string MyAllowSpecificOrigins = "_myAllowSpecificOrigins";

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddCors(options =>
{
    options.AddPolicy(name: MyAllowSpecificOrigins,
                      builder =>
                      {
                          builder.WithOrigins("http://example.com",
                                              "http://www.contoso.com");
                      });
});

var app = builder.Build();
app.UseCors();

app.MapGet("/cors", [EnableCors(MyAllowSpecificOrigins)] () => 
                           "This endpoint allows cross origin requests!");
app.MapGet("/cors2", () => "This endpoint allows cross origin requests!")
                     .RequireCors(MyAllowSpecificOrigins);

app.Run();

자세한 내용은 ASP.NET Core에서 CORS(원본 간 요청) 사용을 참조하세요.

참고 항목

최소 API에서 OpenAPI 지원