다음을 통해 공유


최소 API 앱에서 응답을 만드는 방법

참고 항목

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

Warning

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

Important

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

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

최소 엔드포인트는 다음과 같은 형식의 반환 값을 지원합니다.

  1. string - 여기에는 Task<string>ValueTask<string>가 포함됩니다.
  2. T(다른 모든 형식) - 여기에는 Task<T>ValueTask<T>가 포함됩니다.
  3. IResult 기반 - 여기에는 Task<IResult>ValueTask<IResult>가 포함됩니다.

string 반환 값

동작 콘텐츠-형식
프레임워크가 응답에 직접 문자열을 씁니다. text/plain

Hello world 텍스트를 반환하는 다음 경로 처리기를 고려합니다.

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

200 상태 코드는 text/plain Content-Type 헤더 및 다음 콘텐츠와 함께 반환됩니다.

Hello World

T (다른 모든 형식) 반환 값

동작 콘텐츠-형식
프레임워크 JSON은 응답을 직렬화합니다. application/json

Message 문자열 속성을 포함하는 익명 형식을 반환하는 다음 경로 처리기를 고려합니다.

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

200 상태 코드는 application/json Content-Type 헤더 및 다음 콘텐츠와 함께 반환됩니다.

{"message":"Hello World"}

IResult 반환 값

동작 콘텐츠-형식
프레임워크가 IResult.ExecuteAsync를 호출합니다. IResult 구현에 의해 결정됩니다.

IResult 인터페이스는 HTTP 엔드포인트의 결과를 나타내는 계약을 정의합니다. 정적 Results 클래스 및 정적 TypedResults는 다양한 유형의 응답을 나타내는 다양한 IResult 개체를 만드는 데 사용됩니다.

TypedResults 및 Results

ResultsTypedResults 정적 클래스는 유사한 결과 도우미 집합을 제공합니다. 클래스는 TypedResults 형식화된 클래스와 Results 동일합니다. 그러나, Results 도우미의 반환 형식은 IResult이지만 각 TypedResults 도우미의 반환 형식은 IResult 구현 형식 중 하나입니다. 이 차이는 Results 도우미의 경우 단위 테스트와 같이 구체적인 형식이 필요할 때 변환이 필요하다는 것을 의미합니다. 구현 형식은 Microsoft.AspNetCore.Http.HttpResults 네임스페이스에 정의됩니다.

TypedResults 반환보다는 Results 다음과 같은 장점이 있습니다.

  • TypedResults 도우미는 강력한 형식의 개체를 반환하여 코드 가독성, 단위 테스트를 개선하고 런타임 오류 가능성을 줄일 수 있습니다.
  • 구현 형식 은 엔드포인트를 설명하기 위해 OpenAPI 에 대한 응답 형식 메타데이터를 자동으로 제공합니다.

예상되는 JSON 응답이 있는 상태 코드가 200 OK 생성되는 다음 엔드포인트를 고려합니다.

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

이 엔드포인트를 올바르게 문서화하기 위해 확장 메서드 Produces이 호출됩니다. 그러나 다음 코드와 같이 TypedResultsResults 대신 사용되는 경우 Produces를 호출할 필요는 없습니다. TypedResults는 엔드포인트에 대한 메타데이터를 자동으로 제공합니다.

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

응답 유형을 설명하는 방법에 대한 자세한 내용은 최소 API에서 OpenAPI 지원을 참조하세요.

앞에서 설명한 것처럼, 사용할 TypedResults때 변환이 필요하지 않습니다. 클래스를 반환하는 다음 최소 API를 고려합니다.TypedResults

public static async Task<Ok<Todo[]>> GetAllTodos(TodoGroupDbContext database)
{
    var todos = await database.Todos.ToArrayAsync();
    return TypedResults.Ok(todos);
}

다음 테스트는 전체 구체적인 형식을 확인합니다.

