Поделиться через


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

Примечание.

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

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

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

Внимание

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

В текущем выпуске см . версию .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. Класс статических результатов и статический TypedResults используются для создания различных объектов, представляющих различные IResult типы ответов.

TypedResults и результаты

Вспомогательные классы и TypedResults статические Results классы предоставляют аналогичные наборы вспомогательных элементов результатов. Класс TypedResults является типизированным эквивалентом Results класса. 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<TResult1, TResultN> типы объединения реализуют неявные операторы приведения, компилятор может автоматически преобразовать типы, указанные в универсальных аргументах, в экземпляр типа объединения.

Это дает дополнительное преимущество при проверке времени компиляции, что обработчик маршрутов фактически возвращает только результаты, объявленные им. Попытка вернуть тип, который не объявлен как один из универсальных аргументов, приводит к Results<> ошибке компиляции.

Рассмотрим следующую конечную точку, для которой 400 BadRequest возвращается код состояния, если значение orderId больше 999. В противном случае он создает ожидаемый 200 OK контент.

app.MapGet("/orders/{orderId}", IResult (int orderId)
    => orderId > 999 ? TypedResults.BadRequest() : TypedResults.Ok(new Order(orderId)))
    .Produces(400)
    .Produces<Order>();

Чтобы правильно задокументировать эту конечную точку, вызывается метод Produces расширения. Однако, так как вспомогательный TypedResults элемент автоматически включает метаданные конечной точки, можно вернуть Results<T1, Tn> тип объединения, как показано в следующем коде.

app.MapGet("/orders/{orderId}", Results<BadRequest, Ok<Order>> (int orderId)
    => orderId > 999 ? TypedResults.BadRequest() : TypedResults.Ok(new Order(orderId)));

Встроенные результаты

Распространенные вспомогательные средства результатов существуют в и TypedResults статических Results классах. TypedResults Возврат предпочтительнее возвращатьResults. Дополнительные сведения см. в разделе TypedResults и Results.

В следующих разделах показано использование распространенных вспомогательных средств результатов.

JSON

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

WriteAsJsonAsync — альтернативный способ возврата JSON:

app.MapGet("/", (HttpContext context) => context.Response.WriteAsJsonAsync
    (new { Message = "Hello World" }));

Пользовательский код состояния

app.MapGet("/405", () => Results.StatusCode(405));

Внутренняя ошибка сервера

app.MapGet("/500", () => Results.InternalServerError("Something went wrong!"));

В предыдущем примере возвращается код состояния 500.

Текст

app.MapGet("/text", () => Results.Text("This is some text"));

Stream

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

var proxyClient = new HttpClient();
app.MapGet("/pokemon", async () => 
{
    var stream = await proxyClient.GetStreamAsync("http://contoso/pokedex.json");
    // Proxy the response as JSON
    return Results.Stream(stream, "application/json");
});

app.Run();

Перегрузки Results.Stream позволяют получить доступ к основному потоку HTTP-ответов без буферизации. В следующем примере используется ImageSharp для возврата уменьшенного размера указанного изображения:

using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Formats.Jpeg;
using SixLabors.ImageSharp.Processing;

var builder = WebApplication.CreateBuilder(args);

var app = builder.Build();

app.MapGet("/process-image/{strImage}", (string strImage, HttpContext http, CancellationToken token) =>
{
    http.Response.Headers.CacheControl = $"public,max-age={TimeSpan.FromHours(24).TotalSeconds}";
    return Results.Stream(stream => ResizeImageAsync(strImage, stream, token), "image/jpeg");
});

async Task ResizeImageAsync(string strImage, Stream stream, CancellationToken token)
{
    var strPath = $"wwwroot/img/{strImage}";
    using var image = await Image.LoadAsync(strPath, token);
    int width = image.Width / 2;
    int height = image.Height / 2;
    image.Mutate(x =>x.Resize(width, height));
    await image.SaveAsync(stream, JpegFormat.Instance, cancellationToken: token);
}

В следующем примере выполняется потоковая передача изображения из хранилища BLOB-объектов Azure:

app.MapGet("/stream-image/{containerName}/{blobName}", 
    async (string blobName, string containerName, CancellationToken token) =>
{
    var conStr = builder.Configuration["blogConStr"];
    BlobContainerClient blobContainerClient = new BlobContainerClient(conStr, containerName);
    BlobClient blobClient = blobContainerClient.GetBlobClient(blobName);
    return Results.Stream(await blobClient.OpenReadAsync(cancellationToken: token), "image/jpeg");
});

В следующем примере выполняется потоковая передача изображения из BLOB-объекта Azure:

// GET /stream-video/videos/earth.mp4
app.MapGet("/stream-video/{containerName}/{blobName}",
     async (HttpContext http, CancellationToken token, string blobName, string containerName) =>
{
    var conStr = builder.Configuration["blogConStr"];
    BlobContainerClient blobContainerClient = new BlobContainerClient(conStr, containerName);
    BlobClient blobClient = blobContainerClient.GetBlobClient(blobName);
    
    var properties = await blobClient.GetPropertiesAsync(cancellationToken: token);
    
    DateTimeOffset lastModified = properties.Value.LastModified;
    long length = properties.Value.ContentLength;
    
    long etagHash = lastModified.ToFileTime() ^ length;
    var entityTag = new EntityTagHeaderValue('\"' + Convert.ToString(etagHash, 16) + '\"');
    
    http.Response.Headers.CacheControl = $"public,max-age={TimeSpan.FromHours(24).TotalSeconds}";

    return Results.Stream(await blobClient.OpenReadAsync(cancellationToken: token), 
        contentType: "video/mp4",
        lastModified: lastModified,
        entityTag: entityTag,
        enableRangeProcessing: true);
});

Перенаправление

app.MapGet("/old-path", () => Results.Redirect("/new-path"));

Файлы

app.MapGet("/download", () => Results.File("myfile.text"));

Интерфейсы HttpResult

Следующие интерфейсы в Microsoft.AspNetCore.Http пространстве имен предоставляют способ обнаружения IResult типа во время выполнения, который является общим шаблоном в реализации фильтров:

Ниже приведен пример фильтра, использующего один из следующих интерфейсов:

app.MapGet("/weatherforecast", (int days) =>
{
    if (days <= 0)
    {
        return Results.BadRequest();
    }

    var forecast = Enumerable.Range(1, days).Select(index =>
       new WeatherForecast(DateTime.Now.AddDays(index), Random.Shared.Next(-20, 55), "Cool"))
        .ToArray();
    return Results.Ok(forecast);
}).
AddEndpointFilter(async (context, next) =>
{
    var result = await next(context);

    return result switch
    {
        IValueHttpResult<WeatherForecast[]> weatherForecastResult => new WeatherHttpResult(weatherForecastResult.Value),
        _ => result
    };
});

Дополнительные сведения см. в статьях "Фильтры" в минимальных приложениях API и типах реализации IResult.

Настройка ответов

Приложения могут управлять ответами через пользовательскую реализацию типа IResult. Следующий код является примером результата с типом HTML:

using System.Net.Mime;
using System.Text;
static class ResultsExtensions
{
    public static IResult Html(this IResultExtensions resultExtensions, string html)
    {
        ArgumentNullException.ThrowIfNull(resultExtensions);

        return new HtmlResult(html);
    }
}

class HtmlResult : IResult
{
    private readonly string _html;

    public HtmlResult(string html)
    {
        _html = html;
    }

    public Task ExecuteAsync(HttpContext httpContext)
    {
        httpContext.Response.ContentType = MediaTypeNames.Text.Html;
        httpContext.Response.ContentLength = Encoding.UTF8.GetByteCount(_html);
        return httpContext.Response.WriteAsync(_html);
    }
}

Мы рекомендуем добавить в Microsoft.AspNetCore.Http.IResultExtensions метод расширения, чтобы эти пользовательские результаты было проще искать.

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/html", () => Results.Extensions.Html(@$"<!doctype html>
<html>
    <head><title>miniHTML</title></head>
    <body>
        <h1>Hello World</h1>
        <p>The time on the server is {DateTime.Now:O}</p>
    </body>
</html>"));

app.Run();

Кроме того, настраиваемый IResult тип может предоставить собственную заметку, реализуя IEndpointMetadataProvider интерфейс. Например, следующий код добавляет заметку к предыдущему HtmlResult типу, описывающего ответ, созданный конечной точкой.

class HtmlResult : IResult, IEndpointMetadataProvider
{
    private readonly string _html;

    public HtmlResult(string html)
    {
        _html = html;
    }

    public Task ExecuteAsync(HttpContext httpContext)
    {
        httpContext.Response.ContentType = MediaTypeNames.Text.Html;
        httpContext.Response.ContentLength = Encoding.UTF8.GetByteCount(_html);
        return httpContext.Response.WriteAsync(_html);
    }

    public static void PopulateMetadata(MethodInfo method, EndpointBuilder builder)
    {
        builder.Metadata.Add(new ProducesHtmlMetadata());
    }
}

Это ProducesHtmlMetadata реализация, которая определяет тип text/html контента создаваемого IProducesResponseTypeMetadata ответа и код 200 OKсостояния.

internal sealed class ProducesHtmlMetadata : IProducesResponseTypeMetadata
{
    public Type? Type => null;

    public int StatusCode => 200;

    public IEnumerable<string> ContentTypes { get; } = new[] { MediaTypeNames.Text.Html };
}

Альтернативный подход используется Microsoft.AspNetCore.Mvc.ProducesAttribute для описания создаваемого ответа. Следующий код изменяет используемый PopulateMetadata метод ProducesAttribute.

public static void PopulateMetadata(MethodInfo method, EndpointBuilder builder)
{
    builder.Metadata.Add(new ProducesAttribute(MediaTypeNames.Text.Html));
}

Настройка параметров сериализации JSON

По умолчанию минимальные приложения API используют Web defaults параметры во время сериализации и десериализации JSON.

Настройка параметров сериализации JSON глобально

Параметры можно настроить глобально для приложения путем ConfigureHttpJsonOptionsвызова. В следующем примере содержатся общедоступные поля и форматы выходных данных JSON.

var builder = WebApplication.CreateBuilder(args);

builder.Services.ConfigureHttpJsonOptions(options => {
    options.SerializerOptions.WriteIndented = true;
    options.SerializerOptions.IncludeFields = true;
});

var app = builder.Build();

app.MapPost("/", (Todo todo) => {
    if (todo is not null) {
        todo.Name = todo.NameField;
    }
    return todo;
});

app.Run();

class Todo {
    public string? Name { get; set; }
    public string? NameField;
    public bool IsComplete { get; set; }
}
// If the request body contains the following JSON:
//
// {"nameField":"Walk dog", "isComplete":false}
//
// The endpoint returns the following JSON:
//
// {
//    "name":"Walk dog",
//    "nameField":"Walk dog",
//    "isComplete":false
// }

Так как поля включены, предыдущий код считывает NameField и включает его в выходной json.

Настройка параметров сериализации JSON для конечной точки

Чтобы настроить параметры сериализации для конечной точки, вызовите Results.Json и передайте в него JsonSerializerOptions объект, как показано в следующем примере:

using System.Text.Json;

var app = WebApplication.Create();

var options = new JsonSerializerOptions(JsonSerializerDefaults.Web)
    { WriteIndented = true };

app.MapGet("/", () => 
    Results.Json(new Todo { Name = "Walk dog", IsComplete = false }, options));

app.Run();

class Todo
{
    public string? Name { get; set; }
    public bool IsComplete { get; set; }
}
// The endpoint returns the following JSON:
//
// {
//   "name":"Walk dog",
//   "isComplete":false
// }

В качестве альтернативы используйте перегрузку WriteAsJsonAsync , которая принимает JsonSerializerOptions объект. В следующем примере используется эта перегрузка для форматирования выходного JSON:

using System.Text.Json;

var app = WebApplication.Create();

var options = new JsonSerializerOptions(JsonSerializerDefaults.Web) {
    WriteIndented = true };

app.MapGet("/", (HttpContext context) =>
    context.Response.WriteAsJsonAsync<Todo>(
        new Todo { Name = "Walk dog", IsComplete = false }, options));

app.Run();

