ASP.NET Core 9.0 的新增功能

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

本文已针对 .NET 9 预览版 4 进行了更新。

Blazor

本部分介绍 Blazor 的新功能。

将静态服务器端渲染 (SSR) 页面添加到全局交互式 Blazor Web 应用程序

随着 .NET 9 的发布,现在可以更简单地将静态 SSR 页面添加到采用全局交互性的应用程序中。

仅当应用程序具有无法使用交互式服务器或 WebAssembly 渲染的特定页面时,此方法才有用。 例如,对于依赖于读/写 HTTP cookie 并且只能在请求/响应周期中运行而无法通过交互式渲染加载的页面,可采用这种方法。 对于使用交互式渲染的页面,不应强制它们使用静态 SSR 渲染,因为这一方法的效率较低且对最终用户的响应较差。

通过 @attributeRazor 指令,用新属性 [ExcludeFromInteractiveRouting] 标记任意 Razor 组件页面:

@attribute [ExcludeFromInteractiveRouting]

应用该属性将停止交互式路由并导航到不同的页面。 入站导航会强制执行全页重载,而不是通过交互式路由解析页面。 全页重载会迫使顶级根组件从服务器重新渲染,通常情况下,这一组件是 App 组件 (App.razor),重新渲染可以让应用切换到其他顶级渲染模式。

HttpContext.AcceptsInteractiveRouting 扩展方法允许组件检测是否已为当前页面应用 [ExcludeFromInteractiveRouting]

App 组件中,使用以下示例中的模式:

  • 默认情况下,缺少 [ExcludeFromInteractiveRouting] 注释的页面会默认使用配备通用交互功能的 InteractiveServer 渲染模式。 可以将 InteractiveServer 替换为 InteractiveWebAssemblyInteractiveAuto 以指定不同的默认全局渲染模式。
  • 使用 [ExcludeFromInteractiveRouting] 注释的页面采用静态 SSR(PageRenderMode 指定为 null)。
<!DOCTYPE html>
<html>
<head>
    ...
    <HeadOutlet @rendermode="@PageRenderMode" />
</head>
<body>
    <Routes @rendermode="@PageRenderMode" />
    ...
</body>
</html>

@code {
    [CascadingParameter]
    private HttpContext HttpContext { get; set; } = default!;

    private IComponentRenderMode? PageRenderMode
        => HttpContext.AcceptsInteractiveRouting() ? InteractiveServer : null;
}

使用 HttpContext.AcceptsInteractiveRouting 扩展方法的替代方法是使用 HttpContext.GetEndpoint()?.Metadata 手动读取终结点元数据。

有关此功能的详细信息,请参阅 ASP.NET CoreBlazor 渲染模式中的参考资料。

构造函数注入

Razor 组件支持构造函数注入。

在以下示例中,部分(代码隐藏)类使用主构造函数注入了 NavigationManager 服务:

public partial class ConstructorInjection(NavigationManager navigation)
{
    protected NavigationManager Navigation { get; } = navigation;
}

有关详细信息,请参阅 ASP.NET Core Blazor 依赖项注入

交互式服务器组件的 Websocket 压缩

默认情况下,交互式服务器组件会为 WebSocket 连接 启用压缩,并将frame-ancestors内容安全策略 (CSP) 指令设置为 'self',这会仅允许在启用压缩时或提供 WebSocket 上下文的配置时将应用嵌入为该应用提供服务的来源的 <iframe> 中。

可以通过将 ConfigureWebSocketOptions 设为 null 来禁用压缩,这可以减少应用受攻击的风险,但可能会导致性能降低:

.AddInteractiveServerRenderMode(o => o.ConfigureWebSocketOptions = null)

请考虑使用带有值 'none'(需要单引号)的更严格的 frame-ancestors CSP,它允许 WebSocket 压缩,但会阻止浏览器将应用嵌入任何 <iframe>

.AddInteractiveServerRenderMode(o => o.ContentSecurityFrameAncestorsPolicy = "'none'")

有关更多信息,请参见以下资源:

处理 Blazor 中的键盘组合事件

新的 KeyboardEventArgs.IsComposing 属性指示键盘事件是否是组合会话的一部分。 跟踪键盘事件的组合状态对于处理国际字符输入法至关重要。

QuickGrid 添加了 OverscanCount 参数

QuickGrid 组件现在公开了一个 OverscanCount 属性,该属性用于指定启用虚拟化时在可见区域之前和之后渲染的附加行数。

默认 OverscanCount 为 3。 以下示例将 OverscanCount 提升到 4:

<QuickGrid ItemsProvider="itemsProvider" Virtualize="true" OverscanCount="4">
    ...
</QuickGrid>

SignalR

本部分介绍 SignalR 的新功能。

