ASP.NET Core 7.0 的新增功能

本文重点介绍 ASP.NET Core 7.0 中最重要的更改,并提供相关文档的链接。

ASP.NET Core 中的速率限制中间件

Microsoft.AspNetCore.RateLimiting 中间件提供速率限制中间件。 应用可配置速率限制策略,然后将策略附加到终结点。 有关详细信息,请参阅 ASP.NET Core 中的速率限制中间件

身份验证使用单个方案作为 DefaultScheme

作为简化身份验证工作的一部分,如果只注册了单个身份验证方案,该方案会自动用作 DefaultScheme,而无需指定。 有关详细信息,请参阅 DefaultScheme

MVC 和 Razor 页面

支持 MVC 视图和 Razor 页面中的可为空模型

支持可为空页面或视图模型以改善在 ASP.NET Core 应用中使用 null 状态检查时的体验:

@model Product?

在 MVC 和 API 控制器中与 IParsable<T>.TryParse 绑定

IParsable<TSelf>.TryParse API 支持绑定控制器操作参数值。 有关详细信息,请参阅IParsable<T>.TryParse 绑定

在低于 7 的 ASP.NET Core 版本中,cookie 同意验证使用 cookie 值 yes 来表示同意。 现在可以指定表示同意的值。 例如,可以使用 true 而不是 yes

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();
builder.Services.Configure<CookiePolicyOptions>(options =>
{
    options.CheckConsentNeeded = context => true;
    options.MinimumSameSitePolicy = SameSiteMode.None;
    options.ConsentCookieValue = "true";
});

var app = builder.Build();

有关详细信息,请参阅自定义 cookie 同意值

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

验证错误中的 JSON 属性名称

默认情况下,当发生验证错误时,模型验证会生成一个 ModelStateDictionary,其中属性名用作错误键。 某些应用(例如单页应用)受益于使用 JSON 属性名来处理 Web API 生成的验证错误。 以下代码将验证配置为使用 SystemTextJsonValidationMetadataProvider 以使用 JSON 属性名:

using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers(options =>
{
    options.ModelMetadataDetailsProviders.Add(new SystemTextJsonValidationMetadataProvider());
});

var app = builder.Build();

app.UseHttpsRedirection();

app.UseAuthorization();

app.MapControllers();

app.Run();

以下代码将验证配置为使用 NewtonsoftJsonValidationMetadataProvider 以在使用 Json.NET 时使用 JSON 属性名称:

using Microsoft.AspNetCore.Mvc.NewtonsoftJson;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers(options =>
{
    options.ModelMetadataDetailsProviders.Add(new NewtonsoftJsonValidationMetadataProvider());
}).AddNewtonsoftJson();

var app = builder.Build();

app.UseHttpsRedirection();

app.UseAuthorization();

app.MapControllers();

app.Run();

有关详细信息,请参阅在验证错误中使用 JSON 属性名称

最小 API

最小 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 时,支持将查询字符串或标头值绑定到复杂类型的数组。 有关详细信息,请参阅绑定标头和查询字符串中的数组和字符串值

有关详细信息,请参阅添加终结点摘要或说明

将请求正文绑定为 StreamPipeReader

请求正文可以绑定为 StreamPipeReader,以有效支持用户必须处理数据的情况,以及:

  • 将数据存储在 Blob 存储中,或将数据排入队列提供程序的队列。
  • 使用工作进程或云功能处理存储的数据。

例如,数据可能排队到 Azure 队列存储 或存储在 Azure Blob 存储中。

有关详细信息,请参阅将请求正文绑定为 StreamPipeReader

新的 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[]>));
    }      
}

改进了最小路由处理程序的单元可测试性

IResult 实现类型现在在 Microsoft.AspNetCore.Http.HttpResults 命名空间中公开提供。 使用命名方法而不是 Lambda 时,可以使用 IResult 实现类型对最小路由处理程序进行单元测试。

下面的代码使用 Ok<TValue> 类:

[Fact]
public async Task GetTodoReturnsTodoFromDatabase()
{
    // Arrange
    await using var context = new MockDb().CreateDbContext();

    context.Todos.Add(new Todo
    {
        Id = 1,
        Title = "Test title",
        Description = "Test description",
        IsDone = false
    });

    await context.SaveChangesAsync();

    // Act
    var result = await TodoEndpointsV1.GetTodo(1, context);

    //Assert
    Assert.IsType<Results<Ok<Todo>, NotFound>>(result);

    var okResult = (Ok<Todo>)result.Result;

    Assert.NotNull(okResult.Value);
    Assert.Equal(1, okResult.Value.Id);
}

有关详细信息,请参阅IResult 实现类型

新的 HttpResult 接口

Microsoft.AspNetCore.Http 命名空间中的以下接口提供了一种在运行时检测 IResult 类型的方法,这是筛选器实现中的常见模式:

有关详细信息,请参阅 IHttpResult 接口

针对最小 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;
});

提供终结点说明和摘要

最小 API 现在支持批注操作,其中包含 OpenAPI 规范生成的说明和摘要。 可以调用扩展方法 WithDescriptionWithSummary 或使用属性 [EndpointDescription][EndpointSummary]

有关详细信息,请参阅最小 API 应用中的 OpenAPI

使用 IFormFile 和 IFormFileCollection 上传文件

最小 API 现在支持使用 IFormFileIFormFileCollection 上传文件。 以下代码使用 IFormFileIFormFileCollection 上传文件:

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

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

app.MapPost("/upload", async (IFormFile file) =>
{
    var tempFile = Path.GetTempFileName();
    app.Logger.LogInformation(tempFile);
    using var stream = File.OpenWrite(tempFile);
    await file.CopyToAsync(stream);
});

app.MapPost("/upload_many", async (IFormFileCollection myFiles) =>
{
    foreach (var file in myFiles)
    {
        var tempFile = Path.GetTempFileName();
        app.Logger.LogInformation(tempFile);
        using var stream = File.OpenWrite(tempFile);
        await file.CopyToAsync(stream);
    }
});

app.Run();

使用授权标头客户端证书或 cookie 标头支持经过身份验证的文件上传请求。

没有针对防伪造的内置支持。 但是,这可以使用 IAntiforgery 服务来实现。

[AsParameters] 特性实现对自变量列表进行参数绑定

[AsParameters] 特性实现对自变量列表进行参数绑定。 有关详细信息,请参阅使用 [AsParameters] 对参数列表进行参数绑定

最小 API 和 API 控制器

新问题详细信息服务

问题详细信息服务实现 IProblemDetailsService 接口,该接口支持创建针对 HTTP API 的问题详细信息

有关详细信息,请参阅问题详细信息服务

路由组

MapGroup 扩展方法有助于组织具有共同前缀的终结点组。 它减少了重复代码,并允许通过对添加终结点元数据RequireAuthorizationWithMetadata 等方法的单一调用来自定义整个终结点组。

例如,以下代码创建两组相似的终结点:

app.MapGroup("/public/todos")
    .MapTodosApi()
    .WithTags("Public");

app.MapGroup("/private/todos")
    .MapTodosApi()
    .WithTags("Private")
    .AddEndpointFilterFactory(QueryPrivateTodos)
    .RequireAuthorization();


EndpointFilterDelegate QueryPrivateTodos(EndpointFilterFactoryContext factoryContext, EndpointFilterDelegate next)
{
    var dbContextIndex = -1;

    foreach (var argument in factoryContext.MethodInfo.GetParameters())
    {
        if (argument.ParameterType == typeof(TodoDb))
        {
            dbContextIndex = argument.Position;
            break;
        }
    }

    // Skip filter if the method doesn't have a TodoDb parameter.
    if (dbContextIndex < 0)
    {
        return next;
    }

    return async invocationContext =>
    {
        var dbContext = invocationContext.GetArgument<TodoDb>(dbContextIndex);
        dbContext.IsPrivate = true;

        try
        {
            return await next(invocationContext);
        }
        finally
        {
            // This should only be relevant if you're pooling or otherwise reusing the DbContext instance.
            dbContext.IsPrivate = false;
        }
    };
}
public static RouteGroupBuilder MapTodosApi(this RouteGroupBuilder group)
{
    group.MapGet("/", GetAllTodos);
    group.MapGet("/{id}", GetTodo);
    group.MapPost("/", CreateTodo);
    group.MapPut("/{id}", UpdateTodo);
    group.MapDelete("/{id}", DeleteTodo);

    return group;
}