class Todo
{
    public string? Name { get; set; }
    public bool IsComplete { get; set; }
}
// The endpoint returns the following JSON:
//
// {
//   "name":"Walk dog",
//   "isComplete":false
// }

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

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

  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. Класс статических результатов и статический TypedResults используются для создания различных объектов, представляющих различные IResult типы ответов.

TypedResults и результаты

Вспомогательные классы и TypedResults статические Results классы предоставляют аналогичные наборы вспомогательных элементов результатов. Класс TypedResults является типизированным эквивалентом Results класса. 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<TResult1, TResultN> типы объединения реализуют неявные операторы приведения, компилятор может автоматически преобразовать типы, указанные в универсальных аргументах, в экземпляр типа объединения.

Это дает дополнительное преимущество при проверке времени компиляции, что обработчик маршрутов фактически возвращает только результаты, объявленные им. Попытка вернуть тип, который не объявлен как один из универсальных аргументов, приводит к Results<> ошибке компиляции.

Рассмотрим следующую конечную точку, для которой 400 BadRequest возвращается код состояния, если значение orderId больше 999. В противном случае он создает ожидаемый 200 OK контент.

app.MapGet("/orders/{orderId}", IResult (int orderId)
    => orderId > 999 ? TypedResults.BadRequest() : TypedResults.Ok(new Order(orderId)))
    .Produces(400)
    .Produces<Order>();

Чтобы правильно задокументировать эту конечную точку, вызывается метод Produces расширения. Однако, так как вспомогательный TypedResults элемент автоматически включает метаданные конечной точки, можно вернуть Results<T1, Tn> тип объединения, как показано в следующем коде.

app.MapGet("/orders/{orderId}", Results<BadRequest, Ok<Order>> (int orderId) 
    => orderId > 999 ? TypedResults.BadRequest() : TypedResults.Ok(new Order(orderId)));

Встроенные результаты

Распространенные вспомогательные средства результатов существуют в и TypedResults статических Results классах. TypedResults Возврат предпочтительнее возвращатьResults. Дополнительные сведения см. в разделе TypedResults и Results.

В следующих разделах показано использование распространенных вспомогательных средств результатов.

JSON

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

WriteAsJsonAsync — альтернативный способ возврата JSON:

app.MapGet("/", (HttpContext context) => context.Response.WriteAsJsonAsync
    (new { Message = "Hello World" }));

Пользовательский код состояния

app.MapGet("/405", () => Results.StatusCode(405));

Текст

app.MapGet("/text", () => Results.Text("This is some text"));

Stream

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

var proxyClient = new HttpClient();
app.MapGet("/pokemon", async () => 
{
    var stream = await proxyClient.GetStreamAsync("http://contoso/pokedex.json");
    // Proxy the response as JSON
    return Results.Stream(stream, "application/json");
});

app.Run();

Перегрузки Results.Stream позволяют получить доступ к основному потоку HTTP-ответов без буферизации. В следующем примере используется ImageSharp для возврата уменьшенного размера указанного изображения:

using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Formats.Jpeg;
using SixLabors.ImageSharp.Processing;

var builder = WebApplication.CreateBuilder(args);

var app = builder.Build();

app.MapGet("/process-image/{strImage}", (string strImage, HttpContext http, CancellationToken token) =>
{
    http.Response.Headers.CacheControl = $"public,max-age={TimeSpan.FromHours(24).TotalSeconds}";
    return Results.Stream(stream => ResizeImageAsync(strImage, stream, token), "image/jpeg");
});

async Task ResizeImageAsync(string strImage, Stream stream, CancellationToken token)
{
    var strPath = $"wwwroot/img/{strImage}";
    using var image = await Image.LoadAsync(strPath, token);
    int width = image.Width / 2;
    int height = image.Height / 2;
    image.Mutate(x =>x.Resize(width, height));
    await image.SaveAsync(stream, JpegFormat.Instance, cancellationToken: token);
}

В следующем примере выполняется потоковая передача изображения из хранилища BLOB-объектов Azure:

app.MapGet("/stream-image/{containerName}/{blobName}", 
    async (string blobName, string containerName, CancellationToken token) =>
{
    var conStr = builder.Configuration["blogConStr"];
    BlobContainerClient blobContainerClient = new BlobContainerClient(conStr, containerName);
    BlobClient blobClient = blobContainerClient.GetBlobClient(blobName);
    return Results.Stream(await blobClient.OpenReadAsync(cancellationToken: token), "image/jpeg");
});

В следующем примере выполняется потоковая передача изображения из BLOB-объекта Azure:

// GET /stream-video/videos/earth.mp4
app.MapGet("/stream-video/{containerName}/{blobName}",
     async (HttpContext http, CancellationToken token, string blobName, string containerName) =>
{
    var conStr = builder.Configuration["blogConStr"];
    BlobContainerClient blobContainerClient = new BlobContainerClient(conStr, containerName);
    BlobClient blobClient = blobContainerClient.GetBlobClient(blobName);
    
    var properties = await blobClient.GetPropertiesAsync(cancellationToken: token);
    
    DateTimeOffset lastModified = properties.Value.LastModified;
    long length = properties.Value.ContentLength;
    
    long etagHash = lastModified.ToFileTime() ^ length;
    var entityTag = new EntityTagHeaderValue('\"' + Convert.ToString(etagHash, 16) + '\"');
    
    http.Response.Headers.CacheControl = $"public,max-age={TimeSpan.FromHours(24).TotalSeconds}";

    return Results.Stream(await blobClient.OpenReadAsync(cancellationToken: token), 
        contentType: "video/mp4",
        lastModified: lastModified,
        entityTag: entityTag,
        enableRangeProcessing: true);
});

Перенаправление

app.MapGet("/old-path", () => Results.Redirect("/new-path"));

Файлы

app.MapGet("/download", () => Results.File("myfile.text"));

Интерфейсы HttpResult

Следующие интерфейсы в Microsoft.AspNetCore.Http пространстве имен предоставляют способ обнаружения IResult типа во время выполнения, который является общим шаблоном в реализации фильтров:

Ниже приведен пример фильтра, использующего один из следующих интерфейсов:

app.MapGet("/weatherforecast", (int days) =>
{
    if (days <= 0)
    {
        return Results.BadRequest();
    }

    var forecast = Enumerable.Range(1, days).Select(index =>
       new WeatherForecast(DateTime.Now.AddDays(index), Random.Shared.Next(-20, 55), "Cool"))
        .ToArray();
    return Results.Ok(forecast);
}).
AddEndpointFilter(async (context, next) =>
{
    var result = await next(context);

    return result switch
    {
        IValueHttpResult<WeatherForecast[]> weatherForecastResult => new WeatherHttpResult(weatherForecastResult.Value),
        _ => result
    };
});

Дополнительные сведения см. в статьях "Фильтры" в минимальных приложениях API и типах реализации IResult.

Настройка ответов

Приложения могут управлять ответами через пользовательскую реализацию типа IResult. Следующий код является примером результата с типом HTML:

using System.Net.Mime;
using System.Text;
static class ResultsExtensions
{
    public static IResult Html(this IResultExtensions resultExtensions, string html)
    {
        ArgumentNullException.ThrowIfNull(resultExtensions);

        return new HtmlResult(html);
    }
}

class HtmlResult : IResult
{
    private readonly string _html;

    public HtmlResult(string html)
    {
        _html = html;
    }

    public Task ExecuteAsync(HttpContext httpContext)
    {
        httpContext.Response.ContentType = MediaTypeNames.Text.Html;
        httpContext.Response.ContentLength = Encoding.UTF8.GetByteCount(_html);
        return httpContext.Response.WriteAsync(_html);
    }
}

Мы рекомендуем добавить в Microsoft.AspNetCore.Http.IResultExtensions метод расширения, чтобы эти пользовательские результаты было проще искать.

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/html", () => Results.Extensions.Html(@$"<!doctype html>
<html>
    <head><title>miniHTML</title></head>
    <body>
        <h1>Hello World</h1>
        <p>The time on the server is {DateTime.Now:O}</p>
    </body>
</html>"));

app.Run();

Кроме того, настраиваемый IResult тип может предоставить собственную заметку, реализуя IEndpointMetadataProvider интерфейс. Например, следующий код добавляет заметку к предыдущему HtmlResult типу, описывающего ответ, созданный конечной точкой.

