引言
随着技术的不断演进,.NET 平台持续为开发者带来创新和改进。作为 .NET 生态系统中的重要组成部分,ASP.NET Core 在每个版本中都引入了令人兴奋的新功能和优化。本文将深入探讨 .NET 10 中 ASP.NET Core 的主要更新,特别是 Blazor 框架中的各项增强功能,旨在为开发者提供一个清晰、全面的概述,帮助您更好地利用这些新特性来构建更强大、更安全的 Web 应用程序。我们将重点关注安全性、性能、开发体验和路由方面的改进,并提供相关的代码示例和详细说明。
正文
Blazor Web App 安全性增强
在 .NET 10 中,Blazor Web App 的安全性得到了显著提升。官方新增并更新了多个安全示例,涵盖了不同的身份验证和授权场景。
1. 新的和经过更新的 Blazor Web App 安全示例
Blazor Web App 的安全示例得到了全面的更新,主要包括以下几个方面:
- OpenID Connect (OIDC) 保护:提供了如何使用 OIDC 保护 Blazor Web App 的详细指南和示例。
- Microsoft Entra ID (原 Azure AD) 保护:更新了使用 Microsoft Entra ID 保护 Blazor Web App 的示例。
- Windows 身份验证保护:新增了使用 Windows 身份验证保护 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 托管标识 配合使用来保护数据。
Blazor UI 组件和性能优化
1. QuickGrid RowClass
参数
为了提供更灵活的 UI 样式控制,QuickGrid
组件新增了 RowClass
参数。 开发者现在可以根据行项的特定条件,动态地将样式表类应用于网格的行。 例如,通过定义一个方法,根据 MyGridItem
的 IsArchived
属性来应用不同的 CSS 类:
/* by 01022.hk - online tools website : 01022.hk/zh/favicon.html */
<QuickGrid ... RowClass="GetRowCssClass">
...
</QuickGrid>
@code {
private string GetRowCssClass(MyGridItem item) =>
item.IsArchived ? "row-archived" : null;
}
这极大地增强了 QuickGrid
的定制能力,使得数据呈现更加直观和富有表现力。
2. 关闭 QuickGrid
列选项
现在,可以使用 QuickGrid
的新方法 HideColumnOptionsAsync
关闭列选项的用户界面。 这对于需要在用户执行特定操作(如应用筛选器)后自动关闭列选项的场景非常有用,从而提升用户体验。
/* by 01022.hk - online tools website : 01022.hk/zh/favicon.html */
<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));
}
3. 响应流式处理的默认启用和选择停用
在 .NET 10 之前,HttpClient
请求的响应流式处理是可选启用的,现在默认启用。 这意味着调用 HttpContent.ReadAsStreamAsync
对于 HttpResponseMessage.Content
(response.Content.ReadAsStreamAsync()
)返回的是 BrowserHttpReadStream
,而不再返回 MemoryStream
。 BrowserHttpReadStream
不支持同步操作,例如 Stream.Read(Span<Byte>)
。 如果代码使用了同步操作,开发者可以选择禁用响应流式处理或手动将 Stream
复制到 MemoryStream
。
要选择退出全局响应流式处理,可以在项目文件中添加 <WasmEnableStreamingResponse>false</WasmEnableStreamingResponse>
属性,或者将 DOTNET_WASM_ENABLE_STREAMING_RESPONSE
环境变量设置为 false
或 0
。
<WasmEnableStreamingResponse>false</WasmEnableStreamingResponse>
要选择退出单个请求的响应流式处理,可以将 HttpRequestMessage
上的 SetBrowserResponseStreamingEnabled
设置为 false
:
requestMessage.SetBrowserResponseStreamingEnabled(false);
4. Blazor 脚本作为静态 Web 资产
为了提升性能和优化资源加载,在 .NET 10 及更高版本中,Blazor 脚本现在被用作具有自动压缩和指纹的静态 Web 资产,而不再从 ASP.NET Core 共享框架中的嵌入资源提供。 这有助于更好地利用浏览器缓存和 CDN,提高应用的加载速度。
5. Blazor WebAssembly 性能分析和诊断计数器
新版本为 Blazor WebAssembly 应用引入了全面的性能分析和诊断计数器。 这些计数器提供了组件生命周期、导航、事件处理和线路管理的详细可观测性,帮助开发者识别和解决性能瓶颈。
6. 预加载的 Blazor 框架静态资源
在 Blazor Web App 中,框架静态资源使用 Link
头信息自动预加载,这允许浏览器在提取和呈现初始页面之前预加载资源。 在独立 Blazor WebAssembly 应用中,框架资源会被安排为高优先级下载,并在浏览器 index.html
页面处理过程早期进行缓存。 这种机制可以显著缩短应用的首次加载时间。
7. Blazor WebAssembly 在 Blazor Web App 中静态资源预加载
为了更好地在 Blazor Web App 中预加载 WebAssembly 资产,Blazor 将 <link>
标头替换为 LinkPreload
组件 (<LinkPreload />
)。 这使得应用程序的基路径配置 (<base href="..." />
) 能够正确识别应用程序的根目录。 默认情况下,Blazor Web App 模板在 .NET 10 中采用此特性。 升级到 .NET 10 的应用可以通过在 LinkPreload
组件的头部内容 (<base>
) 中,将 App
组件放置在基 URL 标签 (App.razor
) 后来实现该功能。
<head>
...
<base href="/" />
+ <LinkPreload />
...
</head>
8. 自定义 Blazor 缓存和 BlazorCacheBootResources
MSBuild 属性已删除
由于所有 Blazor 客户端文件现在都由浏览器进行指纹标记和缓存,Blazor 的自定义缓存机制和 BlazorCacheBootResources
MSBuild 属性已从框架中移除。 开发者应从客户端项目的项目文件中删除此属性,因为它不再有任何影响。
Blazor 路由和导航改进
1. 路由模板要点
[Route]
属性现在支持路由语法突出显示,以帮助开发者更好地可视化路由模板的结构,从而减少路由配置错误。
2. MapsTo
不再滚动到顶部来进行同一页面导航
以前,NavigationManager.NavigateTo
在进行同一页面导航时会滚动到页面顶部。 在 .NET 10 中,此行为已更改,浏览器在导航到同一页面时不再滚动到页面顶部。 这意味着在更新当前页面的地址(例如更改查询字符串或片段)时不再重置视口,从而提升了用户体验,尤其是在单页应用中。
3. 已将重新连接 UI 组件添加到 Blazor Web App 项目模板中
Blazor Web App 项目模板现在包含一个 ReconnectModal
组件,它包含了并置的样式表和 JavaScript 文件,旨在在客户端失去与服务器的 WebSocket 连接时,改进开发者对重新连接 UI 的控制。 该组件不会以编程方式插入样式,确保符合 style-src
策略更严格的内容安全策略 (CSP) 设置。 新的重新连接 UI 功能包括通过在重新连接 UI 元素上设置特定的 CSS 类来指示重新连接状态,以及调度新的 components-reconnect-state-changed
事件以更改重新连接状态。 代码可以使用 CSS 类和新事件所指示的新重新连接状态“retrying
”更好地区分重新连接过程的阶段。
4. 使用 NavLinkMatch.All
时忽略查询字符串和片段
当使用 NavLink
参数的 NavLinkMatch.All
值时,Match
组件现在将忽略查询字符串和片段。 这意味着如果 URL 路径匹配但查询字符串或片段发生更改,则链接将保留 active
类。 要还原到原始行为,可以将 Microsoft.AspNetCore.Components.Routing.NavLink.EnableMatchAllForQueryStringAndFragment
AppContext
开关设置为 true
。 开发者也可以通过重写 NavLink
的 ShouldMatch
方法来自定义匹配行为。
public class CustomNavLink : NavLink
{
protected override bool ShouldMatch(string currentUriAbsolute)
{
// Custom matching logic
}
}
5. NavigationManager.NavigateTo
不再抛出异常 NavigationException
以前,在进行静态服务器端渲染 (SSR) 时,调用 NavigationManager.NavigateTo
会在转换为重定向响应前抛出 NavigationException
,从而中断执行。 在 .NET 10 中,在静态 SSR 期间调用 NavigationManager.NavigateTo
不再引发 NavigationException
。 其行为与交互式呈现一致,执行导航但不会抛出异常。 依赖于 NavigationException
被抛出的代码应进行更新。 例如,在默认 Blazor Identity UI 中,IdentityRedirectManager
过去在调用 RedirectTo
之后抛出一个 InvalidOperationException
,以确保它不会在交互式呈现期间被调用。 现在应删除此异常和 [DoesNotReturn]
属性。
6. Blazor 路由器具有参数 NotFoundPage
Blazor 现在提供了一种改进的方法,用于在导航到不存在的页面时显示“找不到”页面。 可以通过使用 NotFoundPage
参数将页面类型传递给组件 Router
来指定在调用 NavigationManager.NotFound
时要呈现的页面。 推荐使用此方式替代 NotFound
的呈现片段 (<NotFound>...</NotFound>
),因为它支持路由、适配状态码页面重执行中间件,并兼容非 Blazor 场景。 如果同时定义了 NotFound
呈现片段和 NotFoundPage
,则以 NotFoundPage
指定的页面优先。
<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
时,此页面都会自动呈现,从而更轻松地处理缺失路由,并提供一致的用户体验。
7. 使用 NavigationManager
处理静态 SSR 和全局交互式呈现中的“未找到”响应
NavigationManager
现在包含一个 NotFound
方法,用于处理在静态服务器端渲染(静态 SSR)或全局交互渲染期间找不到请求资源的情况。
- 静态服务器端呈现 (SSR):调用
NotFound
会将 HTTP 状态代码设置为 404。 - 交互式呈现:通知 Blazor 路由器 (
Router
组件) 呈现“未找到”内容。 - 流式呈现:如果增强导航处于活动状态,流式呈现会呈现“未找到”内容,而无需重新加载页面。 当增强的导航被阻止时,框架会重定向到“找不到”内容,并刷新页面。
流式 NavigationManager.NotFound
内容呈现使用以下顺序:
NotFoundPage
传递给Router
组件(如果存在)。- 已配置的状态码页面重执行中间件页面。
- 如果上述两种方法均未采用,则不采取任何操作。
非流式 NavigationManager.NotFound
内容呈现使用以下顺序:
NotFoundPage
传递给Router
组件(如果存在)。- 若存在“未找到”呈现片段内容,则使用该内容。不建议在 .NET 10 或更高版本中使用。
DefaultNotFound
404 内容(“Not found
”纯文本)。
UseStatusCodePagesWithReExecute
在处理浏览器地址路由问题(如 URL 输入错误或点击无效链接)时优先。 当 NavigationManager.OnNotFound
被调用时,可以使用 NotFound
事件进行通知。
Blazor 开发体验和互操作性
1. 用于保存组件和服务状态的声明性模型
现在可以通过声明方式指定状态,使其能够通过 [SupplyParameterFromPersistentComponentState]
特性从组件和服务中持久化。 在预呈现期间,具有此属性的属性会通过 PersistentComponentState
服务自动持久化。 当组件以交互方式呈现或实例化服务时,将检索状态。
以前,使用 PersistentComponentState
服务预呈现期间保留组件状态涉及大量代码。 现在可以使用新的声明性模型简化此代码:
@page "/movies"
@inject IMovieService MovieService
@if (MoviesList == null)
{
<p><em>Loading...</em></p>
}
else
{
<QuickGrid Items="MoviesList.AsQueryable()">
...
</QuickGrid>
}
@code {
[SupplyParameterFromPersistentComponentState]
public List<Movie>? MoviesList { get; set; }
protected override async Task OnInitializedAsync()
{
MoviesList ??= await MovieService.GetMoviesAsync();
}
}
可以为同一类型的多个组件序列化状态,并且可以在服务中建立声明性状态,通过在 RegisterPersistentService
组件生成器(Razor)上以自定义服务类型和渲染模式调用 AddRazorComponents
,以便在整个应用中使用。
2. 新的 JavaScript 互作功能
Blazor 添加了对以下 JS 互操作功能的支持:
- 使用构造函数创建对象的实例 JS,并获取引用实例的
IJSObjectReference
/IJSInProcessObjectReference
.NET 句柄。 - 读取或修改 JS 对象属性的值,包括数据属性和访问器属性。
以下异步方法在 IJSRuntime
和 IJSObjectReference
上可用:
-
InvokeNewAsync(string identifier, object?[]? args)
:异步调用指定的 JS 构造函数。var classRef = await JSRuntime.InvokeNewAsync("jsInterop.TestClass", "Blazor!"); var text = await classRef.GetValueAsync<string>("text"); var textLength = await classRef.InvokeAsync<int>("getTextLength");
-
GetValueAsync<TValue>(string identifier)
:异步读取指定 JS 属性的值。var valueFromDataPropertyAsync = await JSRuntime.GetValueAsync<int>( "jsInterop.testObject.num");
-
SetValueAsync<TValue>(string identifier, TValue value)
:异步更新指定 JS 属性的值。await JSRuntime.SetValueAsync("jsInterop.testObject.num", 30);
这些方法都有重载版本,可接收 CancellationToken
参数或 TimeSpan
超时时间参数。
以下同步方法可在 IJSInProcessRuntime
和 IJSInProcessObjectReference
上使用:
-
InvokeNew(string identifier, object?[]? args)
:同步调用指定的 JS 构造函数。var inProcRuntime = ((IJSInProcessRuntime)JSRuntime); var classRef = inProcRuntime.InvokeNew("jsInterop.TestClass", "Blazor!"); var text = classRef.GetValue<string>("text"); var textLength = classRef.Invoke<int>("getTextLength");
-
GetValue<TValue>(string identifier)
:同步读取指定 JS 属性的值。var inProcRuntime = ((IJSInProcessRuntime)JSRuntime); var valueFromDataProperty = inProcRuntime.GetValue<int>( "jsInterop.testObject.num");
-
SetValue<TValue>(string identifier, TValue value)
:同步更新指定 JS 属性的值。var inProcRuntime = ((IJSInProcessRuntime)JSRuntime); inProcRuntime.SetValue("jsInterop.testObject.num", 20);
3. JavaScript 捆绑程序支持
Blazor 的构建输出现在可以通过将 MSBuild 属性 WasmBundlerFriendlyBootConfig
设置为 true
,在发布期间生成捆绑程序友好的输出,从而兼容 JavaScript 捆绑程序(如 Gulp、Webpack 和 Rollup)。 这为开发者在 Blazor 应用中使用现有 JavaScript 工具链提供了更大的灵活性。
4. 在独立 Blazor WebAssembly 应用中设置环境
从 .NET 10 开始,Properties/launchSettings.json
文件不再用于控制独立 Blazor WebAssembly 应用中的环境。 现在,开发者应在应用的项目文件(.csproj
)中使用 <WasmApplicationEnvironmentName>
属性来设置环境。
<WasmApplicationEnvironmentName>Staging</WasmApplicationEnvironmentName>
默认环境为:Development
(用于生成)和 Production
(用于发布)。
5. 内联的启动配置文件
Blazor 的启动配置,在 .NET 10 发布之前存在于名为 blazor.boot.json
的文件中,现在已内联到 dotnet.js
脚本中。 这主要影响直接操作 blazor.boot.json
文件的开发者。
6. 改进了表单验证
Blazor 现在改进了表单验证功能,包括对验证嵌套对象和集合项的属性的支持。 要选择使用新的验证功能,需要执行以下操作:
-
在注册服务的文件
Program
中调用扩展方法AddValidation
。builder.Services.AddValidation();
-
在 C# 类文件中声明表单模型类型,而不是在组件 Razor (
.razor
) 中。 -
使用
[ValidatableType]
特性批注根窗体模型类型。[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(); }
在组件中,继续使用 DataAnnotationsValidator
组件内部的 EditForm
组件:
<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编译器本身都使用源生成器。 目前,一个源生成器的输出不能用作另一个源生成器的输入。
ASP.NET Core Identity 的 Web 身份验证 API(密钥)支持
ASP.NET Core Identity 现在支持基于 WebAuthn 和 FIDO2 标准的密钥身份验证。 Web 身份验证(WebAuthn)API,广泛被称为 passkeys,是一种现代的抗钓鱼身份验证方法,通过利用公钥加密和基于设备的身份验证来提高安全性和用户体验。 此功能允许用户使用安全、基于设备的身份验证方法(例如生物识别或安全密钥)在没有密码的情况下登录。 预览版 6 Blazor Web App 项目模板提供现成的密钥管理和登录功能。
线路状态持久性
在服务器端呈现期间,即使与服务器连接长时间断开或主动暂停,Blazor Web App 也可以保留用户会话(电路)状态,只要没有触发整页刷新。 这样,用户就可以在浏览器标签页节流、移动设备用户切换应用、网络中断或主动资源管理(暂停非活动电路)等情况下恢复会话,而不会丢失未保存的工作。
持久化状态所需的服务器资源比持久化线路少:
- 即使断开连接,线路也可能继续执行工作,并消耗 CPU、内存和其他资源。持久化状态仅消耗开发人员控制的固定内存量。
- 持久化状态表示应用消耗的内存子集,因此服务器不需要跟踪应用的组件和其他服务器端对象。
以下两种情况会保留状态:
- 组件状态:组件用于交互式服务器呈现的状态,例如,从数据库检索到的项目列表或用户正在填写的表单。
- 作用域服务:如当前用户这类保存在服务器端服务中的状态。
默认情况下,当在 AddInteractiveServerComponents
文件中调用 AddRazorComponents
时,会启用状态持久性。 MemoryCache
是单个应用实例的默认存储实现,存储最多 1,000 条持久化线路两小时,这是可配置的。 开发者可以使用以下选项更改内存提供程序的默认值:
PersistedCircuitInMemoryMaxRetained
:要保留的最大线路数。默认值为 1,000 条线路。PersistedCircuitInMemoryRetentionPeriod
:最长保留期是TimeSpan
。默认为 2 小时。
services.Configure<CircuitOptions>(options => {
options.PersistedCircuitInMemoryMaxRetained = {CIRCUIT COUNT};
options.PersistedCircuitInMemoryRetentionPeriod = {RETENTION PERIOD};
});
批注组件属性 [SupplyFromPersistentComponentState]
以启用线路状态持久性。
@foreach (var item in Items) {
<ItemDisplay @key="@($"unique-prefix-{item.Id}")" Item="item" />
}
@code {
[SupplyFromPersistentComponentState]
public List<Item> Items { get; set; }
protected override async Task OnInitializedAsync()
{
Items ??= await LoadItemsAsync();
}
}
若要为作用域服务保留状态,请用 [SupplyFromPersistentComponentState]
注解服务属性,将服务添加到服务集合,并调用 RegisterPersistentService
扩展方法:
public class CustomUserService {
[SupplyFromPersistentComponentState]
public string UserData { get; set; }
}
services.AddScoped<CustomUserService>();
services.AddRazorComponents()
.AddInteractiveServerComponents()
.RegisterPersistentService<CustomUserService>(RenderMode.InteractiveAuto);
Client-side fingerprinting
在 .NET 10 中,开发者可以选择启用独立 Blazor WebAssembly 应用的 JavaScript 模块的客户端指纹识别功能。 在生成/发布期间的独立 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
属性。
<StaticWebAssetFingerprintPattern Include="JSModule" Pattern="*.mjs"
Expression="#[.{fingerprint}]!" />
文件自动放置在导入映射中,并在解析 JavaScript 互操作的导入时,浏览器会使用导入映射来解析指纹文件。
结论
.NET 10 中的 ASP.NET Core 带来了诸多重要更新,特别是 Blazor 框架,它在安全性、性能、用户体验和开发效率方面均有显著提升。 从增强的 Blazor Web App 安全示例、QuickGrid
组件的样式和操作控制,到响应流式处理的默认启用和客户端指纹识别,这些功能都旨在帮助开发者构建更安全、更快速、更易于维护的现代 Web 应用。 新的 JavaScript 互操作功能和声明式状态管理模型也极大地简化了开发流程。 此外,路由和导航的改进,特别是 NavigationManager.NavigateTo
行为的优化以及 NotFoundPage
参数的引入,使得 Blazor 应用的用户体验更加流畅和可控。 密钥身份验证的支持则进一步加强了应用的安全性。 总体而言,.NET 10 的 ASP.NET Core 持续致力于提升开发者的生产力,并为构建高性能的 Web 应用程序提供了更坚实的基础。 开发者应积极探索和利用这些新功能,以充分发挥 .NET 10 的潜力。
系列文章
.NET 10 中的新增功能系列文章1——运行时中的新增功能
本文是由葡萄城技术开发团队发布,转载请注明出处:葡萄城官网