工程

在 VS Code 中使用 .NET Framework 与多目标框架:每个框架,C# 与 F# 皆可

在 net48 处于活动状态时显示

打开几乎任何一个正经的 .NET 库,你都会看到类似这样的一行:

<TargetFrameworks>net462;net48;netstandard2.0;net8.0;net10.0</TargetFrameworks>

一个项目文件,五个框架,还有只在其中一个框架下才会编译的 #if NETFRAMEWORK 代码孤岛。大多数编辑器会悄悄只选一个框架,把其余的当作不存在——于是,你正在为 .NET Framework 客户调试的那段代码变成灰色、得不到分析,还满是幻影错误。

SharpLsp 0.23 为项目声明的每个框架都提供完整的体验,F# 与 C# 完全一样。 下面看看这在实践中意味着什么。

切换框架,代码随之改变

当聚焦的文件属于多目标项目时,状态栏会显示它当前的活动框架,比如 net48。点击它(或运行 SharpLsp: Select Target Framework)并选择另一个框架。

切换作用于整个项目,并且两种语言同时生效:

默认值是 <TargetFrameworks> 的第一项,与 Visual Studio 一致。

C#:每个框架一个 Roslyn 上下文

MSBuildWorkspace 为每个框架提供一个 Roslyn 项目——Csv(net48)、Csv(net8.0)——因此每个文档在每个框架下都有一个上下文,每个请求都从活动上下文作答。

多目标框架最容易出问题的是项目范围的查询。查找引用、CodeLens 计数、实现、调用层级和类型层级以及死代码分析,都会搜索每个项目的活动框架,并且每个位置只报告一次。同一处使用绝不会因为框架不同、或经由属性访问器而被重复计数。

F#:按框架使用 MSBuild 的选项

F# 得到同样的待遇,而且编译器选项不是猜出来的。对于每个框架,SharpLsp 都会向 MSBuild 请求该项目真实的设计时编译:

dotnet msbuild  -p:TargetFramework=net48 -p:DesignTimeBuild=true
  -p:SkipCompilerExecution=true -p:ProvideCommandLineArgs=true ...
  -getItem:FscCommandLineArgs -getItem:ReferencePathWithRefAssemblies

因此 net48 会使用 --targetprofile:mscorlib、4.8 引用程序集以及 NETFRAMEWORK;NET48;…_OR_GREATER 进行编译——源文件顺序、通配符、条件和 Directory.Build.* 都来自 MSBuild,而不是我们的推测。每个框架大约耗时一秒:第一个在加载时构建,其他的在首次切换时构建,然后保留下来。

有两个细节比看上去更重要:

引用其他 F# 项目的 F# 项目,会按 MSBuild 为该引用实际选定的框架,在内存中读取被引用的项目。

在任何操作系统上都能分析

SDK 会自动引用 Microsoft.NETFramework.ReferenceAssemblies,因此在 macOS 和 Linux 上同样可以分析 .NET Framework 代码——net48 的补全、导航和诊断在 MacBook 上也能用。运行则是另一回事:

目标 分析 生成 运行 / 测试 调试
net462–net481 所有系统 所有系统 仅 Windows 拒绝,并说明原因
netstandard2.0/2.1 所有系统 所有系统 从不——这是库 —
net8.0+ 所有系统 所有系统 需要对应运行时 netcoredbg

缺少运行时的框架——例如只装了 8 和 10 的机器上的 net9.0——会被点名报告为无法运行,绝不会显示成失败或缺失的测试。

懂框架的测试资源管理器

测试资源管理器现在能理解多目标测试项目,VSTest 和 MTP 都支持,C# 和 F# 都适用:

调试:对桌面 CLR 坦诚相告

netcoredbg 能调试 .NET,但不能调试 .NET Framework 的桌面 CLR。一个苦等永远不会附加的调试器的测试宿主只会挂起,所以 SharpLsp 不会让这种事发生:

在真实仓库上验证

对于这样的功能,人工构造的测试夹具远远不够。因此,一个 Windows CI 分片会按固定提交克隆三个真实的多目标仓库,并通过真实的扩展驱动它们:

仓库 语言 目标
kekyo/GitReader F# 和 C# net35…net481、netstandard1.6–2.1、现代 .NET
JoshClose/CsvHelper C# net462;net47;net48;netstandard2.0;2.1;net8.0;net9.0
NLog/NLog C# net35;net46;netstandard2.0;netstandard2.1

每个仓库都能加载,能在 #if NETFRAMEWORK 代码中响应语言请求,能发现它的 .NET Framework 测试程序集,并报告按框架划分的结果。

动手试试

从 VS Code Marketplace 或 Open VSX 更新到 SharpLsp 0.23 或更高版本,打开一个多目标解决方案,然后点击状态栏中的框架。

发现哪个项目表现不对?请提交 issue——一行能让 SharpLsp 出错的真实 <TargetFrameworks>,正是我们最想看到的。