引言
随着技术的不断演进,.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 类:- <QuickGrid ... Row>
- ...
- </QuickGrid>
- @code {
- private string GetRowCssClass(MyGridItem item) =>
- item.IsArchived ? "row-archived" : null;
- }
复制代码 这极大地增强了 QuickGrid 的定制能力,使得数据呈现更加直观和富有表现力。
2. 关闭 QuickGrid 列选项
现在,可以使用 QuickGrid 的新方法 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));
- }
复制代码 3. 响应流式处理的默认启用和选择停用
在 .NET 10 之前,HttpClient 请求的响应流式处理是可选启用的,现在默认启用。 这意味着调用 HttpContent.ReadAsStreamAsync 对于 HttpResponseMessage.Content(response.Content.ReadAsStreamAsync())返回的是 BrowserHttpReadStream,而不再返回 MemoryStream。 BrowserHttpReadStream 不支持同步操作,例如 Stream.Read(Span)。 如果代码使用了同步操作,开发者可以选择禁用响应流式处理或手动将 Stream 复制到 MemoryStream。
要选择退出全局响应流式处理,可以在项目文件中添加 <WasmEnableStreamingResponse>false</WasmEnableStreamingResponse> 属性,或者将 DOTNET_WASM_ENABLE_STREAMING_RESPONSE 环境变量设置为 <WasmEnableStreamingResponse>false</WasmEnableStreamingResponse> 或 0。- <WasmEnableStreamingResponse>false</WasmEnableStreamingResponse>
复制代码 要选择退出单个请求的响应流式处理,可以将 HttpRequestMessage 上的 SetBrowserResponseStreamingEnabled 设置为 <WasmEnableStreamingResponse>false</WasmEnableStreamingResponse>:- requestMessage.SetBrowserResponseStreamingEnabled(<WasmEnableStreamingResponse>false</WasmEnableStreamingResponse>);
复制代码 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 将 标头替换为 LinkPreload 组件 ()。 这使得应用程序的基路径配置 () 能够正确识别应用程序的根目录。 默认情况下,Blazor Web App 模板在 .NET 10 中采用此特性。 升级到 .NET 10 的应用可以通过在 LinkPreload 组件的头部内容 () 中,将 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 的呈现片段 (...),因为它支持路由、适配状态码页面重执行中间件,并兼容非 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(string identifier):异步读取指定 JS 属性的值。
- var valueFromDataPropertyAsync = await JSRuntime.GetValueAsync<int>(
- "jsInterop.testObject.num");
复制代码 - SetValueAsync(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(string identifier):同步读取指定 JS 属性的值。
- var inProcRuntime = ((IJSInProcessRuntime)JSRuntime);
- var valueFromDataProperty = inProcRuntime.GetValue<int>(
- "jsInterop.testObject.num");
复制代码 - SetValue(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>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>
-
- <label>
- Full Name
- <InputText @bind-Value="Model!.Customer.FullName" />
- </label>
- <ValidationMessage For="@(() => Model!.Customer.FullName)" />
-
- @* ... 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>
- ...
- +
- </head>
- <body>
- ...
- -
- +
- </body>
- </html>
复制代码 在项目文件中(.csproj),将 属性集添加到 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):- [/code]若要对独立应用中的其他 JS 模块进行指纹识别,请使用 Blazor WebAssembly 应用的项目文件()中的 .csproj 属性。
- [code]
复制代码 文件自动放置在导入映射中,并在解析 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——运行时中的新增功能
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |