首页>教程>ASP.NET Core7.0教程>ASP.NET Core 7.0 预览版的新增功能

ASP.NET Core 7.0 预览版的新增功能

内容纲要

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 控制器操作参数绑定源的新机制使用以下规则:

  1. 以前指定的 BindingInfo.BindingSource 永远不会被覆盖。
  2. 在 DI 容器中注册的复杂类型参数被分配 BindingSource.Services
  3. 在 DI 容器中未注册的复杂类型参数被分配 BindingSource.Body
  4. 具有在任何路由模板中显示为路由值的名称的参数被分配 BindingSource.Path
  5. 所有其他参数都是 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 中:

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
今日签到
有新私信 私信列表
搜索