本文重点介绍了 .NET 10 中 ASP.NET Core 中最重要的更改,并提供了相关文档的链接。
本文将在提供新的预览版时更新。 有关中断性变更,请参阅 .NET 中的中断性变更。
Blazor
本部分介绍 Blazor的新功能。
新的和经过更新的Blazor Web App安全示例
我们添加并更新了以下文章中链接的 Blazor Web App 个安全示例:
- 使用 OpenID Connect (OIDC) 保护 ASP.NET Core Blazor Web App 安全
- 使用 Microsoft Entra ID 保护 ASP.NET 核心Blazor Web App
- 使用 Windows 身份验证保护 ASP.NET 核心Blazor Web App
我们的所有 OIDC 和 Entra 示例解决方案现在都包含单独的 Web API 项目(MinimalApiJwt),演示如何安全地配置和调用外部 Web API。 调用 Web API 的方式演示了使用令牌处理程序和具名 HTTP 客户端来对接 OIDC 身份提供程序,或使用 Microsoft Entra ID 的 Microsoft Identity Web 包/API。
示例解决方案在 Program 文件中的 C# 代码中配置。 若要从应用设置文件(例如,appsettings.json)配置解决方案,请参阅 OIDC 或 Entra 文章中 新的JSON 配置提供程序(应用设置)配置供应部分。
我们的 Entra 文章和示例应用还包括有关以下方法的新指南:
- 如何对 Web 场托管方案使用加密的分布式令牌缓存。
- 如何将 Azure Key Vault 与 Azure 托管标识 配合使用来保护数据。
QuickGrid
RowClass 参数
使用新 RowClass 参数,根据行项将样式表类应用于网格的行。 在以下示例中,对每一行调用 GetRowCssClass 方法,根据行项有条件地应用样式表类:
<QuickGrid ... RowClass="GetRowCssClass">
...
</QuickGrid>
@code {
private string GetRowCssClass(MyGridItem item) =>
item.IsArchived ? "row-archived" : null;
}
有关详细信息,请参阅 ASP.NET Core Blazor “QuickGrid” 组件。
Blazor 脚本作为静态 Web 资产
在 .NET 的早期版本中,Blazor 脚本从 ASP.NET Core 共享框架中的嵌入资源提供。 在 .NET 10 或更高版本中,Blazor 脚本用作具有自动压缩和指纹的静态 Web 资产。
有关详细信息,请参阅以下资源:
路由模板要点
[Route] 属性 现在支持路由语法突出显示,以帮助可视化路由模板的结构:
计数器值的路由属性 
NavigateTo 不再滚动到顶部来进行同一页面导航
之前,NavigationManager.NavigateTo 会滚动到页面顶部来进行同一页面导航。 .NET 10 中已更改此行为,以便在导航到同一页面时,浏览器不再滚动到页面顶部。 这意味着在更新当前页面的地址(例如更改查询字符串或片段)时不再重置视区。
已将重新连接 UI 组件添加到 Blazor Web App 项目模板中。
Blazor Web App 项目模板现在包含一个 ReconnectModal 组件,包括并置样式表和 JavaScript 文件,用于在客户端失去与服务器的 WebSocket 连接时改进开发人员对重新连接 UI 的控制。 该组件不会以编程方式插入样式,确保符合 style-src 策略更严格的内容安全策略 (CSP) 设置。 在以前的版本中,默认重新连接 UI 由框架创建,从而可能导致 CSP 冲突。 请注意,当应用程序未定义重新连接 UI 时,默认的重新连接 UI 仍会作为备用方案使用,例如通过项目模板的 ReconnectModal 组件或类似的自定义组件。
新的重新连接 UI 功能:
- 除了通过在重新连接 UI 元素上设置特定的 CSS 类来指示重新连接状态外,还调度新的
components-reconnect-state-changed事件以更改重新连接状态。 - 代码可以使用 CSS 类和新事件所指示的新重新连接状态“
retrying”更好地区分重新连接过程的阶段。
有关详细信息,请参阅 ASP.NET Core BlazorSignalR 指南。
使用 NavLinkMatch.All 时忽略查询字符串和片段
使用 NavLink 参数的 NavLinkMatch.All 值时,Match 组件现在将忽略查询字符串和片段。 这意味着,如果 URL 路径匹配但查询字符串或片段发生更改,则链接将保留 active 类。 若要还原为原始行为,请使用 Microsoft.AspNetCore.Components.Routing.NavLink.EnableMatchAllForQueryStringAndFragmentAppContext 开关 设置为 true。
可以在 ShouldMatch 上替代 NavLink 方法,以自定义匹配行为。
public class CustomNavLink : NavLink
{
protected override bool ShouldMatch(string currentUriAbsolute)
{
// Custom matching logic
}
}
有关详细信息,请参阅 ASP.NET Core Blazor 路由和导航。
关闭 QuickGrid 列选项
现在可以使用新的 QuickGrid 方法关闭 HideColumnOptionsAsync 列选项用户界面。
以下示例展示如何在应用标题筛选器后,立即使用 HideColumnOptionsAsync 方法关闭列选项界面:
<QuickGrid @ref="movieGrid" Items="movies">
<PropertyColumn Property="@(m => m.Title)" Title="Title">
<ColumnOptions>
<input type="search" @bind="titleFilter" placeholder="Filter by title"
@bind:after="@(() => movieGrid.HideColumnOptionsAsync())" />
</ColumnOptions>
</PropertyColumn>
<PropertyColumn Property="@(m => m.Genre)" Title="Genre" />
<PropertyColumn Property="@(m => m.ReleaseYear)" Title="Release Year" />
</QuickGrid>
@code {
private QuickGrid<Movie>? movieGrid;
private string titleFilter = string.Empty;
private IQueryable<Movie> movies = new List<Movie> { ... }.AsQueryable();
private IQueryable<Movie> filteredMovies =>
movies.Where(m => m.Title!.Contains(titleFilter));
}
响应流式处理可以选择启用,如何选择停用
在以前的 Blazor 版本中,HttpClient 请求的响应流式处理可以选择启用。 现在,默认启用响应流式处理。
这是一项重大变更,因为调用 HttpContent.ReadAsStreamAsync 对于 HttpResponseMessage.Content (response.Content.ReadAsStreamAsync()) 返回的是 BrowserHttpReadStream,而不再返回 MemoryStream。
BrowserHttpReadStream 不支持同步作,例如 Stream.Read(Span<Byte>)。 如果代码使用同步操作,可以选择禁用响应流式处理或自行复制Stream到MemoryStream。
若要选择退出全局响应流式处理,请使用以下任一方法:
将
<WasmEnableStreamingResponse>属性添加到项目文件,值为false:<WasmEnableStreamingResponse>false</WasmEnableStreamingResponse>将
DOTNET_WASM_ENABLE_STREAMING_RESPONSE环境变量false设置为或0。
要选择退出单个请求的响应流式处理,请将 SetBrowserResponseStreamingEnabled 上的 false 设为 HttpRequestMessage(在本例中为 requestMessage):
requestMessage.SetBrowserResponseStreamingEnabled(false);
有关详细信息,请参阅 HttpClient 和 HttpRequestMessage 以及“提取 API 请求”选项(调用 Web API 文章)。
客户端指纹识别
.NET 9 的发布引入了静态资产的服务器端指纹识别,并引入了映射静态资产的路由终结点约定(
在生成/发布期间的独立 Blazor WebAssembly 应用中,框架使用生成期间计算的值来替代 index.html 中的占位符,以对静态资产进行指纹识别。 指纹会植入到 blazor.webassembly.js 脚本文件名中。
文件中必须存在 wwwroot/index.html 以下标记才能采用指纹功能:
<head>
...
+ <script type="importmap"></script>
</head>
<body>
...
- <script src="_framework/blazor.webassembly.js"></script>
+ <script src="_framework/blazor.webassembly#[.{fingerprint}].js"></script>
</body>
</html>
在项目文件中(.csproj),将 <OverrideHtmlAssetPlaceholders> 属性集添加到 true:
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
+ <OverrideHtmlAssetPlaceholders>true</OverrideHtmlAssetPlaceholders>
</PropertyGroup>
</Project>
任何带有指纹标记的 index.html 中的脚本都会被框架打上指纹。 例如,名为 scripts.js 的脚本文件位于应用的 wwwroot/js 文件夹中,通过在文件扩展名之前添加 #[.{fingerprint}] 进行指纹处理(.js):
<script src="js/scripts#[.{fingerprint}].js"></script>
若要对独立应用中的其他JS模块进行指纹识别,请使用Blazor WebAssembly应用的项目文件 (<StaticWebAssetFingerprintPattern>) 中的.csproj属性。
在以下示例中,将为应用中所有开发人员提供 .mjs 的文件添加指纹:
<StaticWebAssetFingerprintPattern Include="JSModule" Pattern="*.mjs"
Expression="#[.{fingerprint}]!" />
文件自动放置在导入映射中:
- 自动适用于 Blazor Web App CSR。
- 根据前面的说明在独立的 Blazor WebAssembly 应用中选择启用模块指纹识别时。
在解析 JavaScript 互操作的导入时,浏览器会使用导入映射来解析指纹文件。
在独立 Blazor WebAssembly 应用中设置环境
该文件 Properties/launchSettings.json 不再用于控制独立 Blazor WebAssembly 应用中的环境。
从 .NET 10 开始,在应用的项目文件(<WasmApplicationEnvironmentName>)中使用.csproj属性来设置环境。
以下示例将应用的环境设置为 Staging:
<WasmApplicationEnvironmentName>Staging</WasmApplicationEnvironmentName>
默认环境为:
-
Development(用于生成)。 -
Production(用于发布)。
内联的启动配置文件
Blazor的启动配置,在.NET 10发布之前存在于名为blazor.boot.json的文件中,现在已内联到dotnet.js脚本中。 这仅影响直接操作 blazor.boot.json 文件的开发者,例如:
- 按照 ASP.NET Core Blazor WebAssembly 的缓存与完整性检查失败指南,使用 PowerShell 脚本检查已发布资源的文件完整性。
- 在不使用默认 Webcil 文件格式时,根据托管和部署 ASP.NET Core Blazor WebAssembly 的指导,更改 DLL 文件的文件名扩展名。
目前,还没有为上述方法制定明确的替代策略。 如果需要上述任一策略,请使用任一文章底部的 “打开文档问题 ”链接来打开描述方案的新文档问题。
用于保存组件和服务状态的声明性模型
您现在可以通过声明方式指定状态,使其能够通过 [PersistentState] 特性从组件和服务中持久化。 在预呈现期间,具有此属性的属性会通过 PersistentComponentState 服务自动持久化。 当组件以交互方式呈现或实例化服务时,将检索状态。
在以前的 Blazor 版本中,使用 PersistentComponentState 服务预呈现期间保留组件状态涉及大量代码,如以下示例所示:
@page "/movies"
@implements IDisposable
@inject IMovieService MovieService
@inject PersistentComponentState ApplicationState
@if (MoviesList == null)
{
<p><em>Loading...</em></p>
}
else
{
<QuickGrid Items="MoviesList.AsQueryable()">
...
</QuickGrid>
}
@code {
public List<Movie>? MoviesList { get; set; }
private PersistingComponentStateSubscription? persistingSubscription;
protected override async Task OnInitializedAsync()
{
if (!ApplicationState.TryTakeFromJson<List<Movie>>(nameof(MoviesList),
out var movies))
{
MoviesList = await MovieService.GetMoviesAsync();
}
else
{
MoviesList = movies;
}
persistingSubscription = ApplicationState.RegisterOnPersisting(() =>
{
ApplicationState.PersistAsJson(nameof(MoviesList), MoviesList);
return Task.CompletedTask;
});
}
public void Dispose() => persistingSubscription?.Dispose();
}
现在可以使用新的声明性模型简化此代码:
@page "/movies"
@inject IMovieService MovieService
@if (MoviesList == null)
{
<p><em>Loading...</em></p>
}
else
{
<QuickGrid Items="MoviesList.AsQueryable()">
...
</QuickGrid>
}
@code {
[PersistentState]
public List<Movie>? MoviesList { get; set; }
protected override async Task OnInitializedAsync()
{
MoviesList ??= await MovieService.GetMoviesAsync();
}
}
可以为同一类型的多个组件序列化状态,并且可以在服务中建立声明性状态,通过在 RegisterPersistentService 组件生成器(Razor)上以自定义服务类型和渲染模式调用 AddRazorComponents,以便在整个应用中使用。 有关详细信息,请参阅 ASP.NET 核心 Blazor 预呈现状态持久性。
新的 JavaScript 互作功能
Blazor 添加了对以下 JS 互操作功能的支持:
- 使用构造函数创建对象的实例 JS ,并获取引用实例的 IJSObjectReference/IJSInProcessObjectReference .NET 句柄。
- 读取或修改JS对象属性的值,包括数据属性和访问器属性。
以下异步方法在 IJSRuntime 和 IJSObjectReference 上可用,其作用范围行为与现有 IJSRuntime.InvokeAsync 方法相同:
InvokeConstructorAsync(string identifier, object?[]? args):异步调用指定的 JS 构造函数。 使用new运算符调用该函数。 在以下示例中,jsInterop.TestClass是一个具有构造函数的类,而classRef则是一个 IJSObjectReference 的实例。var classRef = await JSRuntime.InvokeConstructorAsync("jsInterop.TestClass", "Blazor!"); var text = await classRef.GetValueAsync<string>("text"); var textLength = await classRef.InvokeAsync<int>("getTextLength");GetValueAsync<TValue>(string identifier):异步读取指定 JS 属性的值。 该属性不能是set仅限属性。 如果该属性不存在,则会抛出一个异常JSException。 以下示例从数据属性返回一个值:var valueFromDataPropertyAsync = await JSRuntime.GetValueAsync<int>( "jsInterop.testObject.num");SetValueAsync<TValue>(string identifier, TValue value):异步更新指定 JS 属性的值。 该属性不能是get仅限属性。 如果未在目标对象上定义该属性,则会创建该属性。 如果属性存在但不可写,或者无法向对象添加新属性,则会引发 A JSException 。 在以下示例中,如果num不存在,则在testObject上创建并赋值为 30。await JSRuntime.SetValueAsync("jsInterop.testObject.num", 30);
上述每个方法都有重载版本,可接收 CancellationToken 参数或 TimeSpan 超时时间参数。
以下同步方法可在 IJSInProcessRuntime 和 IJSInProcessObjectReference 上使用,其范围行为与现有的 IJSInProcessObjectReference.Invoke 方法相同:
InvokeConstructor(string identifier, object?[]? args):同步调用指定的 JS 构造函数。 使用new运算符调用该函数。 在以下示例中,jsInterop.TestClass是一个具有构造函数的类,而classRef则是一个 IJSInProcessObjectReference 的实例。var inProcRuntime = ((IJSInProcessRuntime)JSRuntime); var classRef = inProcRuntime.InvokeConstructor("jsInterop.TestClass", "Blazor!"); var text = classRef.GetValue<string>("text"); var textLength = classRef.Invoke<int>("getTextLength");GetValue<TValue>(string identifier):同步读取指定 JS 属性的值。 该属性不能是set仅限属性。 如果该属性不存在,则会抛出一个异常JSException。 以下示例从数据属性返回一个值:var inProcRuntime = ((IJSInProcessRuntime)JSRuntime); var valueFromDataProperty = inProcRuntime.GetValue<int>( "jsInterop.testObject.num");SetValue<TValue>(string identifier, TValue value):同步更新指定 JS 属性的值。 该属性不能是get仅限属性。 如果未在目标对象上定义该属性,则会创建该属性。 如果属性存在但不可写,或者无法向对象添加新属性,则会引发 A JSException 。 在以下示例中,如果num在testObject中不存在,则使用值 20 创建它:var inProcRuntime = ((IJSInProcessRuntime)JSRuntime); inProcRuntime.SetValue("jsInterop.testObject.num", 20);
有关详细信息,请参阅 .NET 方法文章中调用 JavaScript 函数 的以下部分:
Blazor WebAssembly 性能分析和诊断计数器
新的性能分析和诊断计数器可用于 Blazor WebAssembly 应用。 如需了解更多信息,请参阅以下文章:
预加载的 Blazor 框架静态资源
在 Blazor Web App 中,框架静态资源使用 Link 头信息 自动预加载,这允许浏览器在提取和呈现初始页面之前预加载资源。 在独立 Blazor WebAssembly 应用中,框架资源会被安排为高优先级下载,并在浏览器 index.html 页面处理过程早期进行缓存。
有关详细信息,请参阅 ASP.NET 核心 Blazor 静态文件。
使用 /> 选择加入以避免在静态服务器端呈现期间使用
在静态服务器端呈现(静态 SSR)期间调用 NavigationManager.NavigateTo 会引发中断 NavigationException执行,然后再转换为重定向响应。 在调试期间,这可能会导致混淆,并且与交互式呈现行为不一致,其中代码在继续正常执行之后 NavigateTo 。
在 .NET 10 中,可以将 MSBuild 属性设置为<BlazorDisableThrowNavigationException>true应用的项目文件中,以避免在静态 SSR 期间引发异常:
<PropertyGroup>
<BlazorDisableThrowNavigationException>true</BlazorDisableThrowNavigationException>
</PropertyGroup>
设置 MSBuild 属性后,在静态 SSR 期间调用NavigationManager.NavigateTo不再引发 。NavigationException 其行为与交互式呈现一致,执行导航但不会抛出异常。 在重定向发生之前执行后 NavigationManager.NavigateTo 的代码。
默认情况下,.NET 10 Blazor Web App 项目模板将 MSBuild 属性设置为 true 。 建议更新到 .NET 10 的应用使用新的 MSBuild 属性,并避免出现以前的行为。
如果使用 MSBuild 属性,则应更新依赖 NavigationException 引发的代码。 在 .NET 10 发布前的项目模板的默认 BlazorIdentity UI Blazor Web App 中,调用 IdentityRedirectManager 后会引发一个 InvalidOperationException 调用 RedirectTo ,以确保在交互式呈现期间未调用该方法。 使用 MSBuild 属性时,现在应删除此异常和 [DoesNotReturn] 属性 。 有关详细信息,请参阅 从 .NET 9 中的 ASP.NET Core 迁移到 .NET 10 中的 ASP.NET Core。
Blazor路由器具有参数NotFoundPage
Blazor 现在提供了一种改进的方法,用于在导航到不存在的页面时显示“找不到”页面。 可以通过使用NavigationManager.NotFound参数将页面类型传递给组件Router来指定在调用NotFoundPage(下一节中所述)时要呈现的页面。 此功能支持路由、跨代码状态代码页重新执行中间件工作,甚至与非Blazor 方案兼容。
.NET 10 或更高版本不支持呈现片段()。NotFound<NotFound>...</NotFound>
<Router AppAssembly="@typeof(Program).Assembly" NotFoundPage="typeof(Pages.NotFound)">
<Found Context="routeData">
<RouteView RouteData="@routeData" />
<FocusOnNavigate RouteData="@routeData" Selector="h1" />
</Found>
<NotFound>This content is ignored because NotFoundPage is defined.</NotFound>
</Router>
项目 Blazor 模板现在默认包含一个 NotFound.razor 页面。 每当在应用中调用NavigationManager.NotFound时,此页面都会自动呈现,从而更轻松地处理缺失路由,并提供一致的用户体验。
有关详细信息,请参阅 ASP.NET Core Blazor 路由和导航。
使用 NavigationManager 处理静态 SSR 和全局交互式呈现中的“未找到”响应
NavigationManager现在包含一个NotFound方法,用于处理在静态服务器端渲染(静态 SSR)或全局交互渲染期间找不到请求资源的情况:
静态服务器端呈现(静态 SSR):调用
NotFound会将 HTTP 状态代码设置为 404。交互式呈现:向路由器(Blazor)发出
Router信号以呈现“找不到”内容。流式呈现:如果 增强的导航 处于活动状态,流式呈现将在找不到内容时无需重新加载页面。 当增强的导航被阻止时,框架会重定向到“找不到”内容,并刷新页面。
流式呈现只能呈现具有路由的组件,例如 NotFoundPage 分配(NotFoundPage="..."),或状态代码页面重新执行中间件页面分配(UseStatusCodePagesWithReExecute)。
DefaultNotFound 404 内容(“Not found”纯文本)没有路由,因此无法在流呈现期间使用。
NavigationManager.NotFound 内容渲染使用以下技术,无论响应是否已开始(按顺序):
- 如果 NotFoundEventArgs.Path 已设置,则呈现已分配页面的内容。
- 如果
Router.NotFoundPage已设置,则呈现分配的页面。 - 已配置的状态码页面重执行中间件页面。
- 如果没有采用上述任何方法,则不采取任何措施。
在处理浏览器地址路由问题(如 URL 输入错误或点击无效链接)时优先。
当NavigationManager.OnNotFound被调用时,可以使用NotFound事件进行通知。
有关详细信息和示例,请参阅 ASP.NET 核心 Blazor 路由和导航。
在不使用 Blazor路由器的应用中支持“未找到”响应
实现自定义路由器的应用可以使用 NavigationManager.NotFound。 可以通过两种方法告知渲染器在调用 NavigationManager.NotFound 时应呈现哪些页面:
无论响应状态如何,建议的方法是调用 UseStatusCodePagesWithReExecute。 调用时 NavigationManager.NotFound ,中间件呈现传递给方法的路径:
app.UseStatusCodePagesWithReExecute(
"/not-found", createScopeForStatusCodePages: true);
如果不想使用 UseStatusCodePagesWithReExecute,应用仍然可以支持已经启动的 NavigationManager.NotFound 响应。 在路由器中订阅OnNotFoundEvent,并设置“未找到”页面路径,通过NotFoundEventArgs.Path通知渲染器在调用NavigationManager.NotFound时需要呈现的内容。
CustomRouter.razor:
@using Microsoft.AspNetCore.Components
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Http
@implements IDisposable
@inject NavigationManager NavigationManager
@code {
protected override void OnInitialized() =>
NavigationManager.OnNotFound += OnNotFoundEvent;
[CascadingParameter]
public HttpContext? HttpContext { get; set; }
private void OnNotFoundEvent(object sender, NotFoundEventArgs e)
{
// Only execute the logic if HTTP response has started
// because setting NotFoundEventArgs.Path blocks re-execution
if (HttpContext?.Response.HasStarted == false)
{
return;
}
e.Path = GetNotFoundRoutePath();
}
// Return the path of the Not Found page that you want to display
private string GetNotFoundRoutePath()
{
...
}
public void Dispose() => NavigationManager.OnNotFound -= OnNotFoundEvent;
}
如果在应用中使用这两种方法,则处理程序中指定的 OnNotFoundEvent “未找到”路径优先于在重新执行中间件中配置的路径。
指标和跟踪
此版本为 Blazor 应用引入了全面的指标和跟踪功能,提供组件生命周期、导航、事件处理和线路管理的详细可观测性。
有关详细信息,请参阅 ASP.NET Core Blazor 性能最佳做法。
JavaScript 捆绑程序支持
Blazor 的构建输出与 JavaScript 打包工具(如 Gulp、Webpack 和 Rollup)不兼容。
Blazor 现在,可以通过将 MSBuild 属性设置为 WasmBundlerFriendlyBootConfigtrue 在发布期间生成捆绑程序友好的输出。
有关详细信息,请参阅托管和部署 ASP.NET Core Blazor。
Blazor WebAssembly在Blazor Web App中静态资源预加载
我们将<link>标头替换为ResourcePreloader组件(<ResourcePreloader />),用于在Blazor Web App中预加载WebAssembly资产。 这样,应用基路径配置(<base href="..." />)便可以正确标识应用的根目录。
如果应用正在使用 loadBootResource 回调 修改 URL,则删除组件将禁用该功能。
默认情况下,Blazor Web App 模板在 .NET 10 中采用此特性,升级到 .NET 10 的应用可以通过在 ResourcePreloader 组件的头部内容 (<base>) 中,将 App 组件放置在基 URL 标签 (App.razor) 后来实现该功能。
<head>
...
<base href="/" />
+ <ResourcePreloader />
...
</head>
有关详细信息,请参阅托管和部署 ASP.NET Core 服务器端 Blazor 应用。
改进了表单验证
Blazor 现在改进了表单验证功能,包括对验证嵌套对象和集合项的属性的支持。
若要创建已验证的表单,请使用 DataAnnotationsValidator 组件内部的 EditForm 组件,就像以前一样。
若要选择使用新的验证功能,请执行以下作:
- 在注册服务的文件
AddValidation中调用扩展方法Program。 - 在 C# 类文件中声明表单模型类型,而不是在组件 Razor (
.razor) 中。 - 使用
[ValidatableType]特性批注根窗体模型类型。
如果不遵循上述步骤,验证行为将保持不变,与以前的 .NET 版本相同。
以下示例演示了采用改进的表单验证的客户订单(为简洁起见省略的详细信息):
在 Program.cs中,调用 AddValidation 服务集合以启用新的验证行为:
builder.Services.AddValidation();
在以下 Order 类中,顶级模型类型需要该 [ValidatableType] 属性。 将自动发现其他类型。
OrderItem 并且 ShippingAddress 不为简洁起见而显示,但如果显示嵌套验证和集合验证,则这些类型的工作方式相同。
Order.cs:
[ValidatableType]
public class Order
{
public Customer Customer { get; set; } = new();
public List<OrderItem> OrderItems { get; set; } = [];
}
public class Customer
{
[Required(ErrorMessage = "Name is required.")]
public string? FullName { get; set; }
[Required(ErrorMessage = "Email is required.")]
public string? Email { get; set; }
public ShippingAddress ShippingAddress { get; set; } = new();
}
在以下 OrderPage 组件中, DataAnnotationsValidator 组件存在于组件中 EditForm 。
OrderPage.razor:
<EditForm Model="Model">
<DataAnnotationsValidator />
<h3>Customer Details</h3>
<div class="mb-3">
<label>
Full Name
<InputText @bind-Value="Model!.Customer.FullName" />
</label>
<ValidationMessage For="@(() => Model!.Customer.FullName)" />
</div>
@* ... form continues ... *@
</EditForm>
@code {
public Order? Model { get; set; }
protected override void OnInitialized() => Model ??= new();
// ... code continues ...
}
声明组件(Razor文件)之外的.razor模型类型的要求是由于新的验证功能和Razor编译器本身都使用源生成器。 目前,一个源生成器的输出不能用作另一个源生成器的输入。
验证支持现在包括:
- 现在支持对嵌套的复杂对象和集合进行验证。
- 这包括由属性属性、类属性和 IValidatableObject 实现定义的验证规则。
- 该
[SkipValidation]属性可以从验证中排除属性或类型。
- 验证现在使用基于源生成器的实现,而不是基于反射的实现,以提高性能和与预先编译(AOT)的兼容性。
组件 DataAnnotationsValidator 现在具有相同的验证顺序和短路行为 System.ComponentModel.DataAnnotations.Validator。 验证类型的 T实例时,将应用以下规则:
- 验证的成员属性
T,包括递归验证嵌套对象。 - 验证的类型
T级别属性。 -
IValidatableObject.Validate如果
T实现该方法,则执行该方法。
如果上述步骤之一生成验证错误,则会跳过其余步骤。
使用不同的程序集中的验证模型
可以通过在库或.Client项目中创建一个方法来验证在不同程序集(例如库或.Client项目的Blazor Web App)中定义的模型,该方法接收IServiceCollection实例作为参数,并对其调用AddValidation。
- 在应用中,调用方法和
AddValidation.
有关详细信息和示例,请参阅 ASP.NET 核心 Blazor 表单验证。
自定义 Blazor 缓存和 BlazorCacheBootResources MSBuild 属性已删除
现在,所有 Blazor 客户端文件都由浏览器进行指纹标记并缓存,框架中已移除 Blazor 的自定义缓存机制和 BlazorCacheBootResources MSBuild 属性。 如果客户端项目的项目文件包含 MSBuild 属性,请删除该属性,因为它不再有任何影响:
- <BlazorCacheBootResources>...</BlazorCacheBootResources>
有关详细信息,请参阅 ASP.NET 核心 Blazor WebAssembly 缓存和完整性检查失败。
ASP.NET Core 的 Web 身份验证 API (密钥)支持 Identity
Web 身份验证(WebAuthn)API支持,广泛被称为passkeys,是一种现代的抗钓鱼身份验证方法,通过利用公钥加密和基于设备的身份验证来提高安全性和用户体验。 ASP.NET Core Identity 现在支持基于 WebAuthn 和 FIDO2 标准的密钥身份验证。 此功能允许用户使用安全、基于设备的身份验证方法(例如生物识别或安全密钥)在没有密码的情况下登录。
项目 Blazor Web App 模板提供现成的通行密钥管理和登录功能。
如需了解更多信息,请参阅以下文章:
线路状态持久性
在服务器端呈现期间,即使与服务器连接长时间断开或主动暂停,Blazor Web App 也可以保留用户会话(电路)状态,只要没有触发整页刷新。 这样,用户就可以在以下情况下恢复会话,而不会丢失未保存的工作:
- 浏览器标签页节流
- 移动设备用户切换应用
- 网络中断
- 主动资源管理(暂停非活动电路)
- 增强型导航
有关详细信息,请参阅 ASP.NET 核心 Blazor 服务器端状态管理。
.NET 和 Blazor WebAssembly 在 WebAssembly 上的热重载
SDK 已迁移到用于 WebAssembly 方案的通用 热重载。 默认情况下,有一个新的 MSBuild 属性,该属性WasmEnableHotReloadtrue用于Debug启用热重载的配置(Configuration == "Debug")。
对于具有自定义配置名称的其他配置,请将该值设置为 true 应用的项目文件中以启用热重载:
<PropertyGroup>
<WasmEnableHotReload>true</WasmEnableHotReload>
</PropertyGroup>
若要禁用 Debug 配置的热重载,请将值设置为 false:
<PropertyGroup>
<WasmEnableHotReload>false</WasmEnableHotReload>
</PropertyGroup>
更新了 PWA 服务工作者注册以防止缓存问题
渐进式 Web 应用程序 (PWA) 项目模板中的Blazor服务辅助角色注册现在包含updateViaCache: 'none'一个选项,该选项可防止在服务辅助角色更新期间出现缓存问题。
- navigator.serviceWorker.register('service-worker.js');
+ navigator.serviceWorker.register('service-worker.js', { updateViaCache: 'none' });
该选项可确保:
- 浏览器不使用服务辅助角色脚本的缓存版本。
- Service Worker 更新可以可靠地应用,而不会被 HTTP 缓存阻止。
- PWA 应用程序可以更可预测的更新其服务工作者。
这解决了可阻止正确应用服务辅助角色更新的缓存问题,这对于依赖服务辅助角色实现脱机功能的 PWA 尤其重要。
我们建议在所有 PWA 中将选项设置为 none,包括那些针对 .NET 9 或更早版本的 PWA。
持久性组件状态的序列化扩展性
使用 IPersistentComponentStateSerializer 接口实现自定义序列化程序。 如果没有注册的自定义序列化程序,序列化将回退到现有的 JSON 序列化。
自定义序列化程序在应用的 Program 文件中注册。 在以下示例中,CustomUserSerializer 为 User 类型注册:
builder.Services.AddSingleton<IPersistentComponentStateSerializer<User>,
CustomUserSerializer>();
该类型使用自定义序列化程序自动持久保存和还原:
[PersistentState]
public User? CurrentUser { get; set; } = new();
OwningComponentBase 现已实现 IAsyncDisposable
OwningComponentBase 现在包括对异步处置的支持,改进资源管理。 有一些新的DisposeAsync和DisposeAsyncCore方法,以及一个更新的Dispose方法,可以同时处理服务范围的同步和异步处置。
用于处理表单中隐藏输入字段的新 InputHidden 组件
新 InputHidden 组件提供用于存储字符串值的隐藏输入字段。
在以下示例中,将创建一个隐藏输入字段用于表单的Parameter属性。 提交表单后,将显示隐藏字段的值:
<EditForm Model="Parameter" OnValidSubmit="Submit" FormName="InputHidden Example">
<InputHidden id="hidden" @bind-Value="Parameter" />
<button type="submit">Submit</button>
</EditForm>
@if (submitted)
{
<p>Hello @Parameter!</p>
}
@code {
private bool submitted;
[SupplyParameterFromForm]
public string Parameter { get; set; } = "stranger";
private void Submit() => submitted = true;
}
对增强型导航的持久性组件状态支持
Blazor 现在支持在 增强导航期间处理永久性组件状态。 在增强导航期间保留的状态可由页面上的交互式组件读取。
默认情况下,持久组件状态仅在最初加载到页面上时由交互式组件加载。 这可以防止在加载组件后将其他增强导航事件覆盖到同一页的重要状态,例如编辑后的 Web 窗体中的数据。
如果数据为只读且不会频繁更改,则选择加入以允许在增强导航期间通过设置AllowUpdates = true属性进行[PersistentState]更新。 这对于显示缓存数据等方案非常有用,这些缓存数据成本高昂,但不会经常更改。 以下示例演示如何对天气预报数据使用 AllowUpdates :
[PersistentState(AllowUpdates = true)]
public WeatherForecast[]? Forecasts { get; set; }
protected override async Task OnInitializedAsync()
{
Forecasts ??= await ForecastService.GetForecastAsync();
}
若要在预呈现期间跳过还原状态,请设置为RestoreBehaviorSkipInitialValue:
[PersistentState(RestoreBehavior = RestoreBehavior.SkipInitialValue)]
public string NoPrerenderedData { get; set; }
若要在重新连接期间跳过还原状态,请设置为 RestoreBehaviorSkipLastSnapshot. 这对于在重新连接后确保新数据非常有用:
[PersistentState(RestoreBehavior = RestoreBehavior.SkipLastSnapshot)]
public int CounterNotRestoredOnReconnect { get; set; }
调用 PersistentComponentState.RegisterOnRestoring 以注册回调以强制控制状态的还原方式,类似于 PersistentComponentState.RegisterOnPersisting 如何完全控制状态的持久化方式。
Blazor WebAssembly 尊重当前的用户界面文化设置
在 .NET 9 或更早版本中,独立 Blazor WebAssembly 应用程序基于 CultureInfo.DefaultThreadCurrentCulture 加载 UI 全球化资源。 如果您想为您定义的 CultureInfo.DefaultThreadCurrentUICulture本地化文化额外加载国际化数据, 请将应用升级到 .NET 10 或更高版本。
Blazor Hybrid
本部分介绍 Blazor Hybrid的新功能。
新 .NET MAUIBlazor Hybrid,其中包含 Blazor Web App 和 ASP.NET Core Identity 文章和示例
已使用 ASP.NET Core .NET MAUI为 Blazor HybridIdentity 和 Web 应用添加了一个新文章和示例应用。
有关详细信息,请参阅以下资源:
- .NET MAUI Blazor Hybrid 和使用 ASP.NET Core Identity 的 Web 应用
-
MauiBlazorWebIdentity示例应用(dotnet/blazor-samplesGitHub 存储库)
SignalR
本部分介绍 SignalR的新功能。
最小 API
本部分介绍最小 API 的新功能。
针对可为空的值类型将窗体提交中的空字符串视为 null
在最小 API 中将 [FromForm] 属性与复杂对象一起使用时,表单帖子中的空字符串值现在转换为 null,而不是导致分析失败。 这种行为与最小 API 中那些与复杂对象无关的表单提交的处理逻辑相匹配。
using Microsoft.AspNetCore.Http;
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapPost("/todo", ([FromForm] Todo todo) => TypedResults.Ok(todo));
app.Run();
public class Todo
{
public int Id { get; set; }
public DateOnly? DueDate { get; set; } // Empty strings map to `null`
public string Title { get; set; }
public bool IsCompleted { get; set; }
}
感谢 @nvmkpk 参与此更改!
最小 API 中的验证支持
现已提供对最小 API 中的验证的支持。 此功能允许请求验证发送到 API 终结点的数据。 启用验证允许 ASP.NET Core 运行时对以下项执行定义的任何验证:
- Query
- Header
- 请求主体
使用命名空间中的 DataAnnotations 属性定义验证。 开发人员通过以下方式自定义验证系统的行为:
- 创建自定义
[Validation]属性实现。 - 实现复杂验证逻辑的
IValidatableObject接口。
如果验证失败,运行时将返回 400 错误的请求响应,其中包含验证错误的详细信息。
为最小 API 启用内置验证支持
通过调用 AddValidation 扩展方法在应用程序的服务容器中注册所需的服务,为最小 API 启用内置验证支持:
builder.Services.AddValidation();
此实现会自动发现在最小 API 处理程序中定义的类型,或是作为在最小 API 处理程序中所定义类型的基类型。 终结点筛选器对这些类型执行验证,并为每个终结点添加。
可以使用扩展方法为特定终结点 DisableValidation 禁用验证,如以下示例所示:
app.MapPost("/products",
([EvenNumber(ErrorMessage = "Product ID must be even")] int productId, [Required] string name)
=> TypedResults.Ok(productId))
.DisableValidation();
Note
对适用于 .NET 10 的 ASP.NET Core 中引入的最小 API 验证生成器进行了一些小改进和修复。 为了支持将来的增强功能,基础验证解析程序 API 现在标记为实验性。 顶级 AddValidation API 和内置验证筛选器保持稳定和非实验性。
使用记录类型的验证
最小 API 还支持使用 C# 记录类型进行验证。 可以使用命名空间中的 System.ComponentModel.DataAnnotations 属性(类似于类)验证记录类型。 例如:
public record Product(
[Required] string Name,
[Range(1, 1000)] int Quantity);
在最小 API 终结点中将记录类型用作参数时,验证属性将自动应用与类类型相同的方式:
app.MapPost("/products", (Product product) =>
{
// Endpoint logic here
return TypedResults.Ok(product);
});
Minimal API 验证与 IProblemDetailsService 的集成
Minimal API 的验证逻辑返回的错误响应现在可以通过应用服务集合中提供的 IProblemDetailsService 实现进行自定义。 这可实现更一致且特定于用户的错误响应。
支持服务器端事件 (SSE)
ASP.NET Core 现在支持使用 TypedResults.ServerSentEvents API 返回 ServerSentEvents 结果。 最小 API 和基于控制器的应用都支持此功能。
Server-Sent 事件是一种服务器推送技术,允许服务器通过单个 HTTP 连接将事件消息流发送到客户端。 在 .NET 中,事件消息表示为SseItem<T>对象,这些对象可能包含事件类型、ID 和数据有效负载。T
TypedResults 类具有一个名为 ServerSentEvents 的新静态方法,可用于返回ServerSentEvents结果。 此方法的第一个参数是 IAsyncEnumerable<SseItem<T>>,它表示要发送到客户端的事件消息流。
以下示例演示如何使用 TypedResults.ServerSentEvents API 将心率事件流作为 JSON 对象返回到客户端:
app.MapGet("/json-item", (CancellationToken cancellationToken) =>
{
async IAsyncEnumerable<HeartRateRecord> GetHeartRate(
[EnumeratorCancellation] CancellationToken cancellationToken)
{
while (!cancellationToken.IsCancellationRequested)
{
var heartRate = Random.Shared.Next(60, 100);
yield return HeartRateRecord.Create(heartRate);
await Task.Delay(2000, cancellationToken);
}
}
return TypedResults.ServerSentEvents(GetHeartRate(cancellationToken),
eventType: "heartRate");
});
有关详细信息,请参见:
- MDN 上的服务器发送的事件。
- 使用 API 将心率事件流作为字符串
TypedResults.ServerSentEvents和 JSON 对象返回给客户端ServerSentEvents。 -
控制器 API 示例应用 使用
TypedResults.ServerSentEventsAPI 将心率事件流作为字符串ServerSentEvents返回,并将 JSON 对象返回到客户端。
验证 API 已移至 Microsoft.Extensions.Validation
验证 API 已移动到 Microsoft.Extensions.Validation 命名空间和 NuGet 包。 此更改使 API 在 ASP.NET 核心 HTTP 方案之外可用。 公共 API 和行为保持不变,只有包和命名空间不同。 现有项目不需要更改代码,因为旧引用会重定向到新实现。
对类和数据记录的增强验证
现在,验证属性可以应用于具有一致代码生成和验证行为的类和记录。 在 ASP.NET 核心应用中使用记录设计模型时,此增强功能可提高灵活性。
社区贡献:感谢 @marcominerva!
OpenAPI
本部分介绍 OpenAPI 的新功能。
OpenAPI 3.1 支持
ASP.NET Core 添加了对在 .NET 10 中生成 OpenAPI 版本 3.1 文档的支持。 尽管只是次要版本更新,但 OpenAPI 3.1 是 OpenAPI 规范的一个重要更新,特别是它对 JSON 模式草案 2020-12的完全支持。
在生成的 OpenAPI 文档中看到的一些更改包括:
- 可以为 null 的类型不再具有架构中的
nullable: true属性。 - 它们没有
nullable: true属性,而是具有一个type关键字,其值是包含null作为类型之一的数组。 - 现在,定义为 C#
int或long的属性或参数显示在生成的 OpenAPI 文档中,并且没有type: integer字段,但包含一个将取值限制为数字的pattern字段。 将 NumberHandling 中的 JsonSerializerOptions 属性设为AllowReadingFromString(即,针对 ASP.NET Core Web 应用的默认值)时,会出现此情况。 若要在 OpenAPI 文档中表示 C#int和long为type: integer,请将 NumberHandling 属性设置为Strict。
使用此功能时,生成的文档的默认 OpenAPI 版本3.1。 通过在 AddOpenApi 的委托参数中显式设置 configureOptions 的 OpenApiVersion 属性,可以更改版本:
builder.Services.AddOpenApi(options =>
{
options.OpenApiVersion = Microsoft.OpenApi.OpenApiSpecVersion.OpenApi3_1;
});
在构建时生成 OpenAPI 文档时,可以通过在 MSBuild 项中设置--openapi-versionOpenApiGenerateDocumentsOptions来选择 OpenAPI 版本。
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<OpenApiGenerateDocuments>true</OpenApiGenerateDocuments>
<!-- Configure build-time OpenAPI generation to produce an OpenAPI 3.1 document. -->
<OpenApiGenerateDocumentsOptions>--openapi-version OpenApi3_1</OpenApiGenerateDocumentsOptions>
</PropertyGroup>
OpenAPI 3.1 支持主要在以下 PR 中添加。
OpenAPI 3.1 重大更改
对 OpenAPI 3.1 的支持需要更新到新的主版本 2.0 的基础 OpenAPI.NET 库。 这个新版本与之前的版本相比有一些重大更改。 如果应用程序具有任何文档、操作或架构转换器,重大变更可能会影响该应用程序。 此迭代中的重大变更包括:
- OpenAPI 文档中的实体(如操作和参数)类型化为接口。 实体的内联和引用变体存在具体的实现。 例如,
IOpenApiSchema可以是内联的OpenApiSchema,也可以是指向文档中其他地方定义的架构的OpenApiSchemaReference。 -
Nullable属性已从OpenApiSchema类型中删除。 若要确定类型是否为可空,请评估OpenApiSchema.Type属性是否设置JsonSchemaType.Null。
最重要的变化之一是,OpenApiAny 类已被删除,以支持直接使用 JsonNode。 使用 OpenApiAny 的转换器需要更新为使用 JsonNode。 以下差异显示架构转换器从 .NET 9 版本升级到 .NET 10 版本过程中的变化:
options.AddSchemaTransformer((schema, context, cancellationToken) =>
{
if (context.JsonTypeInfo.Type == typeof(WeatherForecast))
{
- schema.Example = new OpenApiObject
+ schema.Example = new JsonObject
{
- ["date"] = new OpenApiString(DateTime.Now.AddDays(1).ToString("yyyy-MM-dd")),
+ ["date"] = DateTime.Now.AddDays(1).ToString("yyyy-MM-dd"),
- ["temperatureC"] = new OpenApiInteger(0),
+ ["temperatureC"] = 0,
- ["temperatureF"] = new OpenApiInteger(32),
+ ["temperatureF"] = 32,
- ["summary"] = new OpenApiString("Bracing"),
+ ["summary"] = "Bracing",
};
}
return Task.CompletedTask;
});
请注意,即使仅将 OpenAPI 版本配置为 3.0,这些更改也是必需的。
YAML 中的 OpenAPI
ASP.NET 现在支持以 YAML 格式提供生成的 OpenAPI 文档。 YAML 比 JSON 更简洁,当可以推断出大括号和引号时,它消除了这些。 YAML 还支持多行字符串,这对于长说明很有用。
若要将应用配置为以 YAML 格式提供生成的 OpenAPI 文档,请使用“.yaml”或“.yml”后缀在 MapOpenApi 调用中指定终结点,如以下示例所示:
if (app.Environment.IsDevelopment())
{
app.MapOpenApi("/openapi/{documentName}.yaml");
}
支持:
- YAML 当前仅适用于从 OpenAPI 终结点提供的 OpenAPI。
- 在未来的预览版中将增加在构建时以 YAML 格式生成 OpenAPI 文档的功能。
请参阅此 PR,它添加了对以 YAML 格式提供生成的 OpenAPI 文档的支持。
API 控制器的响应说明ProducesResponseType
ProducesAttribute
ProducesResponseTypeAttribute现在ProducesDefaultResponseTypeAttribute接受可选字符串参数,Description用于设置响应的说明:
[HttpGet(Name = "GetWeatherForecast")]
[ProducesResponseType<IEnumerable<WeatherForecast>>(StatusCodes.Status200OK,
Description = "The weather forecast for the next 5 days.")]
public IEnumerable<WeatherForecast> Get()
{
生成的 OpenAPI 数据:
"responses": {
"200": {
"description": "The weather forecast for the next 5 days.",
"content": {
API 控制器和最小 API 都支持此功能。 对于 Minimal APIs,即使属性的类型与推断的返回类型不完全匹配,该 Description 属性也会被正确设置。
社区贡献(dotnet/aspnetcore #58193) 由 桑德·滕·布林克。
将 XML 文档注释填充到 OpenAPI 文档中
ASP.NET Core OpenAPI 文档生成现在将包含来自 XML 文档注释的元数据,这些元数据涉及 OpenAPI 文档中的方法、类和成员定义。 必须在项目文件中启用 XML 文档注释才能使用此功能。 可以通过将以下属性添加到项目文件来执行此作:
<PropertyGroup>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
在生成时,OpenAPI 包将利用源生成器来发现当前应用程序程序集中的 XML 注释以及任何项目引用,并发出源代码,以通过 OpenAPI 文档转换器将其插入文档中。
请注意,C# 生成过程不会捕获在 lambda expresions 上放置的 XML 文档注释,因此若要使用 XML 文档注释将元数据添加到最小的 API 终结点,必须将终结点处理程序定义为方法,将 XML 文档注释放在方法上,然后从 MapXXX 方法引用该方法。 例如,若要使用 XML 文档注释将元数据添加到最初定义为 lambda 表达式的最小 API 终结点:
app.MapGet("/hello", (string name) =>$"Hello, {name}!");
更改 MapGet 调用以引用方法:
app.MapGet("/hello", Hello);
使用 XML 文档注释定义 Hello 方法:
static partial class Program
{
/// <summary>
/// Sends a greeting.
/// </summary>
/// <remarks>
/// Greeting a person by their name.
/// </remarks>
/// <param name="name">The name of the person to greet.</param>
/// <returns>A greeting.</returns>
public static string Hello(string name)
{
return $"Hello, {name}!";
}
}
在前面的示例中,Hello 方法将添加到 Program 类,但你可以将其添加到项目中的任何类。
上一个示例演示 <summary>、<remarks>和 <param> XML 文档注释。
有关 XML 文档注释(包括所有受支持的标记)的详细信息,请参阅 C# 文档。
由于核心功能是通过源生成器提供的,因此可以通过将以下 MSBuild 添加到项目文件来禁用该功能。
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.0-preview.2.*" GeneratePathProperty="true" />
</ItemGroup>
<Target Name="DisableCompileTimeOpenApiXmlGenerator" BeforeTargets="CoreCompile">
<ItemGroup>
<Analyzer Remove="$(PkgMicrosoft_AspNetCore_OpenApi)/analyzers/dotnet/cs/Microsoft.AspNetCore.OpenApi.SourceGenerators.dll" />
</ItemGroup>
</Target>
源生成器处理 AdditionalFiles 属性中包含的 XML 文件。 若要添加或删除,源将修改属性,如下所示:
<Target Name="AddXmlSources" BeforeTargets="CoreCompile">
<ItemGroup>
<AdditionalFiles Include="$(PkgSome_Package)/lib/net10.0/Some.Package.xml" />
</ItemGroup>
</Target>
Microsoft.AspNetCore.OpenApi 添加到 ASP.NET Core Web API(本机 AOT)模板
ASP.NET Core Web API(本机 AOT)项目模板(短名称webapiaot)现在默认支持使用Microsoft.AspNetCore.OpenApi包生成 OpenAPI 文档。 创建新项目时,使用 --no-openapi 标志禁用此支持。
社区贡献(dotnet/aspnetcore #60337) 由 桑德·滕·布林克。
在DI容器中支持IOpenApiDocumentProvider。
ASP.NET Core 在 .NET 10 中支持依赖注入(DI)容器中的 IOpenApiDocumentProvider。 开发人员可以将 IOpenApiDocumentProvider 注入其应用中,并使用该组件来访问 OpenAPI 文档。 此方法可用于在 HTTP 请求上下文之外访问 OpenAPI 文档,例如在后台服务或自定义中间件中。
以前,可以通过 HostFactoryResolver 和 no-op IServer 实现来运行应用程序启动逻辑,而不启动 HTTP 服务器。 新功能通过提供受 Aspire 启发的 IDistributedApplicationPublisher简化 API 来简化此过程,该 API 是 Aspire 的分布式应用程序托管和发布框架的一部分。
有关详细信息,请参阅 dotnet/aspnetcore #61463。
对 XML 注释生成器的改进
XML 注释生成比早期版本的 .NET 更好地处理 .NET 10 中的复杂类型。
- 它为更广泛的类型生成准确的完整 XML 注释。
- 它处理更复杂的方案。
- 它能够优雅地跳过复杂类型的处理,这些复杂类型在早期版本中会导致构建错误。
这些改进将某些方案的失败模式从生成错误更改为缺少元数据。
此外,现在可以将 XML 文档注释处理配置为访问其他程序集中的 XML 注释。 这对于为当前程序集外部定义的类型(如 ProblemDetails 命名空间中的 Microsoft.AspNetCore.Http 类型)生成文档非常有用。
此配置是使用项目生成文件中的指令完成的。 以下示例演示如何配置 XML 注释生成器以访问程序集中 Microsoft.AspNetCore.Http 类型的 XML 注释,其中包括该 ProblemDetails 类。
<Target Name="AddOpenApiDependencies" AfterTargets="ResolveReferences">
<ItemGroup>
<!-- Include XML documentation from Microsoft.AspNetCore.Http.Abstractions
to get metadata for ProblemDetails -->
<AdditionalFiles
Include="@(ReferencePath->'
%(RootDir)%(Directory)%(Filename).xml')"
Condition="'%(ReferencePath.Filename)' ==
'Microsoft.AspNetCore.Http.Abstractions'"
KeepMetadata="Identity;HintPath" />
</ItemGroup>
</Target>
我们期望在将来的预览版中,在共享框架中包含来自一组选定程序集的 XML 注释,以避免在大多数情况下需要进行此配置。
OpenAPI XML 注释生成器中统一处理文档标识符
即使引用的程序集的文档 ID 包含返回类型后缀,XML 文档注释也能正确合并。 因此,所有有效的 XML 注释都可靠地包含在生成的 OpenAPI 文档中,从而提高了使用引用程序集的 API 的文档准确性和完整性。
表单数据枚举参数在 OpenAPI 中使用实际枚举类型
MVC 控制器作中的表单数据参数现在使用实际枚举类型生成 OpenAPI 元数据,而不是默认为字符串。
社区贡献:感谢 @ascott18!
支持在转换器中生成 OpenApiSchemas
开发人员现在可以使用与 ASP.NET Core OpenAPI 文档生成相同的逻辑为 C# 类型生成架构,并将其添加到 OpenAPI 文档。 然后,可以从 OpenAPI 文档中的其他地方引用架构。
传递给文档、操作和架构转换器的上下文中包括一个可以用于为类型生成架构的新 GetOrCreateSchemaAsync 方法。
此方法还有一个可选 ApiParameterDescription 参数,用于为生成的架构指定其他元数据。
为了支持将架构添加到 OpenAPI 文档,已在操作和架构转换器上下文中增加了一个 Document 属性。 这允许任何转换器使用文档 AddComponent 的方法将架构添加到 OpenAPI 文档。
Example
若要在文档、作或架构转换器中使用此功能,请使用 GetOrCreateSchemaAsync 上下文中提供的方法创建架构,并使用文档 AddComponent 的方法将其添加到 OpenAPI 文档中。
builder.Services.AddOpenApi(options =>
{
options.AddOperationTransformer(async (operation, context, cancellationToken) =>
{
// Generate schema for error responses
var errorSchema = await context.GetOrCreateSchemaAsync(typeof(ProblemDetails), null, cancellationToken);
context.Document?.AddComponent("Error", errorSchema);
operation.Responses ??= new OpenApiResponses();
// Add a "4XX" response to the operation with the newly created schema
operation.Responses["4XX"] = new OpenApiResponse
{
Description = "Bad Request",
Content = new Dictionary<string, OpenApiMediaType>
{
["application/problem+json"] = new OpenApiMediaType
{
Schema = new OpenApiSchemaReference("Error", context.Document)
}
}
};
});
});
将 Microsoft.OpenApi 升级到 2.0.0
Microsoft.OpenApi ASP.NET Core 中用于 OpenAPI 文档生成的库已升级到版本 2.0.0(GA)。
2.0.0 中的中断性变更
预览版中引入了以下重大更改,并保留在 GA 版本中。 这些主要影响实施文档、操作或架构转换器的用户:
随着正式版(GA版本)的更新,预计不会在 OpenAPI 文档生成中出现进一步的重大更改。
OpenAPI 模式生成功能增强
在 OpenAPI 规范中使用 oneOf 为可为 null 的类型建模
通过使用 oneOf 模式替代复杂类型和集合的 nullable 属性,改进了可空类型的 OpenAPI 架构生成。 实现方式:
- 在请求和响应架构中,使用
oneOf与null及其实际类型架构来处理可为 null 的复杂类型。 - 使用反射和
NullabilityInfoContext检测参数、属性以及返回类型的可空性。 - 从组件化架构中删除 null 类型以避免重复。
对架构引用解析的修复和改进
此版本通过正确解析根架构文档中的相对 JSON 架构引用($ref)来改进 OpenAPI 文档生成的 JSON 架构的处理。
在 OpenAPI 架构中将属性说明作为$ref的同级包含
在 .NET 10 之前,ASP.NET Core 放弃了对生成的 OpenAPI 文档中定义的 $ref 属性的说明,因为 OpenAPI v3.0 不允许在架构定义中同时 $ref 具有同级属性。 OpenAPI 3.1 现在允许您将描述与 $ref 一起包含。 RC1 添加了支持在生成的 OpenAPI 架构中将属性描述作为与 $ref 同级的内容。
这是一个社区贡献。 谢谢 @desjoerd!
将 XML 注释中的 [AsParameters] 类型的元数据添加到 OpenAPI 架构中
OpenAPI 架构生成现在处理参数类[AsParameters]属性上的XML注释,以提取用于文档的元数据。
从 OpenAPI 中排除未知 HTTP 方法
OpenAPI 架构生成现在从生成的 OpenAPI 文档中排除未知 HTTP 方法。 不被 OpenAPI 识别的查询方法虽然属于标准 HTTP 方法,但它们现在以优雅的方式从生成的 OpenAPI 文档中排除。
这是一个社区贡献。 谢谢 @martincostello!
改进 JSON 补丁请求体的说明
用于 JSON Patch 操作的 OpenAPI 架构生成现在在使用 JSON Patch 的请求正文中正确应用 application/json-patch+json 媒体类型。 这可确保生成的 OpenAPI 文档准确反映 JSON 补丁操作的预期媒体类型。 此外,JSON补丁请求正文有一个详细的架构来描述JSON补丁文档的结构,包括可执行的操作。
这是一个社区贡献。 谢谢 @martincostello!
使用不变文化生成 OpenAPI 文档
OpenAPI 文档生成现在使用不变文化设置格式化生成的 OpenAPI 文档中的数字和日期。 这可确保生成的文档是一致的,并且不会根据服务器的区域性设置而有所不同。
这是一个社区贡献。 谢谢 @martincostello!
身份验证和授权
身份验证和授权指标
为 ASP.NET Core 中的某些身份验证和授权事件添加了指标。 通过此更改,现在可以获取以下事件的指标:
- Authentication:
- 经过身份验证的请求持续时间
- 质询计数
- 禁止计数
- 登录次数
- 注销计数
- Authorization:
- 需要授权的请求计数
下图显示了 Aspire 仪表板中经过身份验证的请求持续时间指标的示例:
有关详细信息,请参阅 ASP.NET 核心内置指标。
ASP.NET 核心 Identity 指标
ASP.NET Core Identity 使用指标改进了 .NET 10 中的可观测性。 指标是计数器、直方图和仪表,可提供系统或应用程序行为的时序度量。
例如,使用新的 ASP.NET 核心 Identity 指标来观察:
- 用户管理:新建用户创建、密码更改和角色分配。
- 登录/会话处理:登录尝试、登录、注销和使用双重身份验证的用户。
新的指标在计量中 Microsoft.AspNetCore.Identity :
aspnetcore.identity.user.create.durationaspnetcore.identity.user.update.durationaspnetcore.identity.user.delete.durationaspnetcore.identity.user.check_password_attemptsaspnetcore.identity.user.generated_tokensaspnetcore.identity.user.verify_token_attemptsaspnetcore.identity.sign_in.authenticate.durationaspnetcore.identity.sign_in.check_password_attemptsaspnetcore.identity.sign_in.sign_insaspnetcore.identity.sign_in.sign_outsaspnetcore.identity.sign_in.two_factor_clients_rememberedaspnetcore.identity.sign_in.two_factor_clients_forgotten
有关在 ASP.NET Core 中使用指标的详细信息,请参阅 ASP.NET Core 指标。
避免对已知 API 终结点进行登录重定向
默认情况下,对受 cookie 身份验证保护的已知 API 终结点发出的未经身份验证和未经授权的请求现在会导致 401 和 403 响应,而不是重定向到登录或访问被拒绝的 URI。
此更改是应广大请求而进行的,因为对 API 终结点来说,重定向未经身份验证的请求到登录页通常没有意义。API 通常依赖于 401 和 403 状态代码来传达身份验证失败,而不是通过 HTML 重定向。
已知 API 终结点 是使用新 IApiEndpointMetadata 接口标识的,实现新接口的元数据已自动添加到以下内容:
-
[ApiController]端点 - 读取 JSON 请求正文或写入 JSON 响应的最小 API 终结点
- 使用
TypedResults返回类型的端点 - SignalR 端点
如果 IApiEndpointMetadata 存在,身份验证 cookie 处理程序现在返回适当的 HTTP 状态代码(401 用于未经身份验证的请求,403 表示禁止请求),而不是重定向。
如果想要阻止此新行为,并且始终将未经身份验证或未经授权的请求重定向到登录页面和访问被拒绝的 URI,无论目标端点如何,都可以按如下所示覆盖 RedirectToLogin 和 RedirectToAccessDenied 事件:
builder.Services.AddAuthentication()
.AddCookie(options =>
{
options.Events.OnRedirectToLogin = context =>
{
context.Response.Redirect(context.RedirectUri);
return Task.CompletedTask;
};
options.Events.OnRedirectToAccessDenied = context =>
{
context.Response.Redirect(context.RedirectUri);
return Task.CompletedTask;
};
});
有关此中断性变更的详细信息,请参阅 ASP.NET 核心中断性变更公告。
Miscellaneous
本部分介绍 .NET 10 中的其他新功能。
配置取消异常处理程序诊断
已将新的配置选项添加到 ASP.NET 核心异常处理程序中间件,用于控制诊断输出: ExceptionHandlerOptions.SuppressDiagnosticsCallback 此回调传递与请求和异常相关的上下文,允许您添加逻辑,以确定中间件是否应写入异常日志和其他遥测信息。
如果知道异常是暂时性的,或者已由异常处理程序中间件处理,并且不希望将错误日志写入可观测性平台,则此设置非常有用。
中间件的默认行为也已更改:它不再为处理 IExceptionHandler异常写入异常诊断。 根据用户反馈,在错误级别记录已处理的异常通常是不受欢迎的,当返回IExceptionHandler.TryHandleAsynctrue时。
可以通过配置 SuppressDiagnosticsCallback以下方法还原到以前的行为:
app.UseExceptionHandler(new ExceptionHandlerOptions
{
SuppressDiagnosticsCallback = context => false;
});
有关此突破性变更的详细信息,请参阅 https://github.com/aspnet/Announcements/issues/524。
支持 .localhost 顶级域
.localhost顶级域(TLD)在RFC2606中定义,RFC6761保留用于测试目的,可供用户在本地使用,就像任何其他域名一样。 这意味着根据这些 RFC,允许并期望本地使用像 myapp.localhost 这样的名称,它会解析为 IP 环回地址。 此外,新型的常青浏览器已经能够自动将任何*.localhost名称解析为IP环回地址(127.0.0.1/::1),这实际上使它们成为在本地计算机上由localhost托管的任何服务的别名。也就是说,任何响应http://localhost:6789的服务也将会响应http://anything-here.localhost:6789,前提是服务没有进行进一步的特定主机名验证或实施。
ASP.NET Core 已在 .NET 10 预览版 7 中进行了更新,以更好地支持 .localhost TLD,因此,现在可以在本地开发环境中创建和运行 ASP.NET Core 应用程序时轻松使用它。 通过不同的名称可以解析在本地运行的不同应用,以便更好地分离某些与域名关联的网站资产,例如 Cookie,并更轻松地识别通过浏览器地址栏中显示的名称浏览的应用。
ASP.NET Core 的内置 HTTP 服务器,Kestrel 现在将正确处理通过 *.localhost 设置的任何 名称,将其视为本地环回地址,从而绑定到该地址,而不是绑定到所有外部地址(即绑定到 127.0.0.1/::1 而不是绑定到 0.0.0.0/::)。 这包括"applicationUrl" 在 launchSettings.json 文件中配置的启动配置文件中的属性,以及ASPNETCORE_URLS 环境变量中的属性。 配置为侦听.localhost地址时,Kestrel会记录一条信息消息,说明 .localhost和localhost地址都是可以使用的。
虽然 Web 浏览器会自动将 *.localhost 名称解析为本地环回地址,但其他应用可能会将 *.localhost 名称当作常规域名,并尝试通过其相应的 DNS 堆栈解析这些名称。 如果 DNS 配置无法将 *.localhost 名称解析为地址,它们将无法连接。 当不在 Web 浏览器中时,可以继续使用常规 localhost 名称来寻址应用。
ASP.NET 核心 HTTPS 开发证书(包括dotnet dev-certs https命令)已更新,以确保证书可用于*.dev.localhost域名。 安装 .NET 10 SDK 预览版 7 后,通过在命令行运行 dotnet dev-certs https --trust 来信任新的开发人员证书,以确保系统配置为信任新证书。
证书将名称 *.dev.localhost 列为使用者可选名称(SAN),而不是 *.localhost 因为对顶级域名使用通配符证书无效。
ASP.NET Core Empty (web) 和 Blazor Web App (blazor) 的项目模板已更新为新选项,该选项指定后,将创建的项目配置为使用.dev.localhost域名后缀,并将其与项目名称相结合,以允许应用浏览到地址,例如https://myapp.dev.localhost:5036:
$ dotnet new web -n MyApp --localhost-tld
The template "ASP.NET Core Empty" was created successfully.
Processing post-creation actions...
Restoring D:\src\MyApp\MyApp.csproj:
Restore succeeded.
$ cd .\MyApp\
$ dotnet run --launch-profile https
info: Microsoft.Hosting.Lifetime[14]
Now listening on: https://myapp.dev.localhost:7099
info: Microsoft.Hosting.Lifetime[14]
Now listening on: https://localhost:7099/
info: Microsoft.Hosting.Lifetime[14]
Now listening on: http://myapp.dev.localhost:5036
info: Microsoft.Hosting.Lifetime[14]
Now listening on: http://localhost:5036/
info: Microsoft.Hosting.Lifetime[0]
Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
Hosting environment: Development
info: Microsoft.Hosting.Lifetime[0]
Content root path: D:\src\local\10.0.1xx\MyApp
MVC 和 Minimal APIs 中的 Json+PipeReader 反序列化支持
公关: https://github.com/dotnet/aspnetcore/pull/62895
MVC、最小 API 和 HttpRequestJsonExtensions.ReadFromJsonAsync 方法都已更新为使用新的 Json+PipeReader 支持,而无需对应用程序进行任何代码更改。
对于大多数应用程序,添加此支持不会影响其行为。 但是,如果应用程序正在使用自定义 JsonConverter,则转换器可能无法正确处理 Utf8JsonReader.HasValueSequence 。 这可能会导致数据缺失和错误,例如 ArgumentOutOfRangeException反序列化时。
快速解决方法(尤其是如果你没有正在使用的自定义 JsonConverter )是将 "Microsoft.AspNetCore.UseStreamBasedJsonParsing"AppContext 开关设置为 "true"。 这应该是暂时的解决方法, JsonConverter 应更新以支持 HasValueSequence。
为了修复 JsonConverter 的实现,有一个快速解决方案可以从 ReadOnlySequence 分配一个数组,示例如下所示:
public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
var span = reader.HasValueSequence ? reader.ValueSequence.ToArray() : reader.ValueSpan;
// previous code
}
还有一个更复杂但性能优越的修复,它将涉及单独的代码路径来处理 ReadOnlySequence。
public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.HasValueSequence)
{
reader.ValueSequence;
// ReadOnlySequence optimized path
}
else
{
reader.ValueSpan;
// ReadOnlySpan optimized path
}
}
从内存池自动移除
IIS 和 HTTP.sys 使用的 Kestrel内存池现在会在应用程序空闲或负载不足时自动逐出内存块。 该功能会自动运行,无需手动启用或配置。
为何内存回收很重要
此前,由内存池分配的内存即使在未使用时也仍然保留。 当应用空闲一段时间时,此功能会将内存释放回系统。 此逐出可减少总体内存使用率,并帮助应用程序在不同工作负荷下保持响应状态。
使用内存回收指标
指标已添加到服务器实现使用的默认内存池。 新指标位于名称 "Microsoft.AspNetCore.MemoryPool"下。
有关指标及其用法的信息,请参阅 ASP.NET 核心指标。
管理内存池
除了通过逐出不需要的内存块来更有效地使用内存池外,.NET 10 还提高了创建内存池的体验。 它通过提供内置的 IMemoryPoolFactory 和 MemoryPoolFactory 的实现来实现这一点。 它通过依赖项注入使实现可供应用程序使用。
下面的代码示例演示了一个简单的后台服务,该服务使用内置的内存池工厂实现来创建内存池。 这些池受益于自动逐出功能:
public class MyBackgroundService : BackgroundService
{
private readonly MemoryPool<byte> _memoryPool;
public MyBackgroundService(IMemoryPoolFactory<byte> factory)
{
_memoryPool = factory.Create();
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
try
{
await Task.Delay(20, stoppingToken);
// do work that needs memory
var rented = _memoryPool.Rent(100);
rented.Dispose();
}
catch (OperationCanceledException)
{
return;
}
}
}
}
为了使用您自己的内存池工厂,请创建一个类以实现 IMemoryPoolFactory,并通过依赖注入注册它,如以下示例所示。 以这种方式创建的内存池也受益于自动逐出功能:
services.AddSingleton<IMemoryPoolFactory<byte>,
CustomMemoryPoolFactory>();
public class CustomMemoryPoolFactory : IMemoryPoolFactory<byte>
{
public MemoryPool<byte> Create()
{
// Return a custom MemoryPool implementation
// or the default, as is shown here.
return MemoryPool<byte>.Shared;
}
}
HTTP.sys 的可自定义安全描述符
现在可以为 HTTP.sys 请求队列指定自定义安全描述符。 新的 RequestQueueSecurityDescriptor 属性支持 HttpSysOptions 对请求队列的访问权限进行更精细的控制。 这种精细控制使你可以根据应用程序的需求定制安全性。
使用新属性可以执行哪些操作
HTTP.sys 中的 请求队列 是一种内核级结构,可以临时存储传入的 HTTP 请求,直到应用程序准备好处理这些请求。 通过自定义安全描述符,可以允许或拒绝对请求队列的特定用户或组的访问权限。 在希望在操作系统级别限制或委托 HTTP.sys 请求处理的情况下,这非常有用。
如何使用新属性
仅当创建新请求队列时,该 RequestQueueSecurityDescriptor 属性才适用。 该属性不会影响现有请求队列。 若要使用此属性,请在配置 HTTP.sys 服务器时将其设置为 GenericSecurityDescriptor 实例。
例如,以下代码允许所有经过身份验证的用户,但拒绝来宾:
using System.Security.AccessControl;
using System.Security.Principal;
using Microsoft.AspNetCore.Server.HttpSys;
// Create a new security descriptor
var securityDescriptor = new CommonSecurityDescriptor(isContainer: false, isDS: false, sddlForm: string.Empty);
// Create a discretionary access control list (DACL)
var dacl = new DiscretionaryAcl(isContainer: false, isDS: false, capacity: 2);
dacl.AddAccess(
AccessControlType.Allow,
new SecurityIdentifier(WellKnownSidType.BuiltinUsersSid, null),
-1,
InheritanceFlags.None,
PropagationFlags.None
);
dacl.AddAccess(
AccessControlType.Deny,
new SecurityIdentifier(WellKnownSidType.BuiltinGuestsSid, null),
-1,
InheritanceFlags.None,
PropagationFlags.None
);
// Assign the DACL to the security descriptor
securityDescriptor.DiscretionaryAcl = dacl;
// Configure HTTP.sys options
var builder = WebApplication.CreateBuilder();
builder.WebHost.UseHttpSys(options =>
{
options.RequestQueueSecurityDescriptor = securityDescriptor;
});
有关详细信息,请参阅 ASP.NET Core 中的HTTP.sys Web 服务器实现。
提供对使用顶层语句测试应用的更好支持
.NET 10 现在更好地支持使用 顶级语句的测试应用。 以前,开发人员必须手动将 public partial class Program 添加到 Program.cs 文件,以便测试项目可以引用 Program class。
public partial class Program 是必需的,因为 C# 9 中的顶级语句特性功能了一个 Program class,它被声明为内部。
在 .NET 10 中,源生成器 用于生成 public partial class Program 声明(如果程序员未显式声明)。 此外,还添加了一个分析器来检测何时显式声明 public partial class Program,并建议开发人员将其删除。
以下 PR 为此功能做出了贡献:
使用 System.Text.Json 实现新的 JSON 补丁
- 描述要应用于 JSON 文档的更改的标准格式。
- 在 RFC 6902 中定义,在 RESTful API 中广泛使用,以对 JSON 资源执行部分更新。
- 表示可用于修改 JSON 文档的作序列(例如,添加、删除、替换、移动、复制、测试)。
在web应用中,JSON Patch通常用于PATCH操作以执行资源的部分更新。 与其发送整个资源进行更新,客户端可以发送仅包含更改的 JSON Patch 文档。 修补可减少有效负载大小并提高效率。
此版本引入了基于 Microsoft.AspNetCore.JsonPatch 序列化的新 System.Text.Json 实现。 此功能:
- 通过利用针对 .NET 进行优化的
System.Text.Json库,与现代 .NET 实践相一致。 - 与基于旧
Newtonsoft.Json版的实现相比,提供改进的性能和减少的内存使用量。
以下基准将新 System.Text.Json 实现的性能与旧 Newtonsoft.Json 实现进行比较。
| Scenario | Implementation | Mean | 分配的内存 |
|---|---|---|---|
| 应用程序基准 | Newtonsoft.JsonPatch | 271.924μs | 25 KB |
| System.Text.JsonPatch | 1.584μs | 3 KB | |
| 反序列化基准 | Newtonsoft.JsonPatch | 19.261μs | 43 KB |
| System.Text.JsonPatch | 7.917μs | 7 KB |
这些基准测试突出显示了显著的性能提升,并减少了新实现的内存使用量。
Notes:
- 新实现不是旧实现的直接替换。 具体而言,新实现不支持动态类型,例如 ExpandoObject。
- JSON 修补程序标准具有 固有的安全风险。 由于这些风险固有于 JSON 修补程序标准,因此新实现 不会尝试降低固有的安全风险。 开发人员有责任确保 JSON 修补程序文档可以安全地应用于目标对象。 有关详细信息,请参阅“ 缓解安全风险 ”部分。
Usage
要通过 System.Text.Json 启用 JSON 修补程序支持,请安装 Microsoft.AspNetCore.JsonPatch.SystemTextJson NuGet 包。
dotnet add package Microsoft.AspNetCore.JsonPatch.SystemTextJson --prerelease
此包提供一个 JsonPatchDocument<T> 类,用于表示类型 T 对象的 JSON Patch 文档,并使用 System.Text.Json 提供用于序列化和反序列化 JSON Patch 文档的自定义逻辑。 类JsonPatchDocument<T>的关键方法是ApplyTo,它将修补操作应用于类型T的目标对象。
以下示例演示如何使用 ApplyTo 该方法将 JSON 修补程序文档应用于对象。
示例:应用 JsonPatchDocument
下面的示例展示了如何:
-
add、replace和remove操作。 - 对嵌套属性的操作。
- 向数组添加新项。
- 在 JSON 修补程序文档中使用 JSON 字符串枚举转换器。
// Original object
var person = new Person {
FirstName = "John",
LastName = "Doe",
Email = "johndoe@gmail.com",
PhoneNumbers = [new() {Number = "123-456-7890", Type = PhoneNumberType.Mobile}],
Address = new Address
{
Street = "123 Main St",
City = "Anytown",
State = "TX"
}
};
// Raw JSON Patch document
var jsonPatch = """
[
{ "op": "replace", "path": "/FirstName", "value": "Jane" },
{ "op": "remove", "path": "/Email"},
{ "op": "add", "path": "/Address/ZipCode", "value": "90210" },
{
"op": "add",
"path": "/PhoneNumbers/-",
"value": { "Number": "987-654-3210", "Type": "Work" }
}
]
""";
// Deserialize the JSON Patch document
var patchDoc = JsonSerializer.Deserialize<JsonPatchDocument<Person>>(jsonPatch);
// Apply the JSON Patch document
patchDoc!.ApplyTo(person);
// Output updated object
Console.WriteLine(JsonSerializer.Serialize(person, serializerOptions));
// Output:
// {
// "firstName": "Jane",
// "lastName": "Doe",
// "address": {
// "street": "123 Main St",
// "city": "Anytown",
// "state": "TX",
// "zipCode": "90210"
// },
// "phoneNumbers": [
// {
// "number": "123-456-7890",
// "type": "Mobile"
// },
// {
// "number": "987-654-3210",
// "type": "Work"
// }
// ]
// }
ApplyTo 方法通常遵循 System.Text.Json 的约定和选项来处理 JsonPatchDocument,包括由以下选项控制的行为:
-
NumberHandling:是否从字符串中读取数值属性。 -
PropertyNameCaseInsensitive:属性名称是否区分大小写。
System.Text.Json和新JsonPatchDocument<T>实现之间的主要区别:
- 目标对象的运行时类型,而不是所声明的类型,决定了
ApplyTo会修补哪些属性。 -
System.Text.Json反序列化依赖于声明的类型来标识符合条件的属性。
示例:应用带有错误处理的 JsonPatchDocument
应用 JSON 修补程序文档时可能会出现各种错误。 例如,目标对象可能没有指定的属性,或者指定的值可能与属性类型不兼容。
JSON 补丁还支持该 test 操作。 该 test 作检查指定的值是否等于目标属性,如果不是,则返回错误。
以下示例演示如何正常处理这些错误。
Important
传递给 ApplyTo 方法的对象被就地修改。 如果任何操作失败,调用方有责任放弃这些更改。
// Original object
var person = new Person {
FirstName = "John",
LastName = "Doe",
Email = "johndoe@gmail.com"
};
// Raw JSON Patch document
var jsonPatch = """
[
{ "op": "replace", "path": "/Email", "value": "janedoe@gmail.com"},
{ "op": "test", "path": "/FirstName", "value": "Jane" },
{ "op": "replace", "path": "/LastName", "value": "Smith" }
]
""";
// Deserialize the JSON Patch document
var patchDoc = JsonSerializer.Deserialize<JsonPatchDocument<Person>>(jsonPatch);
// Apply the JSON Patch document, catching any errors
Dictionary<string, string[]>? errors = null;
patchDoc!.ApplyTo(person, jsonPatchError =>
{
errors ??= new ();
var key = jsonPatchError.AffectedObject.GetType().Name;
if (!errors.ContainsKey(key))
{
errors.Add(key, new string[] { });
}
errors[key] = errors[key].Append(jsonPatchError.ErrorMessage).ToArray();
});
if (errors != null)
{
// Print the errors
foreach (var error in errors)
{
Console.WriteLine($"Error in {error.Key}: {string.Join(", ", error.Value)}");
}
}
// Output updated object
Console.WriteLine(JsonSerializer.Serialize(person, serializerOptions));
// Output:
// Error in Person: The current value 'John' at path 'FirstName' is not equal
// to the test value 'Jane'.
// {
// "firstName": "John",
// "lastName": "Smith", <<< Modified!
// "email": "janedoe@gmail.com", <<< Modified!
// "phoneNumbers": []
// }
减轻安全风险
使用 Microsoft.AspNetCore.JsonPatch.SystemTextJson 包时,了解和缓解潜在的安全风险至关重要。 以下部分概述了与 JSON 修补程序关联的已识别的安全风险,并提供建议的缓解措施,以确保包的安全使用。
Important
这不是威胁的详尽列表。 应用开发人员必须进行自己的威胁模型评审,以确定特定于应用的综合列表,并根据需要提出适当的缓解措施。 例如,向修补操作公开集合的应用程序,应考虑到如果这些操作在集合开头插入或删除元素,可能会引发算法复杂性攻击。
通过运行自己的应用的综合威胁模型并解决已识别的威胁,同时遵循以下建议的缓解措施,这些包的使用者可以将 JSON 修补程序功能集成到其应用中,同时最大程度地降低安全风险。
这些包的使用者可以将 JSON 修补程序功能集成到其应用中,同时最大程度地降低安全风险,包括:
- 为自己的应用运行全面的威胁模型。
- 解决已识别的威胁。
- 请遵循以下部分中的建议缓解措施进行操作。
通过内存放大拒绝服务 (DoS)
-
场景:恶意客户端提交了一个
copy操作,该操作多次重复复制大型对象图,导致内存过度消耗。 - 影响:潜在的Of-Memory(内存不足条件),导致服务中断。
-
Mitigation:
- 在调用
ApplyTo之前验证传入的 JSON 补丁文档的大小和结构。 - 验证必须特定于应用,但示例验证可能类似于以下内容:
- 在调用
public void Validate(JsonPatchDocument<T> patch)
{
// This is just an example. It's up to the developer to make sure that
// this case is handled properly, based on the app's requirements.
if (patch.Operations.Where(op=>op.OperationType == OperationType.Copy).Count()
> MaxCopyOperationsCount)
{
throw new InvalidOperationException();
}
}
业务逻辑 Subversion
- 场景:补丁操作可以操纵具有隐式不变量的字段(例如,内部标志、ID 或计算字段),从而违反业务约束。
- 影响:数据完整性问题和意外的应用行为。
-
Mitigation:
- 将 POCO 对象与显式定义的属性一起使用,这些属性可以安全地进行修改。
- 避免在目标对象中公开敏感或安全关键属性。
- 如果未使用 POCO 对象,请在应用操作后验证已修补对象,以确保不违反业务规则和不可变条件。
身份验证和授权
- 场景:未经身份验证或未经授权的客户端发送恶意 JSON 修补请求。
- 影响:未经授权的访问以修改敏感数据或中断应用行为。
-
Mitigation:
- 使用适当的身份验证和授权机制保护接受 JSON 修补请求的终结点。
- 限制对具有适当权限的受信任客户端或用户的访问权限。
使用 RedirectHttpResult.IsLocalUrl 检测 URL 是否为本地
使用新的 RedirectHttpResult.IsLocalUrl(url) 辅助方法检测 URL 是否为本地。 如果符合以下条件,则该 URL 被视为本地 URL:
使用 虚拟路径"~/" 的 URL 也是本地的。
IsLocalUrl 用于在重定向到 URL 之前验证 URL,以防止打开重定向攻击。
if (RedirectHttpResult.IsLocalUrl(url))
{
return Results.LocalRedirect(url);
}
感谢 @martincostello 的贡献!
重大变化
使用 .NET 中重大更改 中的文章查找在将应用升级到较新版本 .NET 时可能适用的重大改动。