在此方案中,可以适用 201 Created 结果中 Location 标头的相对地址:

public static async Task<Created<Todo>> CreateTodo(Todo todo, TodoDb database)
{
    await database.AddAsync(todo);
    await database.SaveChangesAsync();

    return TypedResults.Created($"{todo.Id}", todo);
}

第一组终结点将仅匹配前缀为 /public/todos 并且无需任何身份验证即可访问的请求。 第二组终结点将仅匹配前缀为 /private/todos 并且需要身份验证的请求。

QueryPrivateTodos 终结点筛选器工厂是一种本地函数,用于修改路由处理程序的 TodoDb 参数,以允许访问和存储专用的 todo 数据。

路由组还支持嵌套组和具有路由参数和约束的复杂前缀模式。 在以下示例中,映射到 user 组的路由处理程序可以捕获外部组前缀中定义的 {org}{group} 路由参数。

前缀也可以为空。 这对于在不更改路由模式的情况下向一组终结点添加终结点元数据或筛选器非常有用。

var all = app.MapGroup("").WithOpenApi();
var org = all.MapGroup("{org}");
var user = org.MapGroup("{user}");
user.MapGet("", (string org, string user) => $"{org}/{user}");

向组添加筛选器或元数据的行为与在添加可能已添加到内部组或特定终结点的任何额外筛选器或元数据之前将它们单独添加到每个终结点的行为相同。

var outer = app.MapGroup("/outer");
var inner = outer.MapGroup("/inner");

inner.AddEndpointFilter((context, next) =>
{
    app.Logger.LogInformation("/inner group filter");
    return next(context);
});

outer.AddEndpointFilter((context, next) =>
{
    app.Logger.LogInformation("/outer group filter");
    return next(context);
});

inner.MapGet("/", () => "Hi!").AddEndpointFilter((context, next) =>
{
    app.Logger.LogInformation("MapGet filter");
    return next(context);
});

在上面的示例中,外部筛选器将在内部筛选器之前记录传入请求,即使它是第二个添加的。 由于筛选器应用于不同的组,因此它们相对于彼此的添加顺序并不重要。 如果应用于相同的组或特定终结点,则筛选器的添加顺序非常重要。

/outer/inner/ 的请求将记录以下内容:

/outer group filter
/inner group filter
MapGet filter

gRPC

JSON 转码

gRPC JSON 转码是为 gRPC 服务创建 RESTful JSON API 的 ASP.NET Core 的扩展。 gRPC JSON 转码允许:

  • 应用使用熟悉的 HTTP 概念调用 gRPC 服务。
  • ASP.NET Core gRPC 应用支持 gRPC 和 RESTful JSON API,而无需复制功能。
  • 通过与 Swashbuckle 集成,为从已转码的 RESTful API 生成 OpenAPI 提供实验性支持。

有关详细信息,请参阅 ASP.NET Core gRPC 应用中的 gRPC JSON 转码将 OpenAPI 与 gRPC JSON 转码 ASP.NET Core 应用配合使用

ASP.NET Core 中的 gRPC 运行状况检查

gRPC 运行状况检查协议是用于报告 gRPC 服务器应用运行状况的一个标准。 应用将运行状况检查作为 gRPC 服务公开。 它们通常与外部监视服务一起用于检查应用的状态。

gRPC ASP.NET Core 包含 Grpc.AspNetCore.HealthChecks 包,因此添加了对 gRPC 运行状况检查的内置支持。 .NET 运行状况检查的结果会报告给调用方。

有关详细信息,请参阅 ASP.NET Core 中的 gRPC 运行状况检查

改进了调用凭据支持