SignalR 中心支持多态类型

中心方法现在可接受基类(而不是派生类)来实现多态方案。 需要注释基类型才能实现多形性

public class MyHub : Hub
{
    public void Method(JsonPerson person)
    {
        if (person is JsonPersonExtended)
        {
        }
        else if (person is JsonPersonExtended2)
        {
        }
        else
        {
        }
    }
}

[JsonPolymorphic]
[JsonDerivedType(typeof(JsonPersonExtended), nameof(JsonPersonExtended))]
[JsonDerivedType(typeof(JsonPersonExtended2), nameof(JsonPersonExtended2))]
private class JsonPerson
{
    public string Name { get; set; }
    public Person Child { get; set; }
    public Person Parent { get; set; }
}

private class JsonPersonExtended : JsonPerson
{
    public int Age { get; set; }
}

private class JsonPersonExtended2 : JsonPerson
{
    public string Location { get; set; }
}

最小 API

本部分介绍最小 API 的新功能。

TypedResults 添加了 InternalServerErrorInternalServerError<TValue>

TypedResults 类是一种有用的工具,用于从最小的 API 返回基于强类型的 HTTP 状态代码响应。 TypedResults 现在包括用于从终结点返回“500 内部服务器错误”响应的工厂方法和类型。 下面是返回 500 响应的示例:

var app = WebApplication.Create();

app.MapGet("/", () => TypedResults.InternalServerError("Something went wrong!"));

app.Run();

OpenAPI

针对 OpenAPI 文档生成的内置支持

OpenAPI 规范是描述 HTTP API 的标准。 开发人员可通过此标准定义可插入至客户端生成器、服务器生成器、测试工具、文档等项目的 API 的形式。 在 .NET 9 Preview 中,ASP.NET Core 提供了内置支持,用于使用 Microsoft.AspNetCore.OpenApi 包生成表示基于控制器或最小 API 的 OpenAPI 文档。

以下突出显示的代码调用了下面两项内容:

  • AddOpenApi,用于将所需的依赖项注册到应用程序的 DI 容器中。
  • MapOpenApi,用于在应用程序的路由中注册所需的 OpenAPI 端点。
var builder = WebApplication.CreateBuilder();

builder.Services.AddOpenApi();

var app = builder.Build();

app.MapOpenApi();

app.MapGet("/hello/{name}", (string name) => $"Hello {name}"!);

app.Run();

使用以下命令在项目中安装 Microsoft.AspNetCore.OpenApi 包:

dotnet add package Microsoft.AspNetCore.OpenApi --prerelease

运行应用程序并导航到 openapi/v1.json 以查看生成的 OpenAPI 文档:

OpenAPI 文档

还可以通过添加 Microsoft.Extensions.ApiDescription.Server 包,在构建时生成 OpenAPI 文档:

dotnet add package Microsoft.Extensions.ApiDescription.Server --prerelease

在应用的项目文件中,添加以下内容:

<PropertyGroup>
  <OpenApiDocumentsDirectory>$(MSBuildProjectDirectory)</OpenApiDocumentsDirectory>
  <OpenApiGenerateDocuments>true</OpenApiGenerateDocuments>
</PropertyGroup>

运行 dotnet build 并检查项目目录中生成的 JSON 文件。

在构建时生成 OpenAPI 文档

ASP.NET Core 的内置 OpenAPI 文档生成为各种自定义和选项提供支持。 它提供文档和操作转换器,并且能够管理同一个应用程序的多个 OpenAPI 文档。

要了解有关 ASP.NET Core 的新 OpenAPI 文档功能的更多信息,请参阅新的 Microsoft.AspNetCore.OpenApi 文档

身份验证和授权

本部分介绍身份验证和授权的新功能。

OIDC 和 OAuth 参数自定义

OAuth 和 OIDC 身份验证处理程序现在具有 AdditionalAuthorizationParameters 选项,以便更轻松地自定义通常作为重定向查询字符串的一部分的授权消息参数。 在 .NET 8 及更早版本中,这需要在自定义处理程序中使用自定义 OnRedirectToIdentityProvider 回调或重写 BuildChallengeUrl 方法。 下面是 .NET 8 代码的示例:

builder.Services.AddAuthentication().AddOpenIdConnect(options =>
{
    options.Events.OnRedirectToIdentityProvider = context =>
    {
        context.ProtocolMessage.SetParameter("prompt", "login");
        context.ProtocolMessage.SetParameter("audience", "https://api.example.com");
        return Task.CompletedTask;
    };
});

前面的示例现在可以简化为以下代码:

builder.Services.AddAuthentication().AddOpenIdConnect(options =>
{
    options.AdditionalAuthorizationParameters.Add("prompt", "login");
    options.AdditionalAuthorizationParameters.Add("audience", "https://api.example.com");
});