[Fact]
public async Task GetAllReturnsTodosFromDatabase()
{
    // Arrange
    await using var context = new MockDb().CreateDbContext();

    context.Todos.Add(new Todo
    {
        Id = 1,
        Title = "Test title 1",
        Description = "Test description 1",
        IsDone = false
    });

    context.Todos.Add(new Todo
    {
        Id = 2,
        Title = "Test title 2",
        Description = "Test description 2",
        IsDone = true
    });

    await context.SaveChangesAsync();

    // Act
    var result = await TodoEndpointsV1.GetAllTodos(context);

    //Assert
    Assert.IsType<Ok<Todo[]>>(result);
    
    Assert.NotNull(result.Value);
    Assert.NotEmpty(result.Value);
    Assert.Collection(result.Value, todo1 =>
    {
        Assert.Equal(1, todo1.Id);
        Assert.Equal("Test title 1", todo1.Title);
        Assert.False(todo1.IsDone);
    }, todo2 =>
    {
        Assert.Equal(2, todo2.Id);
        Assert.Equal("Test title 2", todo2.Title);
        Assert.True(todo2.IsDone);
    });
}

모든 메서드 Results 가 서명에서 반환 IResult 되므로 컴파일러는 단일 엔드포인트에서 다른 결과를 반환할 때 자동으로 요청 대리자 반환 형식으로 유추합니다. TypedResults 에는 이러한 대리자의 Results<T1, TN> 사용이 필요합니다.

다음 메서드는 반환된 개체의 실제 구체적인 형식이 다르더라도 둘 다 Results.Ok Results.NotFound 반환 IResult으로 선언되기 때문에 컴파일됩니다.

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

다른 형식을 반환하는 것으로 선언되고 TypedResults.NotFound 컴파일러가 가장 일치하는 형식을 유추하려고 시도하지 않으므로 다음 메서드는 컴파일 TypedResults.Ok 되지 않습니다.

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

사용 TypedResults하려면 비동기에서 래퍼가 필요한 Task<> 경우 반환 형식을 완전히 선언해야 합니다. 사용 TypedResults 은 더 자세한 내용이지만 형식 정보를 정적으로 사용할 수 있으므로 OpenAPI에 대해 자체 설명할 수 있게 하는 것이 절호입니다.

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

Results<TResult1, TResultN>

다음과 같은 경우, IResult 대신 엔드포인트 처리기 반환 형식으로 Results<TResult1, TResultN>을 사용합니다.

  • 엔드포인트 처리기에서 여러 IResult 구현 형식이 반환됩니다.
  • 정적 TypedResult 클래스는 IResult 개체를 만드는 데 사용됩니다.

이 대안은 제네릭 공용 구조체 형식이 엔드포인트 메타데이터를 자동으로 유지하므로 IResult을 반환하는 것보다 좋습니다. Results<TResult1, TResultN> 공용 구조체 암시적 캐스트 연산자를 구현하므로 컴파일러는 제네릭 인수에 지정된 형식을 공용 구조체 형식의 인스턴스로 자동 변환할 수 있습니다.

이렇게 하면 경로 처리기가 실제로 선언한 결과만 반환한다는 컴파일 시간 검사를 제공하는 이점이 추가됩니다. 제네릭 인수 중 하나로 선언되지 않은 형식을 Results<>로 반환하려고 시도하면 컴파일 오류가 발생합니다.

orderId999보다 클 때 400 BadRequest 상태 코드가 반환되는 다음 엔드포인트를 고려합니다. 그렇지 않으면 예상 콘텐츠가 포함된 200 OK을 생성합니다.

app.MapGet("/orders/{orderId}", IResult (int orderId)
    => orderId > 999 ? TypedResults.BadRequest() : TypedResults.Ok(new Order(orderId)))
    .Produces(400)
    .Produces<Order>();