改进后,建议使用调用凭据配置 gRPC 客户端以将身份验证令牌发送到服务器。 gRPC 客户端支持两个新功能,使调用凭据更易于使用:

  • 支持纯文本连接使用调用凭据。 以前,只有在使用 TLS 保护连接的情况下,gRPC 调用才会发送调用凭据。 GrpcChannelOptions 上的新设置 UnsafeUseInsecureChannelCallCredentials 允许自定义此行为。 如果不使用 TLS 来保护连接,就会存在安全问题。
  • gRPC 客户端工厂中提供了名为 AddCallCredentials 的新方法。 AddCallCredentials 是为 gRPC 客户端配置调用凭据的一种快速方法,可与依赖项注入 (DI) 很好地集成。

以下代码将 gRPC 客户端工厂配置为发送 Authorization 元数据:

builder.Services
    .AddGrpcClient<Greeter.GreeterClient>(o =>
    {
       o.Address = new Uri("https://localhost:5001");
    })
    .AddCallCredentials((context, metadata) =>
    {
       if (!string.IsNullOrEmpty(_token))
       {
          metadata.Add("Authorization", $"Bearer {_token}");
       }
       return Task.CompletedTask;
    });

有关详细信息,请参阅使用 gRPC 客户端工厂配置持有者令牌

SignalR

客户端结果

服务器现在支持从客户端请求结果。 这要求服务器使用 ISingleClientProxy.InvokeAsync,并且客户端从其 .On 处理程序返回结果。 强类型中心还可以从接口方法返回值。

有关详细信息,请参阅客户端结果

SignalR 中心方法的依赖项注入

SignalR 中心方法现支持通过依赖项注入 (DI) 注入服务。

中心构造函数可以接受 DI 中的服务作为参数,这些参数可以存储在类的属性中,以便在中心方法中使用。 有关详细信息,请参阅将服务注入中心

Blazor

处理位置更改事件和导航状态

在 .NET 7 中,Blazor 支持位置更改事件和维护导航状态。 这允许你在用户执行页面导航时警告用户未保存的工作或执行相关操作。

有关详细信息,请参阅“路由和导航”文章的以下部分:

空 Blazor 项目模板

Blazor 有两个新的项目模板,用于从空白盖板开始。 新的 Blazor Server 应用空项目模板和 Blazor WebAssembly 应用空项目模板与其非空项目模板类似,但没有示例代码。 这些空模板仅包含基本 home 页,并且我们删除了“启动”,以便你可以从其他 CSS 框架开始。

有关详细信息,请参阅以下文章:

Blazor 自定义元素

Microsoft.AspNetCore.Components.CustomElements 包支持使用 Blazor 构建基于标准的自定义 DOM 元素

有关详细信息,请参阅 ASP.NET Core Razor 组件