配置 HTTP.sys 扩展身份验证标志

现在,可以使用 HTTP.sys AuthenticationManager 上的新 EnableKerberosCredentialCachingCaptureCredentials 属性来配置 HTTP_AUTH_EX_FLAG_ENABLE_KERBEROS_CREDENTIAL_CACHINGHTTP_AUTH_EX_FLAG_CAPTURE_CREDENTIAL HTTP.sys 标志,以优化 Windows 身份验证的处理方式。 例如:

webBuilder.UseHttpSys(options =>
{
    options.Authentication.Schemes = AuthenticationSchemes.Negotiate;
    options.Authentication.EnableKerberosCredentialCaching = true;
    options.Authentication.CaptureCredentials = true;
});

杂项

以下部分介绍了其他新功能。

HybridCache

HybridCache API 弥补了现有 IDistributedCacheIMemoryCache API 中的一些差距。 它还添加了新功能,例如:

  • “踩踏”保护可防止系统对同一作业执行多个并行获取操作。
  • 可配置的序列化。

HybridCache 旨在作为现有 IDistributedCacheIMemoryCache 的即插即用替代品,并且提供了一个简单的 API 以便添加新的缓存代码。 它为进程内和进程外缓存提供统一的 API。

要了解 HybridCache API 的简化方式,请将其与使用 IDistributedCache 的代码进行比较。 下面是使用 IDistributedCache 方法的示例:

public class SomeService(IDistributedCache cache)
{
    public async Task<SomeInformation> GetSomeInformationAsync
        (string name, int id, CancellationToken token = default)
    {
        var key = $"someinfo:{name}:{id}"; // Unique key for this combination.
        var bytes = await cache.GetAsync(key, token); // Try to get from cache.
        SomeInformation info;
        if (bytes is null)
        {
            // Cache miss; get the data from the real source.
            info = await SomeExpensiveOperationAsync(name, id, token);

            // Serialize and cache it.
            bytes = SomeSerializer.Serialize(info);
            await cache.SetAsync(key, bytes, token);
        }
        else
        {
            // Cache hit; deserialize it.
            info = SomeSerializer.Deserialize<SomeInformation>(bytes);
        }
        return info;
    }

    // This is the work we're trying to cache.
    private async Task<SomeInformation> SomeExpensiveOperationAsync(string name, int id,
        CancellationToken token = default)
    { /* ... */ }
}

每次都要处理很多工作,包括序列化之类的操作。 在缓存缺失的情况下,最终可能会有多个并发线程,对于这些线程而言,其均会经历如下过程:出现缓存缺失,获取基础数据,对数据执行序列化,然后将数据发送至缓存。

为了使用 HybridCache 简化和改进此代码,我们首先需要添加新库 Microsoft.Extensions.Caching.Hybrid

<PackageReference Include="Microsoft.Extensions.Caching.Hybrid" Version="9.0.0" />

注册 HybridCache 服务,操作与注册 IDistributedCache 实现一样:

services.AddHybridCache(); // Not shown: optional configuration API.

现在大多数缓存问题都可以转移到 HybridCache

public class SomeService(HybridCache cache)
{
    public async Task<SomeInformation> GetSomeInformationAsync
        (string name, int id, CancellationToken token = default)
    {
        return await cache.GetOrCreateAsync(
            $"someinfo:{name}:{id}", // Unique key for this combination.
            async cancel => await SomeExpensiveOperationAsync(name, id, cancel),
            token: token
        );
    }
}

我们通过依赖注入提供了 HybridCache 抽象类的具体实现,但其目的是让开发人员可以提供 API 的自定义实现。 HybridCache 实现处理与缓存相关的所有内容,包括并发操作处理。 这里的 cancel 标记代表所有并发调用方的联合取消——而不仅仅是我们所见的指定调用方的取消(即 token)。

可以使用 TState 模式进一步优化高吞吐量方案,以避免捕获的变量和每个实例回调产生一些开销:

public class SomeService(HybridCache cache)
{
    public async Task<SomeInformation> GetSomeInformationAsync(string name, int id, CancellationToken token = default)
    {
        return await cache.GetOrCreateAsync(
            $"someinfo:{name}:{id}", // unique key for this combination
            (name, id), // all of the state we need for the final call, if needed
            static async (state, token) =>
                await SomeExpensiveOperationAsync(state.name, state.id, token),
            token: token
        );
    }
}

HybridCache 使用已配置的 IDistributedCache 实现(如有)进行辅助进程外缓存,例如使用 Redis。 但即使没有 IDistributedCacheHybridCache 服务仍将提供进程内缓存和“踩踏”保护。