이 엔드포인트를 올바르게 문서화하기 위해 확장 메서드 Produces이 호출됩니다. 그러나 TypedResults 도우미는 엔드포인트에 대한 메타데이터를 자동으로 포함하므로 다음 코드와 같이 대신 Results<T1, Tn> 공용 구조체 형식을 반환할 수 있습니다.

app.MapGet("/orders/{orderId}", Results<BadRequest, Ok<Order>> (int orderId)
    => orderId > 999 ? TypedResults.BadRequest() : TypedResults.Ok(new Order(orderId)));

기본 제공 결과

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

다음 섹션에서는 일반적인 결과 도우미의 사용을 보여줍니다.

JSON

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

WriteAsJsonAsync 는 JSON을 반환하는 다른 방법입니다.

app.MapGet("/", (HttpContext context) => context.Response.WriteAsJsonAsync
    (new { Message = "Hello World" }));

사용자 지정 상태 코드

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

Internal Server Error

app.MapGet("/500", () => Results.InternalServerError("Something went wrong!"));

앞의 예제에서는 500 상태 코드를 반환합니다.

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

Results.Stream 오버로드는 버퍼링 없이 기본 HTTP 응답 스트림에 대한 액세스를 허용합니다. 다음 예제에서는 ImageSharp를 사용하여 지정된 이미지의 축소된 크기를 반환합니다.

using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Formats.Jpeg;
using SixLabors.ImageSharp.Processing;

var builder = WebApplication.CreateBuilder(args);

var app = builder.Build();

app.MapGet("/process-image/{strImage}", (string strImage, HttpContext http, CancellationToken token) =>
{
    http.Response.Headers.CacheControl = $"public,max-age={TimeSpan.FromHours(24).TotalSeconds}";
    return Results.Stream(stream => ResizeImageAsync(strImage, stream, token), "image/jpeg");
});

async Task ResizeImageAsync(string strImage, Stream stream, CancellationToken token)
{
    var strPath = $"wwwroot/img/{strImage}";
    using var image = await Image.LoadAsync(strPath, token);
    int width = image.Width / 2;
    int height = image.Height / 2;
    image.Mutate(x =>x.Resize(width, height));
    await image.SaveAsync(stream, JpegFormat.Instance, cancellationToken: token);
}

다음 예제에서는 Azure Blob Storage에서 이미지를 스트리밍합니다.

app.MapGet("/stream-image/{containerName}/{blobName}", 
    async (string blobName, string containerName, CancellationToken token) =>
{
    var conStr = builder.Configuration["blogConStr"];
    BlobContainerClient blobContainerClient = new BlobContainerClient(conStr, containerName);
    BlobClient blobClient = blobContainerClient.GetBlobClient(blobName);
    return Results.Stream(await blobClient.OpenReadAsync(cancellationToken: token), "image/jpeg");
});

다음 예제에서는 Azure Blob에서 비디오를 스트리밍합니다.

// GET /stream-video/videos/earth.mp4
app.MapGet("/stream-video/{containerName}/{blobName}",
     async (HttpContext http, CancellationToken token, string blobName, string containerName) =>
{
    var conStr = builder.Configuration["blogConStr"];
    BlobContainerClient blobContainerClient = new BlobContainerClient(conStr, containerName);
    BlobClient blobClient = blobContainerClient.GetBlobClient(blobName);
    
    var properties = await blobClient.GetPropertiesAsync(cancellationToken: token);
    
    DateTimeOffset lastModified = properties.Value.LastModified;
    long length = properties.Value.ContentLength;
    
    long etagHash = lastModified.ToFileTime() ^ length;
    var entityTag = new EntityTagHeaderValue('\"' + Convert.ToString(etagHash, 16) + '\"');
    
    http.Response.Headers.CacheControl = $"public,max-age={TimeSpan.FromHours(24).TotalSeconds}";

    return Results.Stream(await blobClient.OpenReadAsync(cancellationToken: token), 
        contentType: "video/mp4",
        lastModified: lastModified,
        entityTag: entityTag,
        enableRangeProcessing: true);
});

