ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

ScottPlot .NET 10 迁移实战:四个沙箱项目的 AI 排障全记录

ScottPlot .NET 10 迁移实战:四个沙箱项目的 AI 排障全记录
  • 数据可视化
  • 图表库

【免费下载链接】ScottPlot

Interactive plotting library for .NET

项目地址:https://gitcode.com/gh_mirrors/sc/ScottPlot
点击查看免费下载

导读

本文基于 ScottPlot 仓库dev/ai-prompts目录下记录的 AI 协作会话,完整还原了 ScottPlot 团队将 Blazor、MAUI、Uno 等沙箱示例项目从 .NET 8/9 升级到 .NET 10 时遭遇的四个典型运行时/构建问题及其修复全过程。读者将掌握 .NET 10 中 Blazor WebAssembly 的模块加载新机制、独立 WASM 应用的静态资源清单要求、MAUI Windows 打包模式与 Windows SDK 投影包的正确配置,以及 WinRT.Runtime 版本冲突的排查思路——这些都是 .NET 大版本升级中高频出现的兼容性坑点,可直接迁移到自己的项目迁移实践中。

文档背景:仓库中 AI Prompts 说明 建议在使用 AI 工具执行直接修改代码的复杂任务时,以YYYY-MM-DD [GitHubUsername] description of feature.md的命名格式记录 AI 对话,案例原文 即是这一实践的典型样本。


一、为何要为 AI 协作保留对话记录

1.1 文档规范

dev/ai-prompts/README.md 给出了非常简短但明确的三条约定:

  • 当使用 AI 工具执行直接修改代码的复杂任务时,建议记录 AI 对话;
  • 文件命名格式为YYYY-MM-DD [GitHubUsername] description of feature.md;
  • Markdown 格式不限,自由组织即可。

这套约定的价值在于:AI 排障过程天然包含"报错信息 → 根因分析 → 修改 → 复现验证"的完整链路,这些信息散落在终端输出与聊天记录中极易丢失,而按日期与作者归档后,就形成了一份可回溯的项目级迁移知识库——当下次升级或回滚时,无需重新摸索。

1.2 本文涉及的案例范围

案例文档 记录了四个独立案例,全部围绕 ScottPlot 5 的沙箱示例工程(位于src/ScottPlot5/ScottPlot5 Sandbox/与src/ScottPlot5/ScottPlot5 Demos/):

案例项目升级路径核心问题
1Sandbox.Blazor(含 Blazor WebAssembly 控制库).NET 9 → .NET 10WASM 动态模块加载失败 + SkiaSharp 原生库 DLL 异常
2Sandbox.Blazor.WebAssembly(独立 WASM 应用).NET 9 → .NET 10静态资源清单文件缺失
3Sandbox.Maui.NET 8 → .NET 10COM 类未注册异常 + MainPage 过时警告
4Sandbox.UnoPlatform.NET 8 → .NET 10WinRT.Runtime 版本冲突

二、案例一:Blazor WebAssembly 升级 .NET 10 的模块加载与 SkiaSharp 问题

2.1 问题现象

Sandbox.Blazor 从 .NET 9 升级到 .NET 10 后,应用启动、页面加载前即抛出运行时异常:WASM 运行时无法加载动态导入的模块,错误指向类似Failed to fetch dynamically imported module: https://localhost:7014/0的路径。

2.2 根因一:程序包版本错配

初步检查发现,工程目标框架已改为net10.0,但引用的Microsoft.AspNetCore.Components.WebAssembly与Microsoft.AspNetCore.Components.WebAssembly.DevServer仍是 .NET 8 时代的8.0.10。运行时与程序集版本不一致会直接导致 WASM 初始化阶段失败。

修复方式:将两个包统一升级为10.0.0(.NET 10 处于 Preview 阶段时使用10.0.0-*)。当前仓库中的 Sandbox.Blazor.csproj 正是修复后的状态:

<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="10.0.0" /> <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="10.0.0" PrivateAssets="all" />

2.3 根因二:脚本加载模式未随运行时升级

版本对齐后异常依旧,说明还有第二个问题。.NET 10 中 Blazor WebAssembly 运行时改用ES6 模块(module)加载机制,而index.html仍沿用 .NET 8 时代的旧脚本引入方式,导致运行时以新机制取模块时路径落空。

修复方式:将脚本标签改为 module 模式。当前 Sandbox.Blazor/wwwroot/index.html 中的写法即为修复后的标准形态:

<script src="_framework/blazor.webassembly.js" type="module"></script>

补充:对照当前工程,Program.cs 使用标准的WebAssemblyHostBuilder.CreateDefault(args)初始化,根组件挂载在#app元素上,与index.html中的<div id="app">一一对应,这是 WASM 启动链路的正确形态。

2.4 根因三:SkiaSharp WebAssembly 原生绑定缺失

应用能走得更远了,但随即抛出System.DllNotFoundException in SkiaSharp.dll与System.TypeInitializationException(SkiaSharp.SKTypeface类型初始化失败)。ScottPlot 5 的 Blazor 控件底层依赖 SkiaSharp 在浏览器中的WASM 原生绑定,这类库需要显式保证原生资产不被裁剪、正确随包发布。

修复要点集中在 Sandbox.Blazor.csproj:

<WasmNativeStrip>false</WasmNativeStrip> ... <PackageReference Include="SkiaSharp.Views.Blazor" Version="3.119.0" />
  • WasmNativeStrip=false:阻止链接器剥离 SkiaSharp 的 WASM 原生库(这是该问题最关键的一处配置,剥离后运行时找不到原生符号就会抛 DLL 异常);
  • 显式引用SkiaSharp.Views.Blazor:确保 WebAssembly 原生绑定被正确纳入发布产物。

同时,控制库 ScottPlot.Blazor.csproj 将Microsoft.AspNetCore.Components.Web改为按目标框架条件化引用:net10.0用10.0、net8.0用8.0,从而让同一控制库同时兼容两个 TFM。

2.5 底层原理印证:SkiaSharp 在 Blazor 中的渲染链路

从源码看,BlazorPlotBase.cs 是 Blazor 控件的核心基类,它实现IPlotControl,内部通过UserInputProcessor将浏览器的指针事件(OnPointerMoved、OnPointerPressed、OnPointerWheelChanged等)统一转换为 ScottPlot 的交互指令。这意味着:所有交互能力都建立在 SkiaSharp 能在 WASM 环境中正常初始化并完成绘制的基础之上。一旦原生绑定缺失导致SKTypeface初始化失败,整个控件在页面加载阶段就会崩溃——这也解释了为什么 SkiaSharp 原生资产配置是本次迁移的咽喉环节。


三、案例二:独立 WASM 应用的静态资源清单缺失

3.1 问题现象

Sandbox.Blazor.WebAssembly项目升级到 .NET 10 后启动即抛:

System.InvalidOperationException: The static resources manifest file '...\bin\Debug\net10.0\Sandbox.Blazor.WebAssembly.staticwebassets.endpoints.json' was not found.

3.2 根因:.NET 10 对独立 WASM 应用的破坏性变更

这是 .NET 10 针对独立(standalone)Blazor WebAssembly 应用的已知破坏性变更:WebAssemblyHostBuilder.CreateDefault(args)启动时会读取staticwebassets.endpoints.json清单文件,但Microsoft.NET.Sdk.BlazorWebAssemblySDK 默认不生成该文件。

对比一下两种宿主形态:

  • 宿主型(hosted)WASM 应用:由 ASP.NET Core 服务端工程在构建时生成该清单,因此无此问题;
  • 独立型 WASM 应用:没有服务端工程,清单默认缺失,升级后必然报错。

3.3 修复:打开清单生成开关

在项目文件中加入 MSBuild 属性即可:

<GenerateStaticWebAssetsEndpointsManifest>true</GenerateStaticWebAssetsEndpointsManifest>

该属性指示 SDK 在构建时产出清单文件。干净重建后,bin\Debug\net10.0\目录下就会出现Sandbox.Blazor.WebAssembly.staticwebassets.endpoints.json,运行时异常随之消失。

当前仓库中该修复已落地在两处独立 WASM 工程:

  • Sandbox.Blazor.WebAssembly.csproj(Microsoft.NET.Sdk.BlazorWebAssembly,targetingnet10.0)
  • Sandbox.Blazor.csproj(同为独立 WASM 应用,同样命中此问题并应用了相同修复)

排查提示:若你的独立 WASM 工程升级 .NET 10 后遇到 "manifest file ... was not found" 异常,优先检查 csproj 中是否设置了GenerateStaticWebAssetsEndpointsManifest=true;而宿主型WASM 应用通常不需要手动设置。


四、案例三:MAUI 沙箱的 COM 异常、包类型与过时 API

4.1 问题现象

Sandbox.Maui 从 .NET 8 升级到 .NET 10 后,Windows 桌面启动时抛出:

System.Runtime.InteropServices.COMException: Class not registered (0x80040154 (REGDB_E_CLASSNOTREG))

且用户明确要求:使用最新 .NET 10 目标与包,但不要使用 .NET 11 Preview。

4.2 根因一:过时的 WindowsSdkPackageVersion 重复属性

对照解决方案中其他 Windows 目标工程(如Sandbox.WinUI.Desktop、ScottPlot.Maui)可以发现:它们都不显式设置WindowsSdkPackageVersion,而是交由 .NET 10 SDK 自动解析正确的 Windows SDK 投影包版本。

而 Sandbox.Maui 的工程文件中残留了两处 .NET 8 时代的WindowsSdkPackageVersion(值形如10.0.19041.34、10.0.19041.38),这些旧投影包与 .NET 10 运行时不兼容,正是REGDB_E_CLASSNOTREG的直接诱因。

修复:删除全部WindowsSdkPackageVersion属性,让 SDK 自动解析。从当前 Sandbox.Maui.csproj 可以看到,工程只保留了TargetFrameworks、SupportedOSPlatformVersion、TargetPlatformMinVersion,已无任何WindowsSdkPackageVersion残留。

4.3 根因二:日志包版本未对齐

Microsoft.Extensions.Logging.Debug仍停留在8.0.0,与net10.0目标框架不匹配。当前仓库中已更新为 Version="10.0.0"。

4.4 根因三:打包模式引发 COM 类未注册

在 Visual Studio 中运行仍报Class not registered (0x80040154),且错误发生在 Windows App SDK 初始化阶段(DeploymentManager.Initialize)。原因在于应用默认按打包(MSIX)应用运行,但缺少包注册信息,导致 COM 类未被注册。

参考解决方案中 Sandbox.WinUI.Desktop.csproj 已有的做法,为 MAUI 工程加入:

<WindowsPackageType>None</WindowsPackageType>

该属性指示构建产出非打包(unpackaged)Windows 应用,完全绕过 MSIX 包注册流程,从根源上消除REGDB_E_CLASSNOTREG。这一配置已固化在 Sandbox.Maui.csproj 中。

4.5 附带处理:MainPage 过时警告

升级后构建伴随警告 CS0618:

'Application.MainPage.set' is obsolete:该属性已弃用,应通过重写Application.CreateWindow完成初始化;多窗口场景下改用Windows[0].Page。

修复方式是把MainPage = new AppShell()改为重写CreateWindow。当前 App.xaml.cs 即修复后的形态:

protected override Window CreateWindow(IActivationState? activationState) { return new Window(new AppShell()); }

说明:MainPage的废弃与WindowsPackageType是相互独立的两个问题——前者是 .NET 10 MAUI 的 API 迁移要求,后者解决打包模式导致的 COM 异常,两者需要分别处理。


五、案例四:Uno 沙箱的 WinRT.Runtime 版本冲突

5.1 问题现象

Sandbox.UnoPlatform 从 .NET 8 升级到 .NET 10 后构建报错:

Assembly 'ScottPlot.WinUI' ... uses 'WinRT.Runtime, Version=2.2.0.0' which has a higher version than referenced assembly 'WinRT.Runtime' with identity 'WinRT.Runtime, Version=2.0.0.0'

5.2 根因:传递依赖带来的版本降级

