关于 dotnet-multi-targeting
面向 .NET 多目标开发的指南,介绍如何让应用和库支持多个目标框架。通过 Polyfill、条件编译和 API 兼容性验证,帮助维护不同 TFM 间的一致支持。
主要功能
- 01PolySharp 语言特性存根
- 02BCL API Polyfill 指南
- 03条件编译策略
- 04多目标项目配置
- 05API 兼容性验证
适用场景
- 01将语言特性回移到较旧 TFM
- 02处理运行时或平台 API 差异
- 03维护支持多个 TFM 的库
- 04发布 NuGet 包前检查 API 兼容性
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ wshaddix/ dotnet-skills --skill dotnet-multi-targeting安装后在 Claude Code 中输入/dotnet-multi-targeting确认已加载。
详细介绍
dotnet-multi-targeting
.NET 多目标框架策略综合指南,采用填充优先的方法。此技能使用 [skill:dotnet-version-detection] 的结构化输出(TFM、C# 版本、预览标志),并提供可操作的指导,涵盖语言功能回移、运行时差异处理,以及跨目标框架验证 API 兼容性。
不在范围内: TFM 检测逻辑(由 [skill:dotnet-version-detection] 负责)、版本升级路线选择(参见 [skill:dotnet-version-upgrade])、特定平台的 UI 框架(MAUI、Blazor)、云部署配置。
交叉引用:[skill:dotnet-version-detection] 提供 TFM 解析和版本矩阵,[skill:dotnet-version-upgrade] 提供升级路线指导和迁移策略。
决策矩阵:填充还是条件编译
使用此矩阵,为最高和最低 TFM 之间的每种差异选择正确的策略。
| 差异类型 | 策略 | 适用场景 | 示例 |
|---|---|---|---|
| 语言/语法功能 | 填充(PolySharp) | 编译器需要属性/类型桩,才能在较旧的 TFM 上生成较新的语法 | required 修饰符、init 属性、net8.0 上的 SetsRequiredMembers |
| BCL API 新增项 | 如果可用则使用填充(SimonCropp/Polyfill),否则使用 #if | 较新的 BCL 类型或方法在较旧的 TFM 上缺失 | net8.0 上的 System.Threading.Lock、netstandard2.0 上的 Index/Range |
| 运行时行为差异 | 条件编译(#if)或适配器模式 | 无论编译方式如何,运行时行为都不同 | 仅限 net11.0 的运行时异步、不同的 GC 模式、SearchValues<T> 运行时优化 |
| 平台 API 差异 | 使用 [SupportedOSPlatform] 的条件编译 | API 仅存在于特定操作系统目标上 | Windows 注册表 API、Android 特有的 intents、iOS 钥匙串 |
决策流程:
- 编译时填充能否满足差异?使用 PolySharp 或 SimonCropp/Polyfill。
- 差异是否为缺少且没有可用填充的 BCL API?使用
#if编写特定于 TFM 的代码。 - 差异是否为运行时行为差异?使用
#if或适配器模式隔离不同的代码路径。 - 差异是否特定于平台?使用带有
[SupportedOSPlatform]属性的#if。
PolySharp(编译器合成的填充)
PolySharp 是一个源生成器,可合成 C# 编译器在面向较旧 TFM 时生成较新语言功能所需的属性和类型桩。它完全在编译时运行,不会添加任何运行时依赖项。
PolySharp 提供的内容
required修饰符支持(C# 11+)init属性访问器(C# 9+)SetsRequiredMembers属性CompilerFeatureRequired属性IsExternalInit类型CallerArgumentExpression属性StackTraceHidden属性UnscopedRef属性InterpolatedStringHandler属性ModuleInitializer属性- Index 和 Range 支持类型
设置
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net8.0;net10.0</TargetFrameworks>
<LangVersion>14</LangVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="PolySharp" Version="1.*">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
</ItemGroup>
</Project>
工作原理
PolySharp 会检测当前 TFM 缺少哪些填充类型,并且只为这些类型生成源代码。在原生支持 required 的 net10.0 上,生成器不会生成任何内容,实现零开销。
// This compiles on net8.0 WITH PolySharp installed,
// because PolySharp generates the required CompilerFeatureRequired
// and IsExternalInit types that the compiler needs.
public class UserProfile
{
public required string DisplayName { get; init; }
public required string Email { get; init; }
public string? Bio { get; set; }
}
PolySharp 的局限性
- PolySharp 仅提供编译器桩,不会回移运行时行为。
- 需要运行时支持的功能(例如运行时异步、
SearchValues<T>硬件加速)无法通过填充实现。 - 如果某项功能同时需要编译器属性和 BCL API(例如带有
Span<T>重载的集合表达式),可能需要同时使用 PolySharp 和 SimonCropp/Polyfill。
SimonCropp/Polyfill(BCL API 填充)
SimonCropp/Polyfill 提供源生成的较新 BCL API 实现,供较旧的 TFM 使用。与 PolySharp(提供编译器属性桩)不同,Polyfill 提供实际的方法和类型实现。
Polyfill 提供的内容
主要 Polyfill API(不完全列举):
System.Threading.Lock(C# 13 / net9.0+)String.Contains(char)、String.Contains(string, StringComparison)String.ReplaceLineEndings()HashCode结构体SkipLocalsInit特性TaskCompletionSource(非泛型)Stream.ReadExactly、Stream.ReadAtLeastMemory<T>和Span<T>扩展IReadOnlySet<T>接口- 各种 LINQ 扩展(
TryGetNonEnumeratedCount、DistinctBy、Chunk等)
设置
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net8.0;net10.0</TargetFrameworks>
<LangVersion>14</LangVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Polyfill" Version="7.*">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
</ItemGroup>
</Project>
使用示例
// System.Threading.Lock is a net9.0+ type.
// With Polyfill installed, this compiles on net8.0.
public class ThrottledProcessor
{
private readonly Lock _lock = new();
public void Process(string item)
{
lock (_lock)
{
// Lock provides better diagnostics than object-based locking
Console.WriteLine($"Processing: {item}");
}
}
}
结合使用 PolySharp 和 Polyfill
如需实现最大兼容性,请同时使用这两个包。它们互为补充,且不会冲突:
<ItemGroup>
<PackageReference Include="PolySharp" Version="1.*">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
<PackageReference Include="Polyfill" Version="7.*">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
</ItemGroup>
同时安装这两个包后,在较旧的 TFM 上,你既可以获得完整的语言功能支持(PolySharp),也可以获得 BCL API 回移(Polyfill)。
条件编译
当差异来自运行时行为,或来自无法在编译时进行 Polyfill 的平台 API 时,请使用条件编译(#if)。
基于 TFM 的条件编译
编译器会为每个 TFM 定义预处理器符号。使用 NET8_0_OR_GREATER 这类符号(从 .NET 5 起可用)进行版本范围检查:
public static class PerformanceHelper
{
#if NET10_0_OR_GREATER
// net10.0+ has optimized SearchValues with hardware acceleration
private static readonly SearchValues<char> s_vowels =
SearchValues.Create("aeiouAEIOU");
public static int CountVowels(ReadOnlySpan<char> text)
=> text.Count(s_vowels);
#else
// Fallback for net8.0: manual loop
public static int CountVowels(ReadOnlySpan<char> text)
{
int count = 0;
foreach (char c in text)
{
if ("aeiouAEIOU".Contains(c))
count++;
}
return count;
}
#endif
}
可用的预处理器符号
| 符号 | 为真时的条件 |
|---|---|
NET8_0 | 恰好是 net8.0 |
NET8_0_OR_GREATER | 是 net8.0 或更高版本 |
NET9_0_OR_GREATER | 是 net9.0 或更高版本 |
NET10_0_OR_GREATER | 是 net10.0 或更高版本 |
NET11_0_OR_GREATER | 是 net11.0 或更高版本 |
NETSTANDARD2_0 | 恰好是 netstandard2.0 |
NETSTANDARD2_0_OR_GREATER | 是 netstandard2.0 或更高版本 |
适合使用 #if 的情况
- 运行时行为差异: API 在两个 TFM 上都存在,但运行时行为不同(例如,
GC.Collect模式、HttpClient连接池行为)。 - 没有可用的 Polyfill: BCL API 不在 SimonCropp/Polyfill 的覆盖范围内,且无法通过存根实现。
- 性能关键路径: 你希望使用针对特定 TFM 优化的 API 路径(例如,
SearchValues<T>、FrozenDictionary<K,V>)。 - 平台 API: API 仅适用于特定的 OS 平台目标。
不适合使用 #if 的情况
- 语言语法功能(例如,
required、init):改用 PolySharp。 - 缺失的 BCL 方法,且有 Polyfill 可用(例如,
System.Threading.Lock):改用 SimonCropp/Polyfill。 - 用
#if代码块包裹整个文件——请改用 TFM 专用源文件(见下文)。
多目标 .csproj 模式
基本多目标设置
<PropertyGroup>
<TargetFrameworks>net8.0;net10.0</TargetFrameworks>
<LangVersion>14</LangVersion>
</PropertyGroup>
条件包引用
某些包仅在特定 TFM 上需要:
<ItemGroup>
<PackageReference Include="PolySharp" Version="1.*">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
<PackageReference Include="System.Text.Json" Version="9.*"
Condition="'$(TargetFramework)' == 'net8.0'" />
</ItemGroup>
TFM 专用源文件
对于较大的 TFM 专用代码块,请使用专用源文件,而不是 #if 代码块:
<ItemGroup>
<Compile Remove="Compatibility\**\*.cs" />
<Compile Include="Compatibility\Net8\**\*.cs"
Condition="'$(TargetFramework)' == 'net8.0'" />
<Compile Include="Compatibility\Net10\**\*.cs"
Condition="$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net10.0'))" />
</ItemGroup>
目录结构:
MyLibrary/
Compatibility/
Net8/
SearchValuesCompat.cs
Net10/
SearchValuesNative.cs
Services/
TextAnalyzer.cs # shared code, references interface
MyLibrary.csproj
平台专用 TFM
对于面向平台专用 TFM(MAUI、Uno)的项目:
<PropertyGroup>
<TargetFrameworks>net10.0;net10.0-android;net10.0-ios;net10.0-windows10.0.19041.0</TargetFrameworks>
</PropertyGroup>
<ItemGroup Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'android'">
<PackageReference Include="Xamarin.AndroidX.Core" Version="1.*" />
</ItemGroup>
通过 Directory.Build.props 共享属性
对于多项目解决方案,可集中配置多目标设置:
<Project>
<PropertyGroup>
<LangVersion>14</LangVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="PolySharp" Version="1.*">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
<PackageReference Include="Polyfill" Version="7.*">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
</ItemGroup>
</Project>
这样可确保解决方案中的所有项目使用相同的 Polyfill 设置。各个项目自行设置其 <TargetFrameworks>。
API 兼容性验证
发布面向多个 TFM 的 NuGet 包时,请验证公共 API 表面的一致性,并确保没有意外破坏使用者。
启用包验证
包验证会在执行 dotnet pack 时自动运行,并检查:
- 基线验证: 将当前包与之前的版本进行比较,以检测破坏性更改。
- 兼容框架验证: 确保一个 TFM 上可用的 API 在所有兼容的 TFM 上均可用。
<PropertyGroup>
<TargetFrameworks>net8.0;net10.0</TargetFrameworks>
<EnablePackageValidation>true</EnablePackageValidation>
<PackageValidationBaselineVersion>1.2.0</PackageValidationBaselineVersion>
</PropertyGroup>
API 兼容性工作流
步骤 1:在 .csproj 中启用验证
<PropertyGroup>
<EnablePackageValidation>true</EnablePackageValidation>
</PropertyGroup>
步骤 2:设置基线版本(针对现有包)
<PropertyGroup>
<PackageValidationBaselineVersion>2.0.0</PackageValidationBaselineVersion>
</PropertyGroup>
步骤 3:打包并检查
# Pack triggers validation automatically
dotnet pack --configuration Release
# Success: no output about compatibility issues
# Failure: error messages listing incompatible API changes
步骤 4:解读结果
| 结果 | 含义 | 操作 |
|---|---|---|
| 打包成功 | 所有 TFM 公开的 API 表面均兼容;相较基线没有重大变更 | 发布 |
CP0001 | 某个兼容 TFM 缺少类型 | 将该类型添加到此 TFM,或使用 #if 将其排除在公共 API 之外 |
CP0002 | 某个兼容 TFM 缺少成员 | 添加该成员,或在有意为之时抑制此警告 |
CP0003 | 相较基线版本存在重大变更 | 提升主版本号或还原此变更 |
PKV004 | 兼容的 TFM 具有不同的 API 表面 | 确保条件 API 是有意设计的 |
抑制已知差异
对于 TFM 之间有意存在的 API 差异,请使用抑制文件。启用抑制文件生成后,包验证可以自动生成抑制文件:
# Build with suppression-file generation enabled
dotnet pack /p:ApiCompatGenerateSuppressionFile=true
# Creates CompatibilitySuppressions.xml in the project directory
生成的 CompatibilitySuppressions.xml 包含针对性的抑制项:
<?xml version="1.0" encoding="utf-8"?>
<Suppressions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:MyLib.PerformanceHelper.CountVowels(System.ReadOnlySpan{System.Char})</Target>
<Left>lib/net8.0/MyLib.dll</Left>
<Right>lib/net10.0/MyLib.dll</Right>
</Suppression>
</Suppressions>
在 .csproj 中引用抑制文件(文件位于项目根目录时会自动引用):
<PropertyGroup>
<ApiCompatSuppressionFile>CompatibilitySuppressions.xml</ApiCompatSuppressionFile>
</PropertyGroup>
优先使用针对性的抑制文件,而不是宽泛的 <NoWarn>$(NoWarn);CP0002</NoWarn>——宽泛的抑制会掩盖实际问题。将抑制文件提交到源代码管理中,让审阅者能够了解有意存在的 API 差异。
独立的 ApiCompat 工具
用于无需打包即可执行验证的 CI 流水线:
# Install as a global tool
dotnet tool install -g Microsoft.DotNet.ApiCompat.Tool
# Global tool invocation (after install -g)
apicompat --left-assembly bin/Release/net8.0/MyLib.dll \
--right-assembly bin/Release/net10.0/MyLib.dll
# Or install as a local tool (preferred for CI reproducibility)
dotnet new tool-manifest # if .config/dotnet-tools.json doesn't exist
dotnet tool install Microsoft.DotNet.ApiCompat.Tool
# Local tool invocation
dotnet tool run apicompat --left-assembly bin/Release/net8.0/MyLib.dll \
--right-assembly bin/Release/net10.0/MyLib.dll
Agent 注意事项
-
不要使用
#if实现语言功能的 Polyfill。 如果差异在于编译器特性或语法功能(例如required、init、SetsRequiredMembers),请使用 PolySharp。为语言功能编写#if代码块会造成不必要的代码重复并增加维护负担。 -
不要在 Polyfill 包上省略
<PrivateAssets>all</PrivateAssets>。 PolySharp 和 SimonCropp/Polyfill 是仅供编译时使用的源生成器。若不设置PrivateAssets=all,Polyfill 类型会泄漏到包的依赖关系图中,并可能与使用者自己的 Polyfill 冲突。 -
不要在条件编译中硬编码 TFM 版本。 请使用
NET10_0_OR_GREATER这类范围符号,而不是精确的NET10_0符号。添加新 TFM 时,精确符号会造成问题(例如,net11.0 会跳过 net10.0 专用路径)。范围符号会自动涵盖未来的 TFM。 -
不要按 TFM 设置
<LangVersion>。 应一次性将其设为所有 TFM 所需的最高版本(例如,<LangVersion>14</LangVersion>)。PolySharp 和 Polyfill 会处理向后移植。按 TFM 设置 LangVersion 会导致难以理解的语法错误。 -
多目标 NuGet 包不要跳过
EnablePackageValidation。 若不启用此项,不同 TFM 可能会意外公开不同的 API 表面,导致使用者切换 TFM 时构建失败。 -
不要在 MSBuild 条件中使用
$(TargetFramework)字符串相等判断来检查范围。 请使用$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net10.0'))执行向前兼容的范围检查。字符串相等判断(例如== 'net10.0')会漏掉 net11.0 及更高版本。 -
不要重新实现 TFM 检测。 此技能使用 [skill:dotnet-version-detection] 提供的结构化输出。绝不要通过解析
.csproj文件来确定 TFM;请使用检测技能的输出(TFM、C# 版本、SDK 版本、警告)。 -
不要认为 Polyfill 能涵盖运行时行为。 PolySharp 和 Polyfill 提供编译时存根和源生成的实现。需要运行时更改的功能(例如 runtime-async、GC 改进、JIT 优化)无法通过 Polyfill 实现——请为这些功能使用
#if。 -
平台目标不要使用特定版本的 TFM Glob。 在文档和工具中使用
net*-android模式匹配(与版本无关),而不是net10.0-android,以避免用户面向不同 .NET 版本时出现误报。
前置条件
- .NET 8.0+ SDK(多目标需要安装版本最高的目标 SDK)
PolySharpNuGet 包(用于语言功能的多态填充)- Simon Cropp 提供的
PolyfillNuGet 包(用于 BCL API 的多态填充) Microsoft.DotNet.ApiCompat.Tool(可选,用于独立的 API 兼容性检查)- [skill:dotnet-version-detection] 的输出(TFM、C# 版本、SDK 版本)
参考资料
最后验证时间:2026-02-12