참고 사항
이 문서의 최신 버전은 아닙니다. 현재 릴리스는 이 문서의 .NET 9 버전을 참조 하세요.
중요
이 정보는 상업적으로 출시되기 전에 실질적으로 수정될 수 있는 시험판 제품과 관련이 있습니다. Microsoft는 여기에 제공된 정보에 대해 어떠한 명시적, 또는 묵시적인 보증을 하지 않습니다.
현재 릴리스는 이 문서의 .NET 9 버전을 참조 하세요.
최소 엔드포인트는 다음과 같은 형식의 반환 값을 지원합니다.
-
string
- 여기에는Task<string>
및ValueTask<string>
가 포함됩니다. -
T
(다른 모든 형식) - 여기에는Task<T>
및ValueTask<T>
가 포함됩니다. -
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
Results 및 TypedResults 정적 클래스는 유사한 결과 도우미 집합을 제공합니다. 클래스는 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
이 호출됩니다. 그러나 다음 코드와 같이 Produces
가 TypedResults
대신 사용되는 경우 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.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.Ok
과 TypedResults.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 BadRequest
가 orderId
보다 클 때 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)));
기본 제공 결과
일반적인 결과 도우미는 Results 및 TypedResults 정적 클래스에 존재합니다.
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
형식을 검색하는 방법을 제공합니다.
- IContentTypeHttpResult
- IFileHttpResult
- INestedHttpResult
- IStatusCodeHttpResult
- IValueHttpResult
- IValueHttpResult<TValue>
다음은 이러한 인터페이스 중 하나를 사용하는 필터의 예입니다.
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
// }
추가 리소스
최소 엔드포인트는 다음과 같은 형식의 반환 값을 지원합니다.
-
string
- 여기에는Task<string>
및ValueTask<string>
가 포함됩니다. -
T
(다른 모든 형식) - 여기에는Task<T>
및ValueTask<T>
가 포함됩니다. -
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
Results 및 TypedResults 정적 클래스는 유사한 결과 도우미 집합을 제공합니다. 클래스는 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
이 호출됩니다. 그러나 다음 코드와 같이 Produces
가 TypedResults
대신 사용되는 경우 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.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.Ok
과 TypedResults.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 BadRequest
가 orderId
보다 클 때 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)));
기본 제공 결과
일반적인 결과 도우미는 Results 및 TypedResults 정적 클래스에 존재합니다.
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
형식을 검색하는 방법을 제공합니다.
- IContentTypeHttpResult
- IFileHttpResult
- INestedHttpResult
- IStatusCodeHttpResult
- IValueHttpResult
- IValueHttpResult<TValue>
다음은 이러한 인터페이스 중 하나를 사용하는 필터의 예입니다.
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
// }
추가 리소스
최소 엔드포인트는 다음과 같은 형식의 반환 값을 지원합니다.
-
string
- 여기에는Task<string>
및ValueTask<string>
가 포함됩니다. -
T
(다른 모든 형식) - 여기에는Task<T>
및ValueTask<T>
가 포함됩니다. -
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
Results 및 TypedResults 정적 클래스는 유사한 결과 도우미 집합을 제공합니다. 클래스는 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
이 호출됩니다. 그러나 다음 코드와 같이 Produces
가 TypedResults
대신 사용되는 경우 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.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.Ok
과 TypedResults.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 BadRequest
가 orderId
보다 클 때 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)));
기본 제공 결과
일반적인 결과 도우미는 Results 및 TypedResults 정적 클래스에 존재합니다.
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
형식을 검색하는 방법을 제공합니다.
- IContentTypeHttpResult
- IFileHttpResult
- INestedHttpResult
- IStatusCodeHttpResult
- IValueHttpResult
- IValueHttpResult<TValue>
다음은 이러한 인터페이스 중 하나를 사용하는 필터의 예입니다.
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
// }
추가 리소스
ASP.NET Core