리디렉션

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

파일

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

HttpResult 인터페이스

Microsoft.AspNetCore.Http 네임스페이스의 다음과 같은 인터페이스는 필터 구현의 일반적 유형으로서 런타임 시 IResult 형식을 검색하는 방법을 제공합니다.

다음은 이러한 인터페이스 중 하나를 사용하는 필터의 예입니다.

app.MapGet("/weatherforecast", (int days) =>
{
    if (days <= 0)
    {
        return Results.BadRequest();
    }

    var forecast = Enumerable.Range(1, days).Select(index =>
       new WeatherForecast(DateTime.Now.AddDays(index), Random.Shared.Next(-20, 55), "Cool"))
        .ToArray();
    return Results.Ok(forecast);
}).
AddEndpointFilter(async (context, next) =>
{
    var result = await next(context);

    return result switch
    {
        IValueHttpResult<WeatherForecast[]> weatherForecastResult => new WeatherHttpResult(weatherForecastResult.Value),
        _ => result
    };
});

자세한 내용은 최소 API 앱의 필터IResult 구현 형식을 참조하세요.

응답 사용자 지정

애플리케이션은 사용자 지정 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 형식은 IEndpointMetadataProvider 인터페이스를 구현하여 자체 주석을 제공할 수 있습니다. 예를 들어 다음 코드는 엔드포인트에서 생성된 응답을 설명하는 주석을 HtmlResult 이전 형식에 추가합니다.

class HtmlResult : IResult, IEndpointMetadataProvider
{
    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);
    }

    public static void PopulateMetadata(MethodInfo method, EndpointBuilder builder)
    {
        builder.Metadata.Add(new ProducesHtmlMetadata());
    }
}

ProducesHtmlMetadata는 생성된 응답 콘텐츠 형식 text/html 과 상태 코드 200 OK를 정의하는 IProducesResponseTypeMetadata의 구현입니다.

internal sealed class ProducesHtmlMetadata : IProducesResponseTypeMetadata
{
    public Type? Type => null;

    public int StatusCode => 200;

    public IEnumerable<string> ContentTypes { get; } = new[] { MediaTypeNames.Text.Html };
}

다른 방법은 Microsoft.AspNetCore.Mvc.ProducesAttribute를 사용하여 생성된 응답을 설명하는 것입니다. 다음 코드는 ProducesAttribute를 사용하도록 PopulateMetadata 메서드를 변경합니다.

public static void PopulateMetadata(MethodInfo method, EndpointBuilder builder)
{
    builder.Metadata.Add(new ProducesAttribute(MediaTypeNames.Text.Html));
}

JSON serialization 옵션 구성

기본적으로 최소 API 앱은 JSON 직렬화 및 역직렬화 중에 옵션을 사용합니다 Web defaults .

전역적으로 JSON serialization 옵션 구성

옵션을 호출하여 앱에 대해 전역적으로 구성할 수 있습니다 ConfigureHttpJsonOptions. 다음 예제에서는 공용 필드를 포함하고 JSON 출력 형식을 지정합니다.

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
// }

필드가 포함되므로 앞의 코드는 이를 읽고 NameField 출력 JSON에 포함합니다.

엔드포인트에 대한 JSON serialization 옵션 구성

엔드포인트에 대한 serialization 옵션을 구성하려면 다음 예제와 같이 개체를 JsonSerializerOptions 호출 Results.Json 하고 전달합니다.

using System.Text.Json;

var app = WebApplication.Create();

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

app.MapGet("/", () => 
    Results.Json(new Todo { Name = "Walk dog", IsComplete = false }, options));

app.Run();

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

또는 개체를 허용하는 오버로드 WriteAsJsonAsyncJsonSerializerOptions 사용합니다. 다음 예제에서는 이 오버로드를 사용하여 출력 JSON의 형식을 지정합니다.

using System.Text.Json;

var app = WebApplication.Create();

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

app.MapGet("/", (HttpContext context) =>
    context.Response.WriteAsJsonAsync<Todo>(
        new Todo { Name = "Walk dog", IsComplete = false }, options));

app.Run();

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

추가 리소스

최소 엔드포인트는 다음과 같은 형식의 반환 값을 지원합니다.

  1. string - 여기에는 Task<string>ValueTask<string>가 포함됩니다.
  2. T(다른 모든 형식) - 여기에는 Task<T>ValueTask<T>가 포함됩니다.
  3. IResult 기반 - 여기에는 Task<IResult>ValueTask<IResult>가 포함됩니다.

string 반환 값

동작 콘텐츠-형식
프레임워크가 응답에 직접 문자열을 씁니다. text/plain

Hello world 텍스트를 반환하는 다음 경로 처리기를 고려합니다.

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

200 상태 코드는 text/plain Content-Type 헤더 및 다음 콘텐츠와 함께 반환됩니다.

Hello World

T (다른 모든 형식) 반환 값

동작 콘텐츠-형식
프레임워크 JSON은 응답을 직렬화합니다. application/json

Message 문자열 속성을 포함하는 익명 형식을 반환하는 다음 경로 처리기를 고려합니다.

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

200 상태 코드는 application/json Content-Type 헤더 및 다음 콘텐츠와 함께 반환됩니다.

{"message":"Hello World"}

IResult 반환 값

동작 콘텐츠-형식
프레임워크가 IResult.ExecuteAsync를 호출합니다. IResult 구현에 의해 결정됩니다.

IResult 인터페이스는 HTTP 엔드포인트의 결과를 나타내는 계약을 정의합니다. 정적 Results 클래스 및 정적 TypedResults는 다양한 유형의 응답을 나타내는 다양한 IResult 개체를 만드는 데 사용됩니다.

TypedResults 및 Results

ResultsTypedResults 정적 클래스는 유사한 결과 도우미 집합을 제공합니다. 클래스는 TypedResults 형식화된 클래스와 Results 동일합니다. 그러나, Results 도우미의 반환 형식은 IResult이지만 각 TypedResults 도우미의 반환 형식은 IResult 구현 형식 중 하나입니다. 이 차이는 Results 도우미의 경우 단위 테스트와 같이 구체적인 형식이 필요할 때 변환이 필요하다는 것을 의미합니다. 구현 형식은 Microsoft.AspNetCore.Http.HttpResults 네임스페이스에 정의됩니다.

TypedResults 반환보다는 Results 다음과 같은 장점이 있습니다.

  • TypedResults 도우미는 강력한 형식의 개체를 반환하여 코드 가독성, 단위 테스트를 개선하고 런타임 오류 가능성을 줄일 수 있습니다.
  • 구현 형식 은 엔드포인트를 설명하기 위해 OpenAPI 에 대한 응답 형식 메타데이터를 자동으로 제공합니다.

예상되는 JSON 응답이 있는 상태 코드가 200 OK 생성되는 다음 엔드포인트를 고려합니다.

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

이 엔드포인트를 올바르게 문서화하기 위해 확장 메서드 Produces이 호출됩니다. 그러나 다음 코드와 같이 TypedResultsResults 대신 사용되는 경우 Produces를 호출할 필요는 없습니다. TypedResults는 엔드포인트에 대한 메타데이터를 자동으로 제공합니다.

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

응답 유형을 설명하는 방법에 대한 자세한 내용은 최소 API에서 OpenAPI 지원을 참조하세요.

앞에서 설명한 것처럼, 사용할 TypedResults때 변환이 필요하지 않습니다. 클래스를 반환하는 다음 최소 API를 고려합니다.TypedResults

public static async Task<Ok<Todo[]>> GetAllTodos(TodoGroupDbContext database)
{
    var todos = await database.Todos.ToArrayAsync();
    return TypedResults.Ok(todos);
}

다음 테스트는 전체 구체적인 형식을 확인합니다.

[Fact]
public async Task GetAllReturnsTodosFromDatabase()
{
    // Arrange
    await using var context = new MockDb().CreateDbContext();

    context.Todos.Add(new Todo
    {
        Id = 1,
        Title = "Test title 1",
        Description = "Test description 1",
        IsDone = false
    });

    context.Todos.Add(new Todo
    {
        Id = 2,
        Title = "Test title 2",
        Description = "Test description 2",
        IsDone = true
    });

    await context.SaveChangesAsync();

    // Act
    var result = await TodoEndpointsV1.GetAllTodos(context);

    //Assert
    Assert.IsType<Ok<Todo[]>>(result);
    
    Assert.NotNull(result.Value);
    Assert.NotEmpty(result.Value);
    Assert.Collection(result.Value, todo1 =>
    {
        Assert.Equal(1, todo1.Id);
        Assert.Equal("Test title 1", todo1.Title);
        Assert.False(todo1.IsDone);
    }, todo2 =>
    {
        Assert.Equal(2, todo2.Id);
        Assert.Equal("Test title 2", todo2.Title);
        Assert.True(todo2.IsDone);
    });
}

모든 메서드 Results 가 서명에서 반환 IResult 되므로 컴파일러는 단일 엔드포인트에서 다른 결과를 반환할 때 자동으로 요청 대리자 반환 형식으로 유추합니다. TypedResults 에는 이러한 대리자의 Results<T1, TN> 사용이 필요합니다.

다음 메서드는 반환된 개체의 실제 구체적인 형식이 다르더라도 둘 다 Results.Ok Results.NotFound 반환 IResult으로 선언되기 때문에 컴파일됩니다.

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

다른 형식을 반환하는 것으로 선언되고 TypedResults.NotFound 컴파일러가 가장 일치하는 형식을 유추하려고 시도하지 않으므로 다음 메서드는 컴파일 TypedResults.Ok 되지 않습니다.

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

사용 TypedResults하려면 비동기에서 래퍼가 필요한 Task<> 경우 반환 형식을 완전히 선언해야 합니다. 사용 TypedResults 은 더 자세한 내용이지만 형식 정보를 정적으로 사용할 수 있으므로 OpenAPI에 대해 자체 설명할 수 있게 하는 것이 절호입니다.

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

Results<TResult1, TResultN>

다음과 같은 경우, IResult 대신 엔드포인트 처리기 반환 형식으로 Results<TResult1, TResultN>을 사용합니다.

  • 엔드포인트 처리기에서 여러 IResult 구현 형식이 반환됩니다.
  • 정적 TypedResult 클래스는 IResult 개체를 만드는 데 사용됩니다.

이 대안은 제네릭 공용 구조체 형식이 엔드포인트 메타데이터를 자동으로 유지하므로 IResult을 반환하는 것보다 좋습니다. Results<TResult1, TResultN> 공용 구조체 암시적 캐스트 연산자를 구현하므로 컴파일러는 제네릭 인수에 지정된 형식을 공용 구조체 형식의 인스턴스로 자동 변환할 수 있습니다.

이렇게 하면 경로 처리기가 실제로 선언한 결과만 반환한다는 컴파일 시간 검사를 제공하는 이점이 추가됩니다. 제네릭 인수 중 하나로 선언되지 않은 형식을 Results<>로 반환하려고 시도하면 컴파일 오류가 발생합니다.

orderId999보다 클 때 400 BadRequest 상태 코드가 반환되는 다음 엔드포인트를 고려합니다. 그렇지 않으면 예상 콘텐츠가 포함된 200 OK을 생성합니다.

app.MapGet("/orders/{orderId}", IResult (int orderId)
    => orderId > 999 ? TypedResults.BadRequest() : TypedResults.Ok(new Order(orderId)))
    .Produces(400)
    .Produces<Order>();

