MVC 和 Razor 页面
支持 MVC 视图和 Razor 页面中的可为空模型
支持可为空页面或视图模型以改善在 ASP.NET Core 应用中使用 null 状态检查时的体验:
@model Product?
API 控制器
在 API 控制器中使用 DI 进行参数绑定
当类型被配置为服务时,API 控制器操作的参数绑定通过依赖关系注入绑定参数。 这意味着不再需要将 [FromServices]
属性显式应用到参数。 在以下代码中,这两个操作返回时间:
[Route("[controller]")]
[ApiController]
public class MyController : ControllerBase
{
public ActionResult GetWithAttribute([FromServices] IDateTime dateTime)
=> Ok(dateTime.Now);
[Route("noAttribute")]
public ActionResult Get(IDateTime dateTime) => Ok(dateTime.Now);
}
在极少数情况下,自动 DI 可能会中断 DI 中具有 API 控制器操作方法中也接受的类型的应用。 在 DI 中拥有类型并作为 API 控制器操作中的参数并不常见。 若要禁用参数的自动绑定,请设置 DisableImplicitFromServicesParameters
using Microsoft.AspNetCore.Mvc;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddSingleton<IDateTime, SystemDateTime>();
builder.Services.Configure<ApiBehaviorOptions>(options =>
{
options.DisableImplicitFromServicesParameters = true;
});
var app = builder.Build();
app.MapControllers();
app.Run();
在 ASP.NET Core 7.0 中,DI 中的类型将在应用启动时使用 IServiceProviderIsService 来检查,以确定 API 控制器操作中的参数是来自 DI 还是来自其他源。
用于推断 API 控制器操作参数绑定源的新机制使用以下规则:
- 以前指定的
BindingInfo.BindingSource
永远不会被覆盖。 - 在 DI 容器中注册的复杂类型参数被分配
BindingSource.Services
。 - 在 DI 容器中未注册的复杂类型参数被分配
BindingSource.Body
。 - 具有在任何路由模板中显示为路由值的名称的参数被分配
BindingSource.Path
。 - 所有其他参数都是
BindingSource.Query
。
最小 API
最小 API 筛选器应用
使用最小 API 筛选器,开发人员可以实现支持以下操作的业务逻辑:
- 在路由处理程序前后运行代码。
- 检查和修改路由处理程序调用期间提供的参数。
- 截获路由处理程序的响应行为。
在以下场景中,筛选器很有用:
- 验证已发送到终结点的请求参数和正文。
- 记录有关请求和响应的信息。
- 验证请求是否面向受支持的 API 版本。
绑定标头和查询字符串中的数组和字符串值
在 ASP.NET 7 中,支持将查询字符串绑定到基元类型、字符串数组和 StringValues 数组:
// Bind query string values to a primitive type array.
// GET /tags?q=1&q=2&q=3
app.MapGet("/tags", (int[] q) =>
$"tag1: {q[0]} , tag2: {q[1]}, tag3: {q[2]}");
// Bind to a string array.
// GET /tags2?names=john&names=jack&names=jane
app.MapGet("/tags2", (string[] names) =>
$"tag1: {names[0]} , tag2: {names[1]}, tag3: {names[2]}");
// Bind to StringValues.
// GET /tags3?names=john&names=jack&names=jane
app.MapGet("/tags3", (StringValues names) =>
$"tag1: {names[0]} , tag2: {names[1]}, tag3: {names[2]}");
在类型实现 TryParse
时,支持将查询字符串或标头值绑定到复杂类型的数组。
将请求正文绑定为 Stream
或 PipeReader
请求正文可以绑定为 Stream
或 PipeReader
,以有效支持用户必须处理数据的情况,以及:
- 将数据存储在 Blob 存储中,或将数据排入队列提供程序的队列。
- 使用工作进程或云功能处理存储的数据。
例如,数据可能排队到 Azure 队列存储 或存储在 Azure Blob 存储中。
以下代码可实现后台队列:
using System.Text.Json;
using System.Threading.Channels;
namespace BackgroundQueueService;
class BackgroundQueue : BackgroundService
{
private readonly Channel<ReadOnlyMemory<byte>> _queue;
private readonly ILogger<BackgroundQueue> _logger;
public BackgroundQueue(Channel<ReadOnlyMemory<byte>> queue,
ILogger<BackgroundQueue> logger)
{
_queue = queue;
_logger = logger;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
await foreach (var dataStream in _queue.Reader.ReadAllAsync(stoppingToken))
{
try
{
var person = JsonSerializer.Deserialize<Person>(dataStream.Span)!;
_logger.LogInformation($"{person.Name} is {person.Age} " +
$"years and from {person.Country}");
}
catch (Exception ex)
{
_logger.LogError(ex.Message);
}
}
}
}
class Person
{
public string Name { get; set; } = String.Empty;
public int Age { get; set; }
public string Country { get; set; } = String.Empty;
}
以下代码将请求正文绑定到 Stream
:
app.MapPost("/register", async (HttpRequest req, Stream body,
Channel<ReadOnlyMemory<byte>> queue) =>
{
if (req.ContentLength is not null && req.ContentLength > maxMessageSize)
{
return Results.BadRequest();
}
// We're not above the message size and we have a content length, or
// we're a chunked request and we're going to read up to the maxMessageSize + 1.
// We add one to the message size so that we can detect when a chunked request body
// is bigger than our configured max.
var readSize = (int?)req.ContentLength ?? (maxMessageSize + 1);
var buffer = new byte[readSize];
// Read at least that many bytes from the body.
var read = await body.ReadAtLeastAsync(buffer, readSize, throwOnEndOfStream: false);
// We read more than the max, so this is a bad request.
if (read > maxMessageSize)
{
return Results.BadRequest();
}
// Attempt to send the buffer to the background queue.
if (queue.Writer.TryWrite(buffer.AsMemory(0..read)))
{
return Results.Accepted();
}
// We couldn't accept the message since we're overloaded.
return Results.StatusCode(StatusCodes.Status429TooManyRequests);
});
以下代码显示完整的 Program.cs
文件:
using System.Threading.Channels;
using BackgroundQueueService;
var builder = WebApplication.CreateBuilder(args);
// The max memory to use for the upload endpoint on this instance.
var maxMemory = 500 * 1024 * 1024;
// The max size of a single message, staying below the default LOH size of 85K.
var maxMessageSize = 80 * 1024;
// The max size of the queue based on those restrictions
var maxQueueSize = maxMemory / maxMessageSize;
// Create a channel to send data to the background queue.
builder.Services.AddSingleton<Channel<ReadOnlyMemory<byte>>>((_) =>
Channel.CreateBounded<ReadOnlyMemory<byte>>(maxQueueSize));
// Create a background queue service.
builder.Services.AddHostedService<BackgroundQueue>();
var app = builder.Build();
// curl --request POST 'https://localhost:<port>/register' --header 'Content-Type: application/json' --data-raw '{ "Name":"Samson", "Age": 23, "Country":"Nigeria" }'
// curl --request POST "https://localhost:<port>/register" --header "Content-Type: application/json" --data-raw "{ \"Name\":\"Samson\", \"Age\": 23, \"Country\":\"Nigeria\" }"
app.MapPost("/register", async (HttpRequest req, Stream body,
Channel<ReadOnlyMemory<byte>> queue) =>
{
if (req.ContentLength is not null && req.ContentLength > maxMessageSize)
{
return Results.BadRequest();
}
// We're not above the message size and we have a content length, or
// we're a chunked request and we're going to read up to the maxMessageSize + 1.
// We add one to the message size so that we can detect when a chunked request body
// is bigger than our configured max.
var readSize = (int?)req.ContentLength ?? (maxMessageSize + 1);
var buffer = new byte[readSize];
// Read at least that many bytes from the body.
var read = await body.ReadAtLeastAsync(buffer, readSize, throwOnEndOfStream: false);
// We read more than the max, so this is a bad request.
if (read > maxMessageSize)
{
return Results.BadRequest();
}
// Attempt to send the buffer to the background queue.
if (queue.Writer.TryWrite(buffer.AsMemory(0..read)))
{
return Results.Accepted();
}
// We couldn't accept the message since we're overloaded.
return Results.StatusCode(StatusCodes.Status429TooManyRequests);
});
app.Run();
将请求正文绑定到 Stream
或 PipeReader
时的限制:
- 读取数据时,
Stream
是与HttpRequest.Body
相同的对象。 - 默认情况下,不缓冲请求正文。 读取正文后,不支持后退。 无法多次读取流。
- 不能在最小操作处理程序之外使用
Stream
和PipeReader
,因为基础缓冲区将被释放或重用。
新的 Results.Stream 重载
我们引入了新的 Results.Stream
重载,以适应需要访问基础 HTTP 响应流而不进行缓冲的场景。 这些重载还改进了 API 将数据流式传输到 HTTP 响应流的情况,例如从 Azure Blob 存储。 以下示例使用 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);
}
最小 API 的类型化结果
在 .NET 6 中,引入了 IResult 接口以表示从最小 API 返回的值,这些 API 不利用对 JSON 将返回的对象序列化为 HTTP 响应的隐式支持。 静态 Results 类用于创建各种 IResult
对象,这些对象表示不同类型的响应。 例如,设置响应状态代码或重定向到另一个 URL。 但是,从这些方法返回的 IResult
实现框架类型是内部类型,因此难以验证从单元测试中的方法返回的特定 IResult
类型。
在 .NET 7 中,实现 IResult
的类型是公共类型,允许在测试时使用类型断言。 例如:
[TestClass()]
public class WeatherApiTests
{
[TestMethod()]
public void MapWeatherApiTest()
{
var result = WeatherApi.GetAllWeathers();
Assert.IsInstanceOfType(result, typeof(Ok<WeatherForecast[]>));
}
}
针对最小 API 的 OpenAPI 改进
Microsoft.AspNetCore.OpenApi
NuGet 包
Microsoft.AspNetCore.OpenApi
包允许与终结点的 OpenAPI 规范进行交互。 该包充当 Microsoft.AspNetCore.OpenApi
包中定义的 OpenAPI 模型和 Minimal API 中定义的终结点之间的链接。 该包提供一个 API,用于检查终结点的参数、响应和元数据,以构造用于描述终结点的 OpenAPI 注释类型。
app.MapPost("/todoitems/{id}", async (int id, Todo todo, TodoDb db) =>
{
todo.Id = id;
db.Todos.Add(todo);
await db.SaveChangesAsync();
return Results.Created($"/todoitems/{todo.Id}", todo);
})
.WithOpenApi();
使用参数调用 WithOpenApi
WithOpenApi
方法接受可用于修改 OpenAPI 注释的函数。 例如,在以下代码中,将说明添加到终结点的第一个参数:
app.MapPost("/todo2/{id}", async (int id, Todo todo, TodoDb db) =>
{
todo.Id = id;
db.Todos.Add(todo);
await db.SaveChangesAsync();
return Results.Created($"/todoitems/{todo.Id}", todo);
})
.WithOpenApi(generatedOperation =>
{
var parameter = generatedOperation.Parameters[0];
parameter.Description = "The ID associated with the created Todo";
return generatedOperation;
});
排除 OpenAPI 说明
在下面的示例中,/skipme
终结点从生成 OpenAPI 说明中排除:
using Microsoft.AspNetCore.OpenApi;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.MapGet("/swag", () => "Hello Swagger!")
.WithOpenApi();
app.MapGet("/skipme", () => "Skipping Swagger.")
.ExcludeFromDescription();
app.Run();
Signal R
SignalR 中心方法的依赖项注入
SignalR 中心方法现支持通过依赖项注入 (DI) 注入服务。
中心构造函数可以接受 DI 中的服务作为参数,这些参数可以存储在类的属性中,以便在中心方法中使用。
性能
HTTP/2 性能改进
.NET 7 引入了 Kestrel 处理 HTTP/2 请求的方式的重要重新架构。 使用繁忙的 HTTP/2 连接的 ASP.NET Core 应用将经历降低的 CPU 使用率和更高的吞吐量。
以前,HTTP/2 多路复用实现依赖于锁,它控制哪些请求可以写入基础 TCP 连接。 线程安全队列将替换写入锁。 现在,请求会排队,并由专用使用者来处理它们,而不是针对哪个线程使用写入锁而争论不已。 以前浪费的 CPU 资源可供应用的其他部分使用。
可以注意到这些改进的一个位置是 gRPC,这是使用 HTTP/2 的常用 RPC 框架。 Kestrel + gRPC 基准显示显著改进:
服务器
用于测量启动时间的新 ServerReady 事件
添加了 ServerReady
事件以度量 ASP.NET Core 应用的启动时间。
IIS
IIS 中的卷影复制
与通过部署应用脱机文件来停止应用相比,将应用程序集卷影复制到 IIS 的 ASP.NET Core 模块 (ANCM) 可以提供更好的最终用户体验。
杂项
dotnet watch
改进了 dotnet watch 的控制台输出
改进了 dotnet watch 的控制台输出,以便更好地与 ASP.NET Core 日志记录保持一致,并使用 😮emojis😍 脱颖而出。
新输出示例如下所示:
将 dotnet watch 配置为始终重启以进行强制编辑
强制编辑是无法热重载的编辑。 若要将 dotnet watch 配置为始终重启而不提示进行强制编辑,请将 DOTNET_WATCH_RESTART_ON_RUDE_EDIT
环境变量设置为 true
。
开发人员异常页深色模式
感谢 Patrick Westerhoff 的贡献,深色模式支持现已添加到开发人员异常页。 若要在浏览器中测试深色模式,请从开发人员工具页将模式设置为深色。 例如,在 Firefox 中:
在 Chrome 中: