다음을 통해 공유


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

참고 사항

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

경고

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

중요

이 정보는 상업적으로 출시되기 전에 실질적으로 수정될 수 있는 시험판 제품과 관련이 있습니다. 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이 호출됩니다. 그러나 다음 코드와 같이 ProducesTypedResults 대신 사용되는 경우 Results를 호출할 필요는 없습니다. 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.OkResults.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.OkTypedResults.NotFound이 서로 다른 형식을 반환하는 것으로 선언되어 있고, 컴파일러가 가장 잘 맞는 형식을 유추하려고 하지 않기 때문입니다.

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

결과<TResult1, TResultN>

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

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

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

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

400 BadRequestorderId보다 클 때 999 상태 코드가 반환되는 다음 엔드포인트를 고려합니다. 그렇지 않으면 예상 콘텐츠가 포함된 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)));

기본 제공 결과

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

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

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

내부 서버 오류

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

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

문제 및 검증 문제

app.MapGet("/problem", () =>
{
    var extensions = new List<KeyValuePair<string, object?>> { new("test", "value") };
    return TypedResults.Problem("This is an error with extensions", 
                                                extensions: extensions);
});

텍스트

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

Server-Sent 이벤트(SSE)

TypedResults.ServerSentEvents API는 ServerSentEvents 결과 반환을 지원합니다.

Server-Sent 이벤트는 서버가 단일 HTTP 연결을 통해 클라이언트에 이벤트 메시지 스트림을 보낼 수 있도록 하는 서버 푸시 기술입니다. .NET에서 이벤트 메시지는 이벤트 형식, ID 및 형식SseItem<T>의 데이터 페이로드를 포함할 수 있는 개체로 T 표시됩니다.

TypedResults 클래스에는 결과를 반환하는 데 사용할 수 있는 ServerSentEvents라는 정적 메서드가 ServerSentEvents 있습니다. 이 메서드의 첫 번째 매개 변수는 클라이언트로 보낼 이벤트 메시지의 스트림을 나타내는 매개 변수입니다 IAsyncEnumerable<SseItem<T>> .

다음 예제에서는 API를 TypedResults.ServerSentEvents 사용하여 심박수 이벤트의 스트림을 클라이언트에 JSON 개체로 반환하는 방법을 보여 줍니다.

app.MapGet("sse-item", (CancellationToken cancellationToken) =>
{
    async IAsyncEnumerable<SseItem<int>> GetHeartRate(
        [EnumeratorCancellation] CancellationToken cancellationToken)
    {
        while (!cancellationToken.IsCancellationRequested)
        {
            var heartRate = Random.Shared.Next(60, 100);
            yield return new SseItem<int>(heartRate, eventType: "heartRate")
            {
                ReconnectionInterval = TimeSpan.FromMinutes(1)
            };
            await Task.Delay(2000, cancellationToken);
        }
    }

    return TypedResults.ServerSentEvents(GetHeartRate(cancellationToken));
});

자세한 내용은 API를 사용하여 심박수 이벤트 스트림을 문자열TypedResults.ServerSentEvents로 반환하고 JSON 개체를 클라이언트에 반환하는 ServerSentEvents 참조하세요.

리디렉션

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 구현 형식을 참조하세요.

헤더 수정

HttpResponse 개체를 사용하여 응답 헤더를 수정합니다.

app.MapGet("/", (HttpContext context) => {
    // Set a custom header
    context.Response.Headers["X-Custom-Header"] = "CustomValue";

    // Set a known header
    context.Response.Headers.CacheControl = $"public,max-age=3600";

    return "Hello World";
});

응답 사용자 지정

애플리케이션은 사용자 지정 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는 생성된 응답 콘텐츠 형식 IProducesResponseTypeMetadata 과 상태 코드 text/html를 정의하는 200 OK의 구현입니다.

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를 사용하여 생성된 응답을 설명하는 것입니다. 다음 코드는 PopulateMetadata를 사용하도록 ProducesAttribute 메서드를 변경합니다.

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 옵션을 구성하려면, 다음 예제와 같이 Results.Json을 호출한 후 JsonSerializerOptions 객체를 전달하십시오.

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

