Share via


최소 API 앱의 매개 변수 바인딩

매개 변수 바인딩은 경로 처리기가 표현하는 강력한 형식의 매개 변수로 요청 데이터를 변환하는 프로세스입니다. 바인딩 소스는 매개 변수가 바인딩되는 위치를 결정합니다. 바인딩 소스는 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를 사용하여 요청 본문에 액세스합니다.
  • 요청 본문을 로컬 파일에 복사합니다.

매개 변수 바인딩은 경로 처리기가 표현하는 강력한 형식의 매개 변수로 요청 데이터를 변환하는 프로세스입니다. 바인딩 소스는 매개 변수가 바인딩되는 위치를 결정합니다. 바인딩 소스는 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를 사용하여 요청 본문에 액세스합니다.
  • 요청 본문을 로컬 파일에 복사합니다.