이 엔드포인트를 올바르게 문서화하기 위해 확장 메서드 Produces이 호출됩니다. 그러나 TypedResults 도우미는 엔드포인트에 대한 메타데이터를 자동으로 포함하므로 다음 코드와 같이 대신 Results<T1, Tn> 공용 구조체 형식을 반환할 수 있습니다.

app.MapGet("/orders/{orderId}", Results<BadRequest, Ok<Order>> (int orderId) 
    => orderId > 999 ? TypedResults.BadRequest() : TypedResults.Ok(new Order(orderId)));

기본 제공 결과

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

다음 섹션에서는 일반적인 결과 도우미의 사용을 보여줍니다.

JSON

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

WriteAsJsonAsync 는 JSON을 반환하는 다른 방법입니다.

app.MapGet("/", (HttpContext context) => context.Response.WriteAsJsonAsync
    (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();

Results.Stream 오버로드는 버퍼링 없이 기본 HTTP 응답 스트림에 대한 액세스를 허용합니다. 다음 예제에서는 ImageSharp를 사용하여 지정된 이미지의 축소된 크기를 반환합니다.

using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Formats.Jpeg;
using SixLabors.ImageSharp.Processing;

var builder = WebApplication.CreateBuilder(args);

var app = builder.Build();

app.MapGet("/process-image/{strImage}", (string strImage, HttpContext http, CancellationToken token) =>
{
    http.Response.Headers.CacheControl = $"public,max-age={TimeSpan.FromHours(24).TotalSeconds}";
    return Results.Stream(stream => ResizeImageAsync(strImage, stream, token), "image/jpeg");
});

async Task ResizeImageAsync(string strImage, Stream stream, CancellationToken token)
{
    var strPath = $"wwwroot/img/{strImage}";
    using var image = await Image.LoadAsync(strPath, token);
    int width = image.Width / 2;
    int height = image.Height / 2;
    image.Mutate(x =>x.Resize(width, height));
    await image.SaveAsync(stream, JpegFormat.Instance, cancellationToken: token);
}

다음 예제에서는 Azure Blob Storage에서 이미지를 스트리밍합니다.

app.MapGet("/stream-image/{containerName}/{blobName}", 
    async (string blobName, string containerName, CancellationToken token) =>
{
    var conStr = builder.Configuration["blogConStr"];
    BlobContainerClient blobContainerClient = new BlobContainerClient(conStr, containerName);
    BlobClient blobClient = blobContainerClient.GetBlobClient(blobName);
    return Results.Stream(await blobClient.OpenReadAsync(cancellationToken: token), "image/jpeg");
});

다음 예제에서는 Azure Blob에서 비디오를 스트리밍합니다.

// GET /stream-video/videos/earth.mp4
app.MapGet("/stream-video/{containerName}/{blobName}",
     async (HttpContext http, CancellationToken token, string blobName, string containerName) =>
{
    var conStr = builder.Configuration["blogConStr"];
    BlobContainerClient blobContainerClient = new BlobContainerClient(conStr, containerName);
    BlobClient blobClient = blobContainerClient.GetBlobClient(blobName);
    
    var properties = await blobClient.GetPropertiesAsync(cancellationToken: token);
    
    DateTimeOffset lastModified = properties.Value.LastModified;
    long length = properties.Value.ContentLength;
    
    long etagHash = lastModified.ToFileTime() ^ length;
    var entityTag = new EntityTagHeaderValue('\"' + Convert.ToString(etagHash, 16) + '\"');
    
    http.Response.Headers.CacheControl = $"public,max-age={TimeSpan.FromHours(24).TotalSeconds}";

    return Results.Stream(await blobClient.OpenReadAsync(cancellationToken: token), 
        contentType: "video/mp4",
        lastModified: lastModified,
        entityTag: entityTag,
        enableRangeProcessing: true);
});

리디렉션

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

파일

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

HttpResult 인터페이스

Microsoft.AspNetCore.Http 네임스페이스의 다음과 같은 인터페이스는 필터 구현의 일반적 유형으로서 런타임 시 IResult 형식을 검색하는 방법을 제공합니다.

다음은 이러한 인터페이스 중 하나를 사용하는 필터의 예입니다.

app.MapGet("/weatherforecast", (int days) =>
{
    if (days <= 0)
    {
        return Results.BadRequest();
    }

    var forecast = Enumerable.Range(1, days).Select(index =>
       new WeatherForecast(DateTime.Now.AddDays(index), Random.Shared.Next(-20, 55), "Cool"))
        .ToArray();
    return Results.Ok(forecast);
}).
AddEndpointFilter(async (context, next) =>
{
    var result = await next(context);

    return result switch
    {
        IValueHttpResult<WeatherForecast[]> weatherForecastResult => new WeatherHttpResult(weatherForecastResult.Value),
        _ => result
    };
});

자세한 내용은 최소 API 앱의 필터IResult 구현 형식을 참조하세요.

응답 사용자 지정

애플리케이션은 사용자 지정 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 형식은 IEndpointMetadataProvider 인터페이스를 구현하여 자체 주석을 제공할 수 있습니다. 예를 들어 다음 코드는 엔드포인트에서 생성된 응답을 설명하는 주석을 HtmlResult 이전 형식에 추가합니다.

class HtmlResult : IResult, IEndpointMetadataProvider
{
    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);
    }

    public static void PopulateMetadata(MethodInfo method, EndpointBuilder builder)
    {
        builder.Metadata.Add(new ProducesHtmlMetadata());
    }
}