대안으로, WriteAsJsonAsync 객체를 허용하는 오버로드 JsonSerializerOptions를 사용하십시오. 다음 예제에서는 이 오버로드를 사용하여 출력 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이 호출됩니다. 그러나 다음 코드와 같이 ProducesTypedResults 대신 사용되는 경우 Results를 호출할 필요는 없습니다. 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.OkResults.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.OkTypedResults.NotFound이 서로 다른 형식을 반환하는 것으로 선언되어 있고, 컴파일러가 가장 잘 맞는 형식을 유추하려고 하지 않기 때문입니다.

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

결과<TResult1, TResultN>

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

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

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

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

400 BadRequestorderId보다 클 때 999 상태 코드가 반환되는 다음 엔드포인트를 고려합니다. 그렇지 않으면 예상 콘텐츠가 포함된 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)));

기본 제공 결과

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

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

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

내부 서버 오류

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

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

문제 및 검증 문제

app.MapGet("/problem", () =>
{
    var extensions = new List<KeyValuePair<string, object?>> { new("test", "value") };
    return TypedResults.Problem("This is an error with extensions", 
                                                extensions: extensions);
});

텍스트

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 구현 형식을 참조하세요.

헤더 수정

HttpResponse 개체를 사용하여 응답 헤더를 수정합니다.

app.MapGet("/", (HttpContext context) => {
    // Set a custom header
    context.Response.Headers["X-Custom-Header"] = "CustomValue";

    // Set a known header
    context.Response.Headers.CacheControl = $"public,max-age=3600";

    return "Hello World";
});

응답 사용자 지정

애플리케이션은 사용자 지정 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는 생성된 응답 콘텐츠 형식 IProducesResponseTypeMetadata 과 상태 코드 text/html를 정의하는 200 OK의 구현입니다.

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를 사용하여 생성된 응답을 설명하는 것입니다. 다음 코드는 PopulateMetadata를 사용하도록 ProducesAttribute 메서드를 변경합니다.

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 옵션을 구성하려면, 다음 예제와 같이 Results.Json을 호출한 후 JsonSerializerOptions 객체를 전달하십시오.

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

대안으로, WriteAsJsonAsync 객체를 허용하는 오버로드 JsonSerializerOptions를 사용하십시오. 다음 예제에서는 이 오버로드를 사용하여 출력 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이 호출됩니다. 그러나 다음 코드와 같이 ProducesTypedResults 대신 사용되는 경우 Results를 호출할 필요는 없습니다. 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.OkResults.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.OkTypedResults.NotFound이 서로 다른 형식을 반환하는 것으로 선언되어 있고, 컴파일러가 가장 잘 맞는 형식을 유추하려고 하지 않기 때문입니다.

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

결과<TResult1, TResultN>

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

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

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

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

400 BadRequestorderId보다 클 때 999 상태 코드가 반환되는 다음 엔드포인트를 고려합니다. 그렇지 않으면 예상 콘텐츠가 포함된 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)));

기본 제공 결과

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

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

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

텍스트

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는 생성된 응답 콘텐츠 형식 IProducesResponseTypeMetadata 과 상태 코드 text/html를 정의하는 200 OK의 구현입니다.

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를 사용하여 생성된 응답을 설명하는 것입니다. 다음 코드는 PopulateMetadata를 사용하도록 ProducesAttribute 메서드를 변경합니다.

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 옵션을 구성하려면, 다음 예제와 같이 Results.Json을 호출한 후 JsonSerializerOptions 객체를 전달하십시오.

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

대안으로, WriteAsJsonAsync 객체를 허용하는 오버로드 JsonSerializerOptions를 사용하십시오. 다음 예제에서는 이 오버로드를 사용하여 출력 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
// }

추가 리소스