绑定修饰符(@bind:after@bind:get@bind:set

重要

@bind:after/@bind:get/@bind:set 功能目前会收到进一步更新。 若要利用最新更新,请确认已安装最新 SDK

不支持使用事件回调参数 ([Parameter] public EventCallback<string> ValueChanged { get; set; }), 而是将 Action-returning 或 Task-returning 方法传递给 @bind:set/@bind:after

有关详细信息,请参阅以下资源:

在 .NET 7 中,在使用新的 @bind:after 修饰符完成绑定事件后,可以运行异步逻辑。 在以下示例中,PerformSearch 异步方法会在检测到对搜索文本所做的任何更改后自动运行:

<input @bind="searchText" @bind:after="PerformSearch" />

@code {
    private string searchText;

    private async Task PerformSearch()
    {
        ...
    }
}

在 .NET 7 中,为组件参数设置绑定也更容易。 组件可以通过定义一对参数来支持双向数据绑定:

  • @bind:get:指定要绑定的值。
  • @bind:set:指定值更改时的回调。

@bind:get@bind:set 修饰符始终一起使用。

示例:

@* Elements *@

<input type="text" @bind="text" @bind:after="() => { }" />

<input type="text" @bind:get="text" @bind:set="(value) => { }" />

<input type="text" @bind="text" @bind:after="AfterAsync" />

<input type="text" @bind:get="text" @bind:set="SetAsync" />

<input type="text" @bind="text" @bind:after="() => { }" />

<input type="text" @bind:get="text" @bind:set="(value) => { }" />

<input type="text" @bind="text" @bind:after="AfterAsync" />

<input type="text" @bind:get="text" @bind:set="SetAsync" />

@* Components *@

<InputText @bind-Value="text" @bind-Value:after="() => { }" />

<InputText @bind-Value:get="text" @bind-Value:set="(value) => { }" />

<InputText @bind-Value="text" @bind-Value:after="AfterAsync" />

<InputText @bind-Value:get="text" @bind-Value:set="SetAsync" />

<InputText @bind-Value="text" @bind-Value:after="() => { }" />

<InputText @bind-Value:get="text" @bind-Value:set="(value) => { }" />

<InputText @bind-Value="text" @bind-Value:after="AfterAsync" />

<InputText @bind-Value:get="text" @bind-Value:set="SetAsync" />

@code {
    private string text = "";

    private void After(){}
    private void Set() {}
    private Task AfterAsync() { return Task.CompletedTask; }
    private Task SetAsync(string value) { return Task.CompletedTask; }
}

有关 InputText 组件的详细信息,请参阅《ASP.NET Core Blazor 输入组件》。

热重载改进

在 .NET 7 中,热重载支持包括以下内容:

  • 当删除值时,组件将其参数重置为其默认值。
  • Blazor WebAssembly:
    • 添加新类型。
    • 添加嵌套类。
    • 向现有类型添加静态和实例方法。
    • 向现有类型添加静态字段和方法。
    • 向现有方法添加静态 Lambda。
    • 向先前已捕获 this 的现有方法添加捕获 this 的 Lambda。

Blazor WebAssembly 中具有 MSAL 的动态身份验证请求

Blazor WebAssembly 是 .NET 7 中的新增功能,支持使用自定义参数在运行时创建动态身份验证请求来处理高级身份验证方案。

有关详细信息,请参阅以下文章:

Blazor WebAssembly 调试改进

Blazor WebAssembly 调试具有以下改进:

  • 支持“仅我的代码”设置,以显示或隐藏不属于用户代码的类型成员。
  • 支持检查多维数组。
  • “调用堆栈”现在显示异步方法的正确名称。
  • 改进了表达式计算。
  • 正确处理派生成员上的 new 关键字。
  • 支持 System.Diagnostics 中与调试器相关的属性。

WebAssembly 上的 System.Security.Cryptography 支持

.NET 6 在 WebAssembly 上运行时支持 SHA 系列哈希算法。 .NET 7 通过尽可能利用 SubtleCrypto,并在 SubtleCrypto 无法使用时回退到 .NET 实现,启用更多加密算法。 .NET 7 中的 WebAssembly 支持以下算法:

  • SHA1
  • SHA256
  • SHA384
  • SHA512
  • HMACSHA1
  • HMACSHA256
  • HMACSHA384
  • HMACSHA512
  • AES-CBC
  • PBKDF2
  • HKDF

有关详细信息,请参阅面向 browser-wasm 的开发人员可以使用 Web Crypto API (dotnet/runtime #40074)

将服务注入到自定义验证属性中

现在可以将服务注入到自定义验证属性中。 Blazor 设置 ValidationContext,使其可用作服务提供程序。

有关详细信息,请参阅 ASP.NET Core Blazor 表单验证

EditContext/EditForm 外部的 Input* 组件

现在,内置输入组件在 Razor 组件标记中的窗体外部受支持。

有关详细信息,请参阅 ASP.NET Core Blazor 输入组件

项目模板更改

去年发布 .NET 6 时,_Host 页面 (Pages/_Host.chstml) 的 HTML 标记在 .NET 6 Blazor Server 项目模板中的 _Host 页面和新的 _Layout 页面 (Pages/_Layout.chstml) 之间拆分。

在 .NET 7 中,HTML 标记已与项目模板中的 _Host 页面重新组合。

对 Blazor 项目模板进行了多项其他更改。 无法在文档中列出模板的每个更改。 若要将应用迁移到 .NET 7 以采用所有更改,请参阅从 ASP.NET Core 6.0 迁移到 7.0

实验 QuickGrid 组件

新的 QuickGrid 组件为最常见的要求提供方便的数据网格组件,对于任何构建 Blazor 数据网格组件的人来说,该组件作为参考体系结构和性能基线。

有关详细信息,请参阅 ASP.NET Core Blazor QuickGrid 组件

实时演示:Blazor 示例应用的 QuickGrid

虚拟化增强功能

.NET 7 中的虚拟化增强功能:

  • Virtualize 组件支持将文档本身用作滚动根,以替代另一种方法:使用应用了 overflow-y: scroll 的某些其他元素。
  • 如果将 Virtualize 组件放在需要特定子标记名称的元素中,则 SpacerElement 可用于获取或设置虚拟化间隔标记名称。

有关详细信息,请参阅“虚拟化”文章的以下部分:

MouseEventArgs 更新

MovementXMovementY 已添加到 MouseEventArgs

有关详细信息,请参阅 ASP.NET Core Blazor 事件处理

新的 Blazor 加载页面

Blazor WebAssembly 项目模板具有一个新的加载 UI,用于显示加载应用的进度。

有关详细信息,请参阅 ASP.NET Core Blazor 启动

改进了在 Blazor WebAssembly 中进行身份验证的诊断

为了帮助诊断 Blazor WebAssembly 应用中的身份验证问题,可以使用详细的日志记录。

有关详细信息,请参阅 ASP.NET Core Blazor 日志记录

WebAssembly 上的 JavaScript 互操作

JavaScript [JSImport]/[JSExport] 互操作 API 是一种新的低级机制,用于在 Blazor WebAssembly 和基于 JavaScript 的应用中使用 .NET。 借助这一新的 JavaScript 互操作功能,可以使用 .NET WebAssembly 运行时从 JavaScript 调用 .NET 代码,并从 .NET 调用 JavaScript 功能,而无需依赖 Blazor UI 组件模型。

更多相关信息:

身份验证状态提供程序的条件注册

在 .NET 7 发布之前,AuthenticationStateProvider 是使用 AddScoped 在服务容器中注册的。 这使得调试应用变得困难,因为它在提供自定义实现时强制执行特定的服务注册顺序。 由于内部框架随时间推移而发生更改,因此不再需要使用 AddScoped 注册 AuthenticationStateProvider

在开发人员代码中,对身份验证状态提供程序服务注册进行以下更改:

- builder.Services.AddScoped<AuthenticationStateProvider, ExternalAuthStateProvider>();
+ builder.Services.TryAddScoped<AuthenticationStateProvider, ExternalAuthStateProvider>();

在前面的示例中,ExternalAuthStateProvider 是开发人员的服务实现。

对 .NET WebAssembly 生成工具的改进

适用于 .NET 7 的 wasm-tools 工作负载中的新增功能,可帮助提高性能和处理异常:

有关详细信息,请参阅 ASP.NET Core Blazor WebAssembly 生成工具和预先 (AOT) 编译

Blazor Hybrid

外部 URL

添加了允许在浏览器中打开外部网页的选项。

有关详细信息,请参阅 ASP.NET Core Blazor Hybrid 路由和导航

安全性

新的指导可用于 Blazor Hybrid 安全方案。 有关详细信息,请参阅以下文章:

性能

输出缓存中间件

输出缓存是一个新的中间件,它存储来自 Web 应用的响应,并从缓存中提供这些响应,而不是每次都计算它们。 输出缓存与响应缓存的不同之处包括以下方面:

  • 缓存行为可在服务器上配置。
  • 缓存条目可以通过编程方式失效。
  • 资源锁定可降低缓存踩踏惊群的风险。
  • 缓存重新验证意味着服务器可以返回 304 Not Modified HTTP 状态代码,而不是缓存的响应正文。
  • 缓存存储介质是可扩展的。

有关详细信息,请参阅缓存概述输出缓存中间件

HTTP/3 改进

新增功能:

  • 使 HTTP/3 完全受 ASP.NET Core 支持,它不再是试验性的。
  • 改进了 Kestrel 对 HTTP/3 的支持。 改进的两个主要方面是与 HTTP/1.1 和 HTTP/2 的功能奇偶一致性,以及性能。
  • 通过 HTTP/3 提供对 UseHttps(ListenOptions, X509Certificate2) 的完全支持。 Kestrel 提供了用于配置连接证书的高级选项,例如连接到服务器名称指示 (SNI)
  • HTTP.sysIIS 上添加了对 HTTP/3 的支持。

以下示例演示了如何使用 SNI 回调解析 TLS 选项:

using Microsoft.AspNetCore.Server.Kestrel.Core;
using Microsoft.AspNetCore.Server.Kestrel.Https;
using System.Net.Security;
using System.Security.Cryptography.X509Certificates;

var builder = WebApplication.CreateBuilder(args);
builder.WebHost.ConfigureKestrel(options =>
{
    options.ListenAnyIP(8080, listenOptions =>
    {
        listenOptions.Protocols = HttpProtocols.Http1AndHttp2AndHttp3;
        listenOptions.UseHttps(new TlsHandshakeCallbackOptions
        {
            OnConnection = context =>
            {
                var options = new SslServerAuthenticationOptions
                {
                    ServerCertificate = 
                         MyResolveCertForHost(context.ClientHelloInfo.ServerName)
                };
                return new ValueTask<SslServerAuthenticationOptions>(options);
            },
        });
    });
});

在 .NET 7 中为减少 HTTP/3 分配做了大量的工作。 可以在以下 GitHub PR 中看到其中一些改进:

HTTP/2 性能改进

.NET 7 引入了 Kestrel 处理 HTTP/2 请求的方式的重要重新架构。 使用繁忙的 HTTP/2 连接的 ASP.NET Core 应用将经历降低的 CPU 使用率和更高的吞吐量。

以前,HTTP/2 多路复用实现依赖于,它控制哪些请求可以写入基础 TCP 连接。 线程安全队列将替换写入锁。 现在,请求会排队,并由专用使用者来处理它们,而不是针对哪个线程使用写入锁而争论不已。 以前浪费的 CPU 资源可供应用的 rest 使用。

可以注意到这些改进的一个位置是 gRPC,这是使用 HTTP/2 的常用 RPC 框架。 Kestrel + gRPC 基准显示显著改进:

gRPC 服务器流式处理性能

HTTP/2 帧编写代码进行了更改,当多个流尝试在单个 HTTP/2 连接上写入数据时,该代码可提高性能。 我们现在将 TLS 工作调度到线程池,并更快地释放其他流可以获取的写锁定来写入其数据。 如果此写锁定存在争用,则缩短等待时间可能会显著提高性能。 单个连接上具有 70 个流的 gRPC 基准显示,进行此更改后,每秒请求数 (RPS) 提高了约 15%。

Http/2 WebSockets 支持

.NET 7 为 Kestrel、SignalR JavaScript 客户端和带有 Blazor WebAssembly 的 SignalR 引入了基于 HTTP/2 的 Websocket 支持。

使用基于 HTTP/2 的 WebSockets 可利用以下新功能:

  • 标头压缩。
  • 多路复用,可减少向服务器发出多个请求时所需的时间和资源。

所有支持 HTTP/2 的平台上的 Kestrel 中都提供了这些受支持的功能。 版本协商在浏览器和 Kestrel 中是自动的,因此不需要新的 API。

有关详细信息,请参阅 Http/2 WebSocket 支持

Kestrel 在高核心计算机上的性能改进

Kestrel 将 ConcurrentQueue<T> 用于多种用途。 其中一个用途是在 Kestrel 的默认套接字传输中计划 I/O 操作。 根据关联的套接字对 ConcurrentQueue 进行分区可在具有许多 CPU 内核的计算机上减少争用并提高吞吐量。

.NET 6 上对高核心计算机的分析显示,在 Kestrel 的其他 ConcurrentQueue 实例之一(即 Kestrel 用于缓存字节缓冲区的 PinnedMemoryPool)中存在严重争用。

在 .NET 7 中,Kestrel 的内存池与其 I/O 队列以相同方式进行分区,这在高核心计算机上显著减少了争用并显著提高了吞吐量。 在 80 核 ARM64 VM 上的 TechEmpower 明文基准测试中,每秒响应 (RPS) 提高了 500% 以上。 在 48 核 AMD VM 上,HTTPS JSON 基准测试中的提升接近 100%。

用于度量启动时间的 ServerReady 事件

使用 EventSource 的应用可以度量启动时间,以了解和优化启动性能。 Microsoft.AspNetCore.Hosting 中的新 ServerReady 事件表示服务器已准备好响应请求的点。

服务器

用于测量启动时间的新 ServerReady 事件

添加了 ServerReady 事件以度量 ASP.NET Core 应用的启动时间

IIS

IIS 中的卷影复制

与通过部署应用脱机文件来停止应用相比,将应用程序集卷影复制到 IIS 的 ASP.NET Core 模块 (ANCM) 可以提供更好的最终用户体验。

有关详细信息,请参阅 IIS 中的卷影复制

杂项

Kestrel 完整证书链改进

HttpsConnectionAdapterOptions 有一个 X509Certificate2Collection 类型的新 ServerCertificateChain 属性,通过允许指定包含中间证书的完整链,使验证证书链变得更加容易。 有关更多详细信息,请参阅 dotnet/aspnetcore#21513

dotnet watch

改进了 dotnet watch 的控制台输出

改进了 dotnet watch 的控制台输出,以便更好地与 ASP.NET Core 日志记录保持一致,并使用 😮emojis😍 脱颖而出。

新输出示例如下所示:

dotnet watch 输出

有关详细信息,请参阅此 GitHub 拉取请求

将 dotnet watch 配置为始终重启以进行强制编辑

强制编辑是无法热重载的编辑。 若要将 dotnet watch 配置为始终重启而不提示进行强制编辑,请将 DOTNET_WATCH_RESTART_ON_RUDE_EDIT 环境变量设置为 true

开发人员异常页深色模式

感谢 Patrick Westerhoff 的贡献,深色模式支持现已添加到开发人员异常页。 若要在浏览器中测试深色模式,请从开发人员工具页将模式设置为深色。 例如,在 Firefox 中:

F12 工具 FF 深色模式

在 Chrome 中:

F12 工具 Chrome 深色模式

使用 Program.Main 方法而不是顶级语句的项目模板选项

.NET 7 模板包括一个选项,可以不使用顶级语句并生成在 Program 类上声明的 namespaceMain 方法。

使用 .NET CLI 时,使用 --use-program-main 选项:

dotnet new web --use-program-main

使用 Visual Studio,在项目创建期间选中新的“请勿使用顶级语句”复选框:

复选框

更新了 Angular 和 React 模板

Angular 项目模板已更新至 Angular 14。 React 项目模板已更新至 React 18.2。

使用 dotnet user-jwts 管理开发中的 JSON Web 令牌

新的 dotnet user-jwts 命令行工具可以创建和管理应用特定的本地 JSON Web 令牌 (JWT)。 有关详细信息,请参阅使用 dotnet user-jwts 管理开发中的 JSON Web 令牌

支持 W3CLogger 中的其他请求头

现在,可以指定通过在 W3CLoggerOptions 上调用 AdditionalRequestHeaders() 来使用 W3C 记录器时要记录的其他请求头:

services.AddW3CLogging(logging =>
{
    logging.AdditionalRequestHeaders.Add("x-forwarded-for");
    logging.AdditionalRequestHeaders.Add("x-client-ssl-protocol");
});

有关详细信息,请参阅 W3CLogger 选项

请求解压缩

新的请求解压缩中间件

  • 允许 API 终结点接受包含压缩内容的请求。
  • 使用 Content-Encoding HTTP 标头自动识别和解压缩包含压缩内容的请求。
  • 无需编写代码来处理压缩的请求。

有关详细信息,请参阅请求解压缩中间件