如何在最小 API 应用中创建响应
注意
此版本不是本文的最新版本。 有关当前版本,请参阅本文的 .NET 9 版本。
警告
此版本的 ASP.NET Core 不再受支持。 有关详细信息,请参阅 .NET 和 .NET Core 支持策略。 有关当前版本,请参阅本文的 .NET 9 版本。
最小终结点支持以下类型的返回值:
string
- 这包括Task<string>
和ValueTask<string>
。T
(任何其他类型)- 这包括Task<T>
和ValueTask<T>
。- 基于
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
Results 和 TypedResults 静态类提供类似的结果帮助程序集。 TypedResults
类是 Results
类的类型化等效类。 但是,Results
帮助程序的返回类型是 IResult,而每个 TypedResults
帮助程序的返回类型都是 IResult
实现类型之一。 这种差异意味着,对于 Results
帮助程序来说,当需要具体类型(例如单元测试)时,需要进行转换。 实现类型在 Microsoft.AspNetCore.Http.HttpResults 命名空间中定义。
返回 TypedResults
(而不是 Results
)有以下优点:
TypedResults
帮助程序返回强类型对象,这可以提高代码可读性、改进单元测试并减少运行时错误的可能性。- 实现类型会自动为 OpenAPI 提供响应类型元数据来描述终结点。
考虑以下终结点,将为其生成带有预期 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.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());
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)));
内置结果
Results 和 TypedResults 静态类中存在常见的结果帮助程序。 优先返回 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
类型的方法,这是筛选器实现中的常见模式:
- IContentTypeHttpResult
- IFileHttpResult
- INestedHttpResult
- IStatusCodeHttpResult
- IValueHttpResult
- IValueHttpResult<TValue>
下面是使用其中的接口之一的筛选器示例:
app.MapGet("/weatherforecast", (int days) =>
{
if (days <= 0)
{
return Results.BadRequest();
}
var forecast = Enumerable.Range(1, days).Select(index =>
new WeatherForecast(DateTime.Now.AddDays(index), Random.Shared.Next(-20, 55), "Cool"))
.ToArray();
return Results.Ok(forecast);
}).
AddEndpointFilter(async (context, next) =>
{
var result = await next(context);
return result switch
{
IValueHttpResult<WeatherForecast[]> weatherForecastResult => new WeatherHttpResult(weatherForecastResult.Value),
_ => result
};
});
有关详细信息,请参阅最小 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 OK
的 IProducesResponseTypeMetadata 实现。
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
// }
其他资源
最小终结点支持以下类型的返回值:
string
- 这包括Task<string>
和ValueTask<string>
。T
(任何其他类型)- 这包括Task<T>
和ValueTask<T>
。- 基于
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
Results 和 TypedResults 静态类提供类似的结果帮助程序集。 TypedResults
类是 Results
类的类型化等效类。 但是,Results
帮助程序的返回类型是 IResult,而每个 TypedResults
帮助程序的返回类型都是 IResult
实现类型之一。 这种差异意味着,对于 Results
帮助程序来说,当需要具体类型(例如单元测试)时,需要进行转换。 实现类型在 Microsoft.AspNetCore.Http.HttpResults 命名空间中定义。
返回 TypedResults
(而不是 Results
)有以下优点:
TypedResults
帮助程序返回强类型对象,这可以提高代码可读性、改进单元测试并减少运行时错误的可能性。- 实现类型会自动为 OpenAPI 提供响应类型元数据来描述终结点。
考虑以下终结点,将为其生成带有预期 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.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());
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)));
内置结果
Results 和 TypedResults 静态类中存在常见的结果帮助程序。 优先返回 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
类型的方法,这是筛选器实现中的常见模式:
- IContentTypeHttpResult
- IFileHttpResult
- INestedHttpResult
- IStatusCodeHttpResult
- IValueHttpResult
- IValueHttpResult<TValue>
下面是使用其中的接口之一的筛选器示例:
app.MapGet("/weatherforecast", (int days) =>
{
if (days <= 0)
{
return Results.BadRequest();
}
var forecast = Enumerable.Range(1, days).Select(index =>
new WeatherForecast(DateTime.Now.AddDays(index), Random.Shared.Next(-20, 55), "Cool"))
.ToArray();
return Results.Ok(forecast);
}).
AddEndpointFilter(async (context, next) =>
{
var result = await next(context);
return result switch
{
IValueHttpResult<WeatherForecast[]> weatherForecastResult => new WeatherHttpResult(weatherForecastResult.Value),
_ => result
};
});
有关详细信息,请参阅最小 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 OK
的 IProducesResponseTypeMetadata 实现。
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
// }