class HtmlResult : IResult, IEndpointMetadataProvider
{
    private readonly string _html;

    public HtmlResult(string html)
    {
        _html = html;
    }

    public Task ExecuteAsync(HttpContext httpContext)
    {
        httpContext.Response.ContentType = MediaTypeNames.Text.Html;
        httpContext.Response.ContentLength = Encoding.UTF8.GetByteCount(_html);
        return httpContext.Response.WriteAsync(_html);
    }

    public static void PopulateMetadata(MethodInfo method, EndpointBuilder builder)
    {
        builder.Metadata.Add(new ProducesHtmlMetadata());
    }
}

Это ProducesHtmlMetadata реализация, которая определяет тип text/html контента создаваемого IProducesResponseTypeMetadata ответа и код 200 OKсостояния.

internal sealed class ProducesHtmlMetadata : IProducesResponseTypeMetadata
{
    public Type? Type => null;

    public int StatusCode => 200;

    public IEnumerable<string> ContentTypes { get; } = new[] { MediaTypeNames.Text.Html };
}

Альтернативный подход используется Microsoft.AspNetCore.Mvc.ProducesAttribute для описания создаваемого ответа. Следующий код изменяет используемый PopulateMetadata метод ProducesAttribute.

public static void PopulateMetadata(MethodInfo method, EndpointBuilder builder)
{
    builder.Metadata.Add(new ProducesAttribute(MediaTypeNames.Text.Html));
}

Настройка параметров сериализации JSON

По умолчанию минимальные приложения API используют Web defaults параметры во время сериализации и десериализации JSON.

Настройка параметров сериализации JSON глобально

Параметры можно настроить глобально для приложения путем ConfigureHttpJsonOptionsвызова. В следующем примере содержатся общедоступные поля и форматы выходных данных JSON.

var builder = WebApplication.CreateBuilder(args);

builder.Services.ConfigureHttpJsonOptions(options => {
    options.SerializerOptions.WriteIndented = true;
    options.SerializerOptions.IncludeFields = true;
});

var app = builder.Build();

app.MapPost("/", (Todo todo) => {
    if (todo is not null) {
        todo.Name = todo.NameField;
    }
    return todo;
});

app.Run();

class Todo {
    public string? Name { get; set; }
    public string? NameField;
    public bool IsComplete { get; set; }
}
// If the request body contains the following JSON:
//
// {"nameField":"Walk dog", "isComplete":false}
//
// The endpoint returns the following JSON:
//
// {
//    "name":"Walk dog",
//    "nameField":"Walk dog",
//    "isComplete":false
// }

Так как поля включены, предыдущий код считывает NameField и включает его в выходной json.

Настройка параметров сериализации JSON для конечной точки

Чтобы настроить параметры сериализации для конечной точки, вызовите Results.Json и передайте в него JsonSerializerOptions объект, как показано в следующем примере:

using System.Text.Json;

var app = WebApplication.Create();

var options = new JsonSerializerOptions(JsonSerializerDefaults.Web)
    { WriteIndented = true };

app.MapGet("/", () => 
    Results.Json(new Todo { Name = "Walk dog", IsComplete = false }, options));

app.Run();

class Todo
{
    public string? Name { get; set; }
    public bool IsComplete { get; set; }
}
// The endpoint returns the following JSON:
//
// {
//   "name":"Walk dog",
//   "isComplete":false
// }

В качестве альтернативы используйте перегрузку WriteAsJsonAsync , которая принимает JsonSerializerOptions объект. В следующем примере используется эта перегрузка для форматирования выходного JSON:

using System.Text.Json;

var app = WebApplication.Create();

var options = new JsonSerializerOptions(JsonSerializerDefaults.Web) {
    WriteIndented = true };

app.MapGet("/", (HttpContext context) =>
    context.Response.WriteAsJsonAsync<Todo>(
        new Todo { Name = "Walk dog", IsComplete = false }, options));

app.Run();

class Todo
{
    public string? Name { get; set; }
    public bool IsComplete { get; set; }
}
// The endpoint returns the following JSON:
//
// {
//   "name":"Walk dog",
//   "isComplete":false
// }

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