Бөлісу құралы:


Как создавать ответы в приложениях с минимальным API

Примечание

Это не самая последняя версия этой статьи. Для текущего выпуска смотрите версию этой статьи для .NET 9.

Предупреждение

Эта версия ASP.NET Core больше не поддерживается. Для получения дополнительной информации см. Политику поддержки .NET и .NET Core. Для текущего выпуска смотрите версию этой статьи для .NET 9.

Важный

Эта информация относится к продукту, который находится в стадии предварительного выпуска, который может быть значительно изменен перед коммерческим выпуском. Корпорация Microsoft не дает никаких гарантий, явных или подразумеваемых, в отношении предоставленной здесь информации.

Для текущего выпуска смотрите версию этой статьи для .NET 9.

Минимальные конечные точки поддерживают следующие типы возвращаемых значений:

  1. string - Это включает в себя Task<string> и ValueTask<string>.
  2. T (любой другой тип) - Это включает Task<T> и ValueTask<T>.
  3. Основано на IResult - Это включает Task<IResult> и ValueTask<IResult>.

string возвращаемые значения

Поведение Тип контента
Фреймворк напрямую записывает строку в ответ. text/plain

Рассмотрим следующий обработчик маршрута, который возвращает текст Hello world.

app.MapGet("/hello", () => "Hello World");

Код состояния 200 возвращается с заголовком text/plain Content-Type и следующим содержимым.

Hello World

T (Любой другой тип) возвращаемые значения

Поведение Тип контента
Фреймворк JSON-сериализует ответ. application/json

Рассмотрите следующий обработчик маршрута, который возвращает анонимный тип, содержащий строковое свойство Message.

app.MapGet("/hello", () => new { Message = "Hello World" });

Код состояния 200 возвращается с заголовком application/json Content-Type и следующим содержимым.

{"message":"Hello World"}

IResult возвращаемые значения

Поведение Тип контента
Фреймворк вызывает IResult.ExecuteAsync. Решается реализацией IResult.

Интерфейс IResult определяет контракт, который представляет собой результат HTTP-эндпоинта. Статический класс Results и статический класс TypedResults используются для создания различных IResult объектов, представляющих разные типы ответов.

TypedResults против Results

Классы Results и TypedResults предоставляют аналогичные наборы помощников для результатов. Класс TypedResults является типизированным аналогом класса . Тем не менее, тип возвращаемого значения для помощников Results — это IResult, тогда как тип возвращаемого значения каждого помощника TypedResults является одним из типов реализации IResult. Разница заключается в том, что для Results помощников требуется конвертация, когда необходим конкретный тип, например, для модульного тестирования. Типы реализации определены в пространстве имен Microsoft.AspNetCore.Http.HttpResults.

Возврат TypedResults вместо Results имеет следующие преимущества:

Рассмотрим следующий конечный точку, для которого создается код состояния 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);
});

события 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 во время выполнения, что является распространенной схемой в реализации фильтров.

Вот пример фильтра, который использует один из этих интерфейсов:

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

Дополнительные ресурсы

Минимальные конечные точки поддерживают следующие типы возвращаемых значений:

  1. string - Это включает в себя Task<string> и ValueTask<string>.
  2. T (любой другой тип) - Это включает Task<T> и ValueTask<T>.
  3. Основано на IResult - Это включает Task<IResult> и ValueTask<IResult>.

string возвращаемые значения

Поведение Тип контента
Фреймворк напрямую записывает строку в ответ. text/plain

Рассмотрим следующий обработчик маршрута, который возвращает текст Hello world.

app.MapGet("/hello", () => "Hello World");

Код состояния 200 возвращается с заголовком text/plain Content-Type и следующим содержимым.

Hello World

T (Любой другой тип) возвращаемые значения

Поведение Тип контента
Фреймворк JSON-сериализует ответ. application/json

Рассмотрите следующий обработчик маршрута, который возвращает анонимный тип, содержащий строковое свойство Message.

app.MapGet("/hello", () => new { Message = "Hello World" });

Код состояния 200 возвращается с заголовком application/json Content-Type и следующим содержимым.

{"message":"Hello World"}

IResult возвращаемые значения

Поведение Тип контента
Фреймворк вызывает IResult.ExecuteAsync. Решается реализацией IResult.

Интерфейс IResult определяет контракт, который представляет собой результат HTTP-эндпоинта. Статический класс Results и статический класс TypedResults используются для создания различных IResult объектов, представляющих разные типы ответов.

TypedResults против Results

Классы Results и TypedResults предоставляют аналогичные наборы помощников для результатов. Класс TypedResults является типизированным аналогом класса . Тем не менее, тип возвращаемого значения для помощников Results — это IResult, тогда как тип возвращаемого значения каждого помощника TypedResults является одним из типов реализации IResult. Разница заключается в том, что для Results помощников требуется конвертация, когда необходим конкретный тип, например, для модульного тестирования. Типы реализации определены в пространстве имен Microsoft.AspNetCore.Http.HttpResults.

Возврат TypedResults вместо Results имеет следующие преимущества:

Рассмотрим следующий конечный точку, для которого создается код состояния 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 во время выполнения, что является распространенной схемой в реализации фильтров.

Вот пример фильтра, который использует один из этих интерфейсов:

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

Дополнительные ресурсы

Минимальные конечные точки поддерживают следующие типы возвращаемых значений:

  1. string - Это включает в себя Task<string> и ValueTask<string>.
  2. T (любой другой тип) - Это включает Task<T> и ValueTask<T>.
  3. Основано на IResult - Это включает Task<IResult> и ValueTask<IResult>.

string возвращаемые значения

Поведение Тип контента
Фреймворк напрямую записывает строку в ответ. text/plain

Рассмотрим следующий обработчик маршрута, который возвращает текст Hello world.

app.MapGet("/hello", () => "Hello World");

Код состояния 200 возвращается с заголовком text/plain Content-Type и следующим содержимым.

Hello World

T (Любой другой тип) возвращаемые значения

Поведение Тип контента
Фреймворк JSON-сериализует ответ. application/json

Рассмотрите следующий обработчик маршрута, который возвращает анонимный тип, содержащий строковое свойство Message.

app.MapGet("/hello", () => new { Message = "Hello World" });

Код состояния 200 возвращается с заголовком application/json Content-Type и следующим содержимым.

{"message":"Hello World"}

IResult возвращаемые значения

Поведение Тип контента
Фреймворк вызывает IResult.ExecuteAsync. Решается реализацией IResult.

Интерфейс IResult определяет контракт, который представляет собой результат HTTP-эндпоинта. Статический класс Results и статический класс TypedResults используются для создания различных IResult объектов, представляющих разные типы ответов.

TypedResults против Results

Классы Results и TypedResults предоставляют аналогичные наборы помощников для результатов. Класс TypedResults является типизированным аналогом класса . Тем не менее, тип возвращаемого значения для помощников Results — это IResult, тогда как тип возвращаемого значения каждого помощника TypedResults является одним из типов реализации IResult. Разница заключается в том, что для Results помощников требуется конвертация, когда необходим конкретный тип, например, для модульного тестирования. Типы реализации определены в пространстве имен Microsoft.AspNetCore.Http.HttpResults.

Возврат TypedResults вместо Results имеет следующие преимущества:

Рассмотрим следующий конечный точку, для которого создается код состояния 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 во время выполнения, что является распространенной схемой в реализации фильтров.

Вот пример фильтра, который использует один из этих интерфейсов:

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

Дополнительные ресурсы