对象重用说明

在使用 IDistributedCache 的典型现有代码中,每次从缓存中检索对象都会导致反序列化。 此行为意味着每个并发调用方都获取一个单独的对象实例,该实例无法与其他实例交互。 这实现了线程安全性,因为并发修改同一对象实例没有风险。

将根据现有 IDistributedCache 代码调整对 HybridCache 的大量使用,因此 HybridCache 在默认情况下会保留此行为,以避免引入并发 bug。 但是,给定用例本质上在以下前提下是线程安全的:

  • 要缓存的类型不可变。
  • 代码未修改它们。

在这种情况下,请通过以下方式通知 HybridCache 可以安全地重用实例:

  • 将类型标记为 sealed。 C# 中的 sealed 关键字表示类无法被继承。
  • 对其应用 [ImmutableObject(true)] 属性。 [ImmutableObject(true)] 属性指示创建对象后无法更改该对象的状态。

通过重用实例,HybridCache 可以减少与每次调用反序列化相关的 CPU 和对象分配开销。 在缓存对象较大或被经常访问的情况下,这会提高性能。

其他 HybridCache 功能

IDistributedCache 类似,HybridCache 支持通过 RemoveKeyAsync 方法使用特定键完成删除。

HybridCache 还为 IDistributedCache 实现提供可选 API,以避免 byte[] 分配。 此功能由 Microsoft.Extensions.Caching.StackExchangeRedisMicrosoft.Extensions.Caching.SqlServer 包的预览版本实现。

会在注册服务过程中配置序列化,支持通过 WithSerializer.WithSerializerFactory 方法进行特定于类型的通用序列化程序(通过 AddHybridCache 调用链接)。 默认情况下,库在内部处理 stringbyte[],并对其他所有内容使用 System.Text.Json,但是你可以使用 protobuf、xml 或其他任何内容。

HybridCache 支持较旧的 .NET 运行时,最低支持 .NET Framework 4.7.2 和 .NET Standard 2.0。

有关 HybridCache 的详细信息,请参阅 ASP.NET Core 中的 HybridCache 库

开发人员异常页改进

当应用程序在开发过程中引发未处理的异常时,系统将显示 ASP.NET Core 开发人员异常页面。 开发人员异常页面提供有关异常和请求的详细信息。

预览版 3 将终结点元数据添加到开发人员异常页面。 ASP.NET Core 使用终结点元数据来控制端点行为,例如路由、响应缓存、速率限制、OpenAPI 生成等。 下图显示了开发人员异常页面的 Routing 部分中的新元数据信息:

开发者异常页上的新元数据信息

在测试开发人员异常页时,发现了少量的日常实用体验提升。 这些提升均由预览版 4 实现:

  • 更合理的文本换行。 长 cookie、查询字符串值和方法名称不再添加水平浏览器滚动条。
  • 采用现代设计,并应用了更大的文本。
  • 表格大小更加一致。

以下动画图像显示了新的开发人员异常页面:

新的开发人员异常页面

字典调试改进

字典和其他键值集合的调试显示具有改进的布局。 键显示在调试程序的键列中,而不是与值连接在一起。 下图显示了调试程序中字典的旧显示和新显示。

之前:

以前的调试器体验

之后:

新的调试器体验

ASP.NET Core 有许多键值集合。 这种改进的调试体验适用于:

  • HTTP 头
  • 查询字符串
  • 窗体
  • Cookies
  • 查看数据
  • 路由数据
  • 功能

修复 IIS 中应用程序回收期间的 503 错误

默认情况下,IIS 收到回收或关闭通知与 ANCM 通知托管服务器启动关闭之间现在有 1 秒的延迟。 延迟可通过 ANCM_shutdownDelay 环境变量或通过设置 shutdownDelay 处理程序设置进行配置。 这两个值均以毫秒为单位。 延迟主要是为了降低争用的可能性,其中:

  • IIS 尚未开始对发送至新应用的请求进行排队。
  • ANCM 开始拒绝进入旧应用的新请求。

速度较慢的计算机或 CPU 使用率较高的计算机可能需要调整此值,以减少出现 503 错误的可能性。

设置 shutdownDelay 的示例:

<aspNetCore processPath="dotnet" arguments="myapp.dll" stdoutLogEnabled="false" stdoutLogFile=".logsstdout">
  <handlerSettings>
    <!-- Milliseconds to delay shutdown by.
    this doesn't mean incoming requests will be delayed by this amount,
    but the old app instance will start shutting down after this timeout occurs -->
    <handlerSetting name="shutdownDelay" value="5000" />
  </handlerSettings>
</aspNetCore>

该修补程序位于以全局方式安装的 ANCM 模块中,此模块包含在托管捆绑包中。