ProducesHtmlMetadata는 생성된 응답 콘텐츠 형식 text/html 과 상태 코드 200 OK를 정의하는 IProducesResponseTypeMetadata의 구현입니다.

internal sealed class ProducesHtmlMetadata : IProducesResponseTypeMetadata
{
    public Type? Type => null;

    public int StatusCode => 200;

    public IEnumerable<string> ContentTypes { get; } = new[] { MediaTypeNames.Text.Html };
}

다른 방법은 Microsoft.AspNetCore.Mvc.ProducesAttribute를 사용하여 생성된 응답을 설명하는 것입니다. 다음 코드는 ProducesAttribute를 사용하도록 PopulateMetadata 메서드를 변경합니다.

public static void PopulateMetadata(MethodInfo method, EndpointBuilder builder)
{
    builder.Metadata.Add(new ProducesAttribute(MediaTypeNames.Text.Html));
}

JSON serialization 옵션 구성

기본적으로 최소 API 앱은 JSON 직렬화 및 역직렬화 중에 옵션을 사용합니다 Web defaults .

전역적으로 JSON serialization 옵션 구성

옵션을 호출하여 앱에 대해 전역적으로 구성할 수 있습니다 ConfigureHttpJsonOptions. 다음 예제에서는 공용 필드를 포함하고 JSON 출력 형식을 지정합니다.

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
// }

필드가 포함되므로 앞의 코드는 이를 읽고 NameField 출력 JSON에 포함합니다.

엔드포인트에 대한 JSON serialization 옵션 구성

엔드포인트에 대한 serialization 옵션을 구성하려면 다음 예제와 같이 개체를 JsonSerializerOptions 호출 Results.Json 하고 전달합니다.

using System.Text.Json;

var app = WebApplication.Create();

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

app.MapGet("/", () => 
    Results.Json(new Todo { Name = "Walk dog", IsComplete = false }, options));

app.Run();

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

또는 개체를 허용하는 오버로드 WriteAsJsonAsyncJsonSerializerOptions 사용합니다. 다음 예제에서는 이 오버로드를 사용하여 출력 JSON의 형식을 지정합니다.

using System.Text.Json;

var app = WebApplication.Create();

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

app.MapGet("/", (HttpContext context) =>
    context.Response.WriteAsJsonAsync<Todo>(
        new Todo { Name = "Walk dog", IsComplete = false }, options));

app.Run();

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

추가 리소스