ScottPlot.WinUI通过其Microsoft.WindowsAppSDK 1.5.240802000依赖编译时使用的是WinRT.Runtime2.2.0.0;而 Uno SDK 5.3.90 默认将WinRT.Runtime解析到较旧的2.0.0.0。NuGet 解析结果低于程序集实际编译版本,构建即报版本冲突。

5.3 修复:显式固定 CsWinRT 版本

WinRT.Runtime程序集由Microsoft.Windows.CsWinRT包提供,因此在 Uno 工程中显式引用Microsoft.Windows.CsWinRT 2.2.0,即可让 NuGet 把WinRT.Runtime解析到 2.2.0.0,与ScottPlot.WinUI的编译基准对齐。当前 Sandbox.UnoPlatform.csproj 已包含该引用:

<PackageReference Include="Microsoft.Windows.CsWinRT" Version="2.2.0" />

5.4 关联佐证:ScottPlot.WinUI 的 Windows 依赖面

对照 ScottPlot.WinUI.csproj:Windows 目标框架下引用Microsoft.WindowsAppSDK 1.5.240802000、Microsoft.Windows.SDK.BuildTools 10.0.22621.756及SkiaSharp.Views.WinUI 3.119.0,并显式钉住Microsoft.Windows.SDK.NET.Ref的RuntimeFrameworkVersion与TargetingPackVersion(10.0.22000.27)。可以推断,WinAppSDK 依赖面越复杂,下游工程(如 Uno)越容易出现WinRT.Runtime之类的传递版本错位,显式引用 CsWinRT 是对这类冲突最直接的解决办法。


六、可复用的迁移排查清单

综合四个案例,将 .NET 8/9 → .NET 10 迁移的检查要点汇总如下,供后续项目升级时逐项核对:

6.1 Blazor WebAssembly

  • 升级Microsoft.AspNetCore.Components.WebAssembly与.DevServer至10.0.0(且与 TFM 一致);
  • index.html中脚本改为<script src="_framework/blazor.webassembly.js" type="module"></script>;
  • 依赖 SkiaSharp 等含 WASM 原生资产的库时,设置WasmNativeStrip=false并显式引用对应 Views 包;
  • 独立 WASM 应用必须设置GenerateStaticWebAssetsEndpointsManifest=true(宿主型应用通常无需)。

6.2 MAUI / WinUI(Windows 目标)

  • 删除遗留的WindowsSdkPackageVersion旧值,交由 SDK 自动解析;
  • 日志、依赖等通用包版本与net10.0对齐;
  • 桌面运行遇 COM 类未注册时,考虑<WindowsPackageType>None</WindowsPackageType>改为非打包模式;
  • 用CreateWindow重写替代已过时的MainPage赋值。

6.3 Uno / WinUI 多目标工程

  • 出现WinRT.Runtime版本冲突时,显式引用Microsoft.Windows.CsWinRT到与上游程序集一致的版本;
  • 核对 WinAppSDK、Windows SDK BuildTools 等 Windows 依赖面版本是否在传递解析中被降级。

6.4 过程规范

  • 将每个报错与修复按YYYY-MM-DD [用户名] 功能描述.md命名归档至dev/ai-prompts/,形成团队可复用的迁移知识库。

七、总结

这四个案例揭示了 .NET 大版本升级中两类典型的"隐形断裂":一类是运行时机制的变更(如 WASM 从脚本加载转向 ES6 模块、CreateDefault开始强依赖静态资源清单、MAUI 废弃MainPage),另一类是版本解析的漂移(程序包停留在旧大版本、旧投影包残留、传递依赖降级)。两者的共同特征是:编译未必失败,但启动或运行阶段才会暴露。ScottPlot 仓库以 AI 对话形式将这些排障过程固化在 2025-03-28 案例文档 中,并在 Sandbox.Blazor.csproj、Sandbox.Maui.csproj、Sandbox.UnoPlatform.csproj 等工程文件中留下了最终修复形态,既是迁移实践的直接参考,也是排查同类问题时最可信的对照样本。

  • 数据可视化
  • 图表库

【免费下载链接】ScottPlot

Interactive plotting library for .NET

项目地址:https://gitcode.com/gh_mirrors/sc/ScottPlot
点击查看免费下载

相关推荐

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进