Примечание.
Для доступа к этой странице требуется авторизация. Вы можете попробовать войти или изменить каталоги.
Для доступа к этой странице требуется авторизация. Вы можете попробовать изменить каталоги.
Примечание
Это не самая последняя версия этой статьи. В текущей версии см. версию .NET 10 этой статьи.
Предупреждение
Эта версия ASP.NET Core больше не поддерживается. Для получения дополнительной информации см. Политику поддержки .NET и .NET Core. В текущей версии см. версию .NET 10 этой статьи.
В этой статье объясняется, как создавать ответы для минимальных конечных точек 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 и 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
Настройте ответы об ошибках логики проверки Minimal API с помощью реализации IProblemDetailsService. Зарегистрируйте эту службу в коллекции служб приложения, чтобы обеспечить более согласованные и пользовательские ответы на ошибки. Поддержка минимальной проверки 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.
Пример полного приложения см. в образце приложения Minimal API, демонстрирующем, как настраивать ответы на ошибки валидации с помощью IProblemDetailsService в ASP.NET Core Minimal APIs.
Текст
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.
Возвращаемые значения результатов файла
Когда конечная точка API возвращает содержимое, отличное от JSON, или поддерживает функции протокола HTTP, такие как условные или диапазонные запросы, ASP.NET Core предоставляет типы результатов, обрабатывающие необходимые сведения о протоколе HTTP. Эти типы результатов называются "результатами файла", но функциональность полезна для сценариев, кроме обслуживания файлов на диске. Существуют типы результатов файлов для минимальных API и API на основе контроллера, и они используют общую базовую реализацию и поведение.
Чтобы получить доступ к этой функции, конечная точка API создает и возвращает объект результата файла — экземпляр одного из этих типов результатов файла.
Объект результата файла инкапсулирует содержимое, отправляемое, тип контента и любые дополнительные параметры, такие как имя файла скачивания.
Результирующий объект реализует метод — ExecuteAsync(HttpContext) для Minimal APIs или ExecuteResultAsync(ActionContext) для контроллеров — который фреймворк вызывает для формирования ответа.
Этот метод задает Content-Type заголовок и, если указано имя файла, Content-Disposition заголовок.
Он также обрабатывает заголовки условного запроса и заголовки запросов диапазона, когда соответствующие параметры задаются в объекте результата.
Типы результатов файлов
В Minimal APIs наиболее распространённый и рекомендуемый способ создать файловый результат — использовать TypedResults.File, который принимает byte[] или Stream и возвращает FileContentHttpResult или FileStreamHttpResult.
В качестве альтернатив можно использовать TypedResults.Bytes для явного вспомогательного объекта для массива байтов, TypedResults.PhysicalFile для предоставления файлов по абсолютному пути или Results.File / Results.Bytes, если вам нужен только интерфейс IResult.
ASP.NET Core также предоставляет ПО промежуточного слоя статических файлов, которое обслуживает файлы относительно корневого веб-сайта (wwwroot) без необходимости явной конечной точки.
app.MapGet("/report", () =>
{
// TypedResults.File with a byte[] returns a FileContentHttpResult
byte[] pdf = GenerateReport();
return TypedResults.File(pdf, "application/pdf", "report.pdf");
});
app.MapGet("/download", () =>
{
// TypedResults.File with a Stream returns a FileStreamHttpResult
Stream stream = new MemoryStream("Hello, World!"u8.ToArray());
return TypedResults.File(stream, "application/octet-stream");
});
В API ControllerBase.File() на основе контроллера вспомогательный метод принимает byte[]Stream или возвращает соответствующий конкретный тип результата (FileContentResult, FileStreamResult).
К альтернативным вариантам относятся возврат VirtualFileResult для файлов относительно корневого каталога веб-приложения или PhysicalFileResult для файлов по абсолютному пути, но такие варианты встречаются реже, поскольку эти сценарии обычно обрабатывает промежуточное ПО для статических файлов.
[HttpGet("report")]
public FileContentResult Report()
{
// File() with a byte[] returns a FileContentResult
byte[] pdf = GenerateReport();
return File(pdf, "application/pdf", "report.pdf");
}
[HttpGet("download")]
public FileStreamResult Download()
{
// File() with a Stream returns a FileStreamResult
Stream stream = new MemoryStream("Hello, World!"u8.ToArray());
return File(stream, "application/octet-stream");
}
Поддержка OpenAPI для результатов файла
Типы результатов файлов не добавляют метаданные ответа автоматически в создаваемый документ OpenAPI. Чтобы получить правильное описание ответа в документе OpenAPI, необходимо явно указать эти метаданные.
В Minimal APIs используйте метод расширения Produces<TResponse>(), чтобы указать метаданные OpenAPI для отклика. Эти метаданные определяют код состояния, тип контента и схему ответа в документе OpenAPI.
Для результата файла важно указать тип контента, например application/pdf, и использовать подходящий TResponse для получения требуемой схемы.
Можно также указать код состояния в методе Produces расширения, если он отличается от значения по умолчанию 200 OK.
Общие коды состояния определяются в StatusCodes классе, а общие типы контента определяются в MediaTypeNames классе.
Рассмотрим пример.
app.MapGet("/image", () =>
{
// A 1x1 red pixel BMP (bitmap header + single pixel)
byte[] data = [0x42, 0x4D, 0x1E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x1A, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x01, 0x00,
0x01, 0x00, 0x01, 0x00, 0x18, 0x00, 0x00, 0x00, 0xFF, 0x00];
return TypedResults.File(data, MediaTypeNames.Image.Bmp, "pixel.bmp");
})
// Use Stream to produce the correct schema in OpenAPI (format: binary)
.Produces<Stream>(contentType: MediaTypeNames.Image.Bmp);
Концептуально TResponse представляет тип тела ответа, но подходящая схема для файлового результата зависит больше от содержимого тела, чем от типа CLR. Поэтому для получения нужной схемы OpenAPI может потребоваться указать TResponse, который не совпадает с CLR-типом содержимого в файловом результате.
В частности, существует три распространенных схемы для результатов файла:
-
двоичный контент, например PDF-файлы, изображения или видео, для которого схема должна быть
type: string, format: binary. Для этого случая рекомендуемымTResponseявляетсяStream. Платформа имеет специальную логику для сопоставления этого типа с форматомbinaryв схеме OpenAPI. -
текстовое содержимое, например CSV или обычный текст. Здесь схема должна быть просто
type: stringбезformat. Используйтеstringв качествеTResponseв этом случае. -
Содержимое, закодированное в base64, где схема должна быть
type: string, format: byte. Обычно содержимое файла в ответе API не кодируют в Base64, но по историческим причинам именно такая схема используется, когдаTResponse— этоbyte[].
В приложении на основе контроллеров используйте [Produces] или атрибут [ProducesResponseType]:
[HttpGet("image")]
// Use Stream to produce the correct schema in OpenAPI (format: binary)
[ProducesResponseType<Stream>(StatusCodes.Status200OK, MediaTypeNames.Image.Bmp)]
public FileContentResult Image()
{
// A 1x1 red pixel BMP (bitmap header + single pixel)
byte[] data = [0x42, 0x4D, 0x1E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x1A, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x01, 0x00,
0x01, 0x00, 0x01, 0x00, 0x18, 0x00, 0x00, 0x00, 0xFF, 0x00];
return File(data, "image/bmp", "pixel.bmp");
}
Как и в случае с минимальными API, следует выбрать параметр Type атрибутов [Produces] и [ProducesResponseType], который соответствует требуемой схеме OpenAPI для ответов с файловым результатом:
-
двоичное содержимое: используйте
FileContentResultилиFileStreamResult, чтобы получить форматbinaryв схеме OpenAPI. -
текстовое содержимое: используйте
stringдля получения простойtype: stringсхемы. -
содержимое, закодированное в Base64: Используйте
byte[], чтобы получить форматbyteв спецификации OpenAPI, хотя для файловых результатов это встречается редко.
Поддержка результатов файла для условных запросов
Результаты файла поддерживают условные запросы на проверку кэша. При создании объекта результата задайте параметры lastModified и/или entityTag, а платформа автоматически проверит входящие заголовки If-None-Match и If-Modified-Since. Если ресурс не изменился, платформа возвращается 304 Not Modified без текста — дополнительный код не требуется.
| Parameter | Purpose |
|---|---|
lastModified |
Last-Modified Задает заголовок ответа. Если клиент отправляет If-Modified-Since и файл не изменился, платформа возвращается 304 Not Modified без текста. |
entityTag |
ETag Задает заголовок ответа. Если клиент отправляет If-None-Match с соответствующим ETag, фреймворк возвращает 304 Not Modified без тела. |
Примечание
Проверки предварительных условий (If-Match, If-Unmodified-Since) обычно требуют пользовательской логики в конечной точке для проверки предварительных условий перед их выполнением.
В следующем примере показано, как использовать результаты файла для включения проверки кэша для конечной точки конфигурации.
app.MapGet("/config", (
[FromHeader(Name = "If-None-Match")] string? ifNoneMatch,
[FromHeader(Name = "If-Modified-Since")] string? ifModifiedSince) =>
{
byte[] data = File.ReadAllBytes("Data/config.json");
var lastModified = File.GetLastWriteTimeUtc("Data/config.json");
var etag = new EntityTagHeaderValue($"\"{Convert.ToHexString(SHA256.HashData(data))}\"");
return TypedResults.File(
data,
contentType: MediaTypeNames.Application.Json,
lastModified: lastModified,
entityTag: etag);
})
.Produces<object>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status304NotModified);
В этом примере также показано, как задокументировать ответ 304 Not Modified в документе OpenAPI с помощью метода расширения Produces, а также как включить заголовки if-none-match и if-modified-since в качестве параметров, чтобы они были включены в схему OpenAPI для эндпоинта.
API на основе контроллера может обеспечить такое же поведение с помощью вспомогательного File метода и атрибута [ProducesResponseType] :
[HttpGet("config")]
[ProducesResponseType<object>(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status304NotModified)]
public FileContentResult Config(
[FromHeader(Name = "If-None-Match")] string? ifNoneMatch,
[FromHeader(Name = "If-Modified-Since")] string? ifModifiedSince)
{
byte[] data = System.IO.File.ReadAllBytes("Data/config.json");
var lastModified = System.IO.File.GetLastWriteTimeUtc("Data/config.json");
var etag = new EntityTagHeaderValue($"\"{Convert.ToHexString(SHA256.HashData(data))}\"");
return File(
data,
MediaTypeNames.Application.Json,
lastModified: lastModified,
entityTag: etag);
}
Поддержка файловых результатов для запросов диапазонов
Запросы диапазона позволяют клиентам запрашивать только часть файла, а не весь контент. Клиент отправляет заголовок Range, указывая смещения в байтах, а сервер отвечает заголовком 206 Partial Content, содержащим только эти байты. Это позволяет возобновлять скачивание, параллельные фрагментированные загрузки и эффективно искать в проигрывателях мультимедиа.
Запросы диапазона также могут быть условными: клиент отправляет If-Range заголовок, содержащий ETag или дату вместе с Range заголовком, и сервер возвращает частичное содержимое, только если ресурс не изменился. Если он изменился, сервер игнорирует диапазон и возвращает полный ресурс.
If-Range вычисляется только в том случае, если для результата файла также задано entityTag или lastModified.
Установите enableRangeProcessing в значение true, чтобы включить обработку диапазона. В следующих примерах можно включить обработку диапазона для конечной точки потоковой передачи видео и задокументируйте дополнительные типы ответов в метаданных OpenAPI.
Минимальный API:
app.MapGet("/video/{id}", (string id,
[FromHeader(Name = "Range")] string? range) =>
{
var bytes = GetVideo(id);
return TypedResults.File(
bytes,
contentType: "video/mp4",
fileDownloadName: "video.mp4",
enableRangeProcessing: true);
})
.Produces<Stream>(StatusCodes.Status200OK, "video/mp4")
.Produces<Stream>(StatusCodes.Status206PartialContent, "video/mp4")
.Produces(StatusCodes.Status416RangeNotSatisfiable);
Контроллер:
[HttpGet("video/{id}")]
[ProducesResponseType<Stream>(StatusCodes.Status200OK, "video/mp4")]
[ProducesResponseType<Stream>(StatusCodes.Status206PartialContent, "video/mp4")]
[ProducesResponseType(StatusCodes.Status416RangeNotSatisfiable)]
public FileContentResult Video(string id,
[FromHeader(Name = "Range")] string? range)
{
var bytes = GetVideo(id);
return File(
bytes,
contentType: "video/mp4",
fileDownloadName: "video.mp4",
enableRangeProcessing: true);
}
С помощью этой конфигурации платформа автоматически обрабатывает следующие http-взаимодействия:
-
Полный запрос: возвращает
200 OKполный файл. -
Запрос диапазона (
Range: bytes=0-1023): Возвращается206 Partial Contentс заголовкомContent-Rangeи только запрошенными байтами. -
Недопустимый диапазон: Возвращает
416 Range Not Satisfiable.
Изменение заголовков
Используйте объект 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 и 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 и 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
// }
Дополнительные ресурсы
ASP.NET Core