Создание ответов в минимальных приложениях API
Примечание.
Это не последняя версия этой статьи. В текущем выпуске см . версию .NET 8 этой статьи.
Предупреждение
Эта версия ASP.NET Core больше не поддерживается. Дополнительные сведения см. в статье о политике поддержки .NET и .NET Core. В текущем выпуске см . версию .NET 8 этой статьи.
Внимание
Эта информация относится к предварительному выпуску продукта, который может быть существенно изменен до его коммерческого выпуска. Майкрософт не предоставляет никаких гарантий, явных или подразумеваемых, относительно приведенных здесь сведений.
В текущем выпуске см . версию .NET 8 этой статьи.
Минимальные конечные точки поддерживают следующие типы возвращаемых значений:
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. Класс статических результатов и статический TypedResults используются для создания различных объектов, представляющих различные IResult
типы ответов.
TypedResults и результаты
Вспомогательные классы и TypedResults статические Results классы предоставляют аналогичные наборы вспомогательных элементов результатов. Класс TypedResults
является типизированным эквивалентом Results
класса. Results
Однако возвращаемый тип вспомогательных элементов имеет типIResult, а возвращаемый тип каждого TypedResults
помощника является одним из IResult
типов реализации. Разница означает, что для Results
вспомогательных служб требуется преобразование, если требуется конкретный тип, например для модульного тестирования. Типы реализации определяются в Microsoft.AspNetCore.Http.HttpResults пространстве имен.
Возврат, TypedResults
а не Results
имеет следующих преимуществ:
TypedResults
вспомогательные средства возвращают строго типизированные объекты, которые могут улучшить удобочитаемость кода, модульное тестирование и снизить вероятность ошибок во время выполнения.- Тип реализации автоматически предоставляет метаданные типа ответа для OpenAPI для описания конечной точки.
Рассмотрим следующую конечную точку, для которой создается код состояния с ожидаемым ответом 200 OK
JSON.
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!" }));
Дополнительные сведения о описании типа ответа см. в разделе "Поддержка OpenAPI" в минимальных API.
Как упоминалось ранее, при использовании 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)));
Встроенные результаты
Распространенные вспомогательные средства результатов существуют в и TypedResults статических Results классах. 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("/text", () => Results.Text("This is some text"));
Stream
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);
}
В следующем примере выполняется потоковая передача изображения из хранилища BLOB-объектов Azure:
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");
});
В следующем примере выполняется потоковая передача изображения из BLOB-объекта Azure:
// 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
реализация, которая определяет тип text/html
контента создаваемого IProducesResponseTypeMetadata ответа и код 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
По умолчанию минимальные приложения API используют Web defaults
параметры во время сериализации и десериализации JSON.
Настройка параметров сериализации JSON глобально
Параметры можно настроить глобально для приложения путем 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 для конечной точки
Чтобы настроить параметры сериализации для конечной точки, вызовите 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. Класс статических результатов и статический TypedResults используются для создания различных объектов, представляющих различные IResult
типы ответов.
TypedResults и результаты
Вспомогательные классы и TypedResults статические Results классы предоставляют аналогичные наборы вспомогательных элементов результатов. Класс TypedResults
является типизированным эквивалентом Results
класса. Results
Однако возвращаемый тип вспомогательных элементов имеет типIResult, а возвращаемый тип каждого TypedResults
помощника является одним из IResult
типов реализации. Разница означает, что для Results
вспомогательных служб требуется преобразование, если требуется конкретный тип, например для модульного тестирования. Типы реализации определяются в Microsoft.AspNetCore.Http.HttpResults пространстве имен.
Возврат, TypedResults
а не Results
имеет следующих преимуществ:
TypedResults
вспомогательные средства возвращают строго типизированные объекты, которые могут улучшить удобочитаемость кода, модульное тестирование и снизить вероятность ошибок во время выполнения.- Тип реализации автоматически предоставляет метаданные типа ответа для OpenAPI для описания конечной точки.
Рассмотрим следующую конечную точку, для которой создается код состояния с ожидаемым ответом 200 OK
JSON.
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!" }));
Дополнительные сведения о описании типа ответа см. в разделе "Поддержка OpenAPI" в минимальных API.
Как упоминалось ранее, при использовании 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)));
Встроенные результаты
Распространенные вспомогательные средства результатов существуют в и TypedResults статических Results классах. 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"));
Stream
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);
}
В следующем примере выполняется потоковая передача изображения из хранилища BLOB-объектов Azure:
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");
});
В следующем примере выполняется потоковая передача изображения из BLOB-объекта Azure:
// 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
реализация, которая определяет тип text/html
контента создаваемого IProducesResponseTypeMetadata ответа и код 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
По умолчанию минимальные приложения API используют Web defaults
параметры во время сериализации и десериализации JSON.
Настройка параметров сериализации JSON глобально
Параметры можно настроить глобально для приложения путем 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 для конечной точки
Чтобы настроить параметры сериализации для конечной точки, вызовите 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