Заметка
Доступ к этой странице требует авторизации. Вы можете попробовать войти в систему или изменить каталог.
Доступ к этой странице требует авторизации. Вы можете попробовать сменить директорию.
Примечание
Это не самая последняя версия этой статьи. В текущей версии см. версию .NET 10 этой статьи.
Предупреждение
Эта версия ASP.NET Core больше не поддерживается. Для получения дополнительной информации см. Политику поддержки .NET и .NET Core. Для текущего выпуска смотрите версию этой статьи для .NET 9.
В этой статье объясняется, как создавать ответы для минимальных конечных точек API в ASP.NET Core. Минимальные API предоставляют несколько способов возврата данных и кодов состояния HTTP.
Минимальные конечные точки поддерживают следующие типы возвращаемых значений:
-
string- Это включает в себяTask<string>иValueTask<string>. -
T(любой другой тип) - Это включаетTask<T>иValueTask<T>. - Основано на
IResult- Это включаетTask<IResult>иValueTask<IResult>.
Важный
Начиная с ASP.NET Core 10 известные конечные точки API больше не перенаправляются на страницы входа при использовании cookie проверки подлинности. Вместо этого они возвращают коды состояния 401/403. Дополнительные сведения см. в статье о поведении проверки подлинности конечной точки API в ASP.NET Core.
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 — это 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.
Примеры типов результатов тестирования см. в документации по тестированию.
Поскольку все методы на 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<>, приводит к ошибке компиляции.
Рассмотрите следующий конечный пункт, для которого возвращается статусный код 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 vs Results.
В следующих разделах демонстрируется использование общих помощников результатов.
JSON (JavaScript Object Notation)
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);
});
Настройка ответов на ошибки проверки с помощью IProblemDetailsService
Настройте ответы на ошибки из минимальной IProblemDetailsService логики проверки API с помощью реализации. Зарегистрируйте эту службу в коллекции служб приложения, чтобы обеспечить более согласованные и пользовательские ответы на ошибки. Поддержка минимальной проверки API появилась в ASP.NET Core в .NET 10.
Чтобы реализовать пользовательские ответы на ошибки проверки, выполните следующее:
- Реализация IProblemDetailsService или использование реализации по умолчанию
- Регистрация службы в контейнере DI
- Система проверки автоматически использует зарегистрированную службу для форматирования ответов на ошибки проверки.
В следующем примере показано, как зарегистрировать и настроить IProblemDetailsService ответы на ошибки проверки:
using System.ComponentModel.DataAnnotations;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddProblemDetails(options =>
{
options.CustomizeProblemDetails = context =>
{
if (context.ProblemDetails.Status == 400)
{
context.ProblemDetails.Title = "Validation error occurred";
context.ProblemDetails.Extensions["support"] = "Contact support@example.com";
context.ProblemDetails.Extensions["traceId"] = Guid.NewGuid().ToString();
}
};
});
При возникновении ошибки проверки будет использоваться для создания ответа об ошибке, включая все настройки, IProblemDetailsService добавленные в обратный CustomizeProblemDetails вызов.
Полный пример приложения см. в примере приложения с минимальным числом api, демонстрирующим настройку ответов на ошибки проверки с помощью IProblemDetailsService api ASP.NET Core Min.
Текст
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)
API TypedResults.ServerSentEvents поддерживает возврат результата ServerSentEvents .
Server-Sent События — это технология push-сообщений, которая позволяет серверу отправлять поток сообщений о событиях клиенту через одно HTTP-подключение. В .NET сообщения о событиях представлены как SseItem<T> объекты, которые могут содержать тип события, идентификатор и полезные данные типа T.
Класс TypedResults имеет статический метод с именем ServerSentEvents , который можно использовать для возврата ServerSentEvents результата. Первым параметром этого метода является IAsyncEnumerable<SseItem<T>> тот, который представляет поток сообщений о событиях, отправляемых клиенту.
В следующем примере показано, как использовать TypedResults.ServerSentEvents API для возврата потока событий пульса в качестве объектов 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 API для возврата потока событий частоты пульса в виде строки, а также ServerSentEventsобъектов JSON, клиенту.
Перенаправить
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
};
});
Для получения дополнительной информации см. Фильтры в приложениях Minimal 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
По умолчанию приложения с минимальным 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-эндпоинта. Статический класс Results и статический класс TypedResults используются для создания различных IResult объектов, представляющих разные типы ответов.
TypedResults против Results
Классы Results и TypedResults предоставляют аналогичные наборы помощников для результатов. Класс TypedResults является типизированным аналогом класса . Тем не менее, тип возвращаемого значения для помощников 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<>, приводит к ошибке компиляции.
Рассмотрите следующий конечный пункт, для которого возвращается статусный код 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 vs Results.
В следующих разделах демонстрируется использование общих помощников результатов.
JSON (JavaScript Object Notation)
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
};
});
Для получения дополнительной информации см. Фильтры в приложениях Minimal 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
По умолчанию приложения с минимальным 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-эндпоинта. Статический класс Results и статический класс TypedResults используются для создания различных IResult объектов, представляющих разные типы ответов.
TypedResults против Results
Классы Results и TypedResults предоставляют аналогичные наборы помощников для результатов. Класс TypedResults является типизированным аналогом класса . Тем не менее, тип возвращаемого значения для помощников 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<>, приводит к ошибке компиляции.
Рассмотрите следующий конечный пункт, для которого возвращается статусный код 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 vs Results.
В следующих разделах демонстрируется использование общих помощников результатов.
JSON (JavaScript Object Notation)
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
};
});
Для получения дополнительной информации см. Фильтры в приложениях Minimal 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
По умолчанию приложения с минимальным 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
// }