如何在最小 API 应用中创建响应

注意

此版本不是本文的最新版本。 对于当前版本,请参阅此文的 .NET 8 版本

警告

此版本的 ASP.NET Core 不再受支持。 有关详细信息,请参阅 .NET 和 .NET Core 支持策略。 对于当前版本,请参阅此文的 .NET 8 版本

重要

此信息与预发布产品相关,相应产品在商业发布之前可能会进行重大修改。 Microsoft 对此处提供的信息不提供任何明示或暗示的保证。

对于当前版本,请参阅此文的 .NET 8 版本

最小终结点支持以下类型的返回值:

  1. string - 这包括 Task<string>ValueTask<string>
  2. T(任何其他类型)- 这包括 Task<T>ValueTask<T>
  3. 基于 IResult - 这包括 Task<IResult>ValueTask<IResult>

string 返回值

行为 Content-Type
框架将字符串直接写入响应。 text/plain

请考虑以下路由处理程序,该处理程序返回 Hello world 文本。

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

200 状态代码与 text/plain Content-Type 标头和以下内容一起返回。

Hello World

T(任何其他类型)返回值

行为 Content-Type
框架将对响应进行 JSON 序列化。 application/json

请考虑以下路由处理程序,该处理程序返回包含 Message 字符串属性的匿名类型。

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

200 状态代码与 application/json Content-Type 标头和以下内容一起返回。

{"message":"Hello World"}

IResult 返回值

行为 Content-Type
框架调用 IResult.ExecuteAsync IResult 实现决定。

IResult 接口定义一个表示 HTTP 终结点结果的协定。 静态 Results 类和静态 TypedResults 用于创建表示不同类型的响应的各种 IResult 对象。

TypedResults 与 Results

ResultsTypedResults 静态类提供类似的结果帮助程序集。 TypedResults 类是 Results 类的类型化等效类。 但是,Results 帮助程序的返回类型是 IResult,而每个 TypedResults 帮助程序的返回类型都是 IResult 实现类型之一。 这种差异意味着,对于 Results 帮助程序来说,当需要具体类型(例如单元测试)时,需要进行转换。 实现类型在 Microsoft.AspNetCore.Http.HttpResults 命名空间中定义。

返回 TypedResults(而不是 Results)有以下优点:

考虑以下终结点,将为其生成带有预期 JSON 响应的 200 OK 状态代码。

app.MapGet("/hello", () => Results.Ok(new Message() { Text = "Hello World!" }))
    .Produces<Message>();

为了正确记录此终结点,调用扩展方法 Produces。 但是,如果使用 TypedResults 而不是 Results,则不需要调用 Produces,如以下代码所示。 TypedResults 自动提供终结点的元数据。

app.MapGet("/hello2", () => TypedResults.Ok(new Message() { Text = "Hello World!" }));

有关描述响应类型的详细信息,请参阅最小 API 中的 OpenAPI 支持

如前所述,使用 TypedResults时,不需要转换。 请考虑以下返回 TypedResults 类的最小 API

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.OkResults.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.OkTypedResults.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());

Results<TResult1, TResultN>

在以下情况下,使用 Results<TResult1, TResultN>(而不是 IResult)作为终结点处理程序返回类型:

  • 从终结点处理程序返回多个 IResult 实现类型。
  • 静态 TypedResult 类用于创建 IResult 对象。

此替代方法比返回 IResult 更好,因为泛型联合类型会自动保留终结点元数据。 由于 Results<TResult1, TResultN> 联合类型实现隐式强制转换运算符,编译器可以自动将泛型参数中指定的类型转换为联合类型的实例。

这增加了一个好处,即提供编译时检查,路由处理程序实际上只返回声明它的结果。 尝试返回未声明为 Results<> 泛型参数之一的类型会导致编译错误。

请考虑以下终结点,当 orderId 大于 999 时,将为其返回 400 BadRequest 状态代码。 否则,它会生成具有预期内容的 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)));

内置结果

ResultsTypedResults 静态类中存在常见的结果帮助程序。 优先返回 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);
}

以下示例从 Azure Blob 存储流式传输映像:

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
    };
});

有关详细信息,请参阅最小 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 和状态代码 200 OKIProducesResponseTypeMetadata 实现。

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 应用在 JSON 序列化和反序列化期间使用 Web defaults 选项。

全局配置 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
// }

或者,使用接受 JsonSerializerOptions 对象的 WriteAsJsonAsync 的重载。 以下示例使用此重载设置输出 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 返回值

行为 Content-Type
框架将字符串直接写入响应。 text/plain

请考虑以下路由处理程序,该处理程序返回 Hello world 文本。

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

200 状态代码与 text/plain Content-Type 标头和以下内容一起返回。

Hello World

T(任何其他类型)返回值

行为 Content-Type
框架将对响应进行 JSON 序列化。 application/json

请考虑以下路由处理程序,该处理程序返回包含 Message 字符串属性的匿名类型。

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

200 状态代码与 application/json Content-Type 标头和以下内容一起返回。

{"message":"Hello World"}

IResult 返回值

行为 Content-Type
框架调用 IResult.ExecuteAsync IResult 实现决定。

IResult 接口定义一个表示 HTTP 终结点结果的协定。 静态 Results 类和静态 TypedResults 用于创建表示不同类型的响应的各种 IResult 对象。

TypedResults 与 Results

ResultsTypedResults 静态类提供类似的结果帮助程序集。 TypedResults 类是 Results 类的类型化等效类。 但是,Results 帮助程序的返回类型是 IResult,而每个 TypedResults 帮助程序的返回类型都是 IResult 实现类型之一。 这种差异意味着,对于 Results 帮助程序来说,当需要具体类型(例如单元测试)时,需要进行转换。 实现类型在 Microsoft.AspNetCore.Http.HttpResults 命名空间中定义。

返回 TypedResults(而不是 Results)有以下优点:

考虑以下终结点,将为其生成带有预期 JSON 响应的 200 OK 状态代码。

app.MapGet("/hello", () => Results.Ok(new Message() { Text = "Hello World!" }))
    .Produces<Message>();

为了正确记录此终结点,调用扩展方法 Produces。 但是,如果使用 TypedResults 而不是 Results,则不需要调用 Produces,如以下代码所示。 TypedResults 自动提供终结点的元数据。

app.MapGet("/hello2", () => TypedResults.Ok(new Message() { Text = "Hello World!" }));

有关描述响应类型的详细信息,请参阅最小 API 中的 OpenAPI 支持

如前所述,使用 TypedResults时,不需要转换。 请考虑以下返回 TypedResults 类的最小 API

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.OkResults.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.OkTypedResults.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());

Results<TResult1, TResultN>

在以下情况下,使用 Results<TResult1, TResultN>(而不是 IResult)作为终结点处理程序返回类型:

  • 从终结点处理程序返回多个 IResult 实现类型。
  • 静态 TypedResult 类用于创建 IResult 对象。

此替代方法比返回 IResult 更好,因为泛型联合类型会自动保留终结点元数据。 由于 Results<TResult1, TResultN> 联合类型实现隐式强制转换运算符,编译器可以自动将泛型参数中指定的类型转换为联合类型的实例。

这增加了一个好处,即提供编译时检查,路由处理程序实际上只返回声明它的结果。 尝试返回未声明为 Results<> 泛型参数之一的类型会导致编译错误。

请考虑以下终结点,当 orderId 大于 999 时,将为其返回 400 BadRequest 状态代码。 否则,它会生成具有预期内容的 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)));

内置结果

ResultsTypedResults 静态类中存在常见的结果帮助程序。 优先返回 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);
}

以下示例从 Azure Blob 存储流式传输映像:

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
    };
});

有关详细信息,请参阅最小 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 和状态代码 200 OKIProducesResponseTypeMetadata 实现。

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 应用在 JSON 序列化和反序列化期间使用 Web defaults 选项。

全局配置 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
// }

或者,使用接受 JsonSerializerOptions 对象的 WriteAsJsonAsync 的重载。 以下示例使用此重载设置输出 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
// }

其他资源