mcpField
D
dotnet-multi-targeting

dotnet-multi-targeting

github.com/wshaddix开发8669 次安装35 周前更新
代码仓库

关于 dotnet-multi-targeting

面向 .NET 多目标开发的指南,介绍如何让应用和库支持多个目标框架。通过 Polyfill、条件编译和 API 兼容性验证,帮助维护不同 TFM 间的一致支持。

主要功能

  1. 01PolySharp 语言特性存根
  2. 02BCL API Polyfill 指南
  3. 03条件编译策略
  4. 04多目标项目配置
  5. 05API 兼容性验证

适用场景

  1. 01将语言特性回移到较旧 TFM
  2. 02处理运行时或平台 API 差异
  3. 03维护支持多个 TFM 的库
  4. 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 钥匙串

决策流程:

  1. 编译时填充能否满足差异?使用 PolySharp 或 SimonCropp/Polyfill。
  2. 差异是否为缺少且没有可用填充的 BCL API?使用 #if 编写特定于 TFM 的代码。
  3. 差异是否为运行时行为差异?使用 #if 或适配器模式隔离不同的代码路径。
  4. 差异是否特定于平台?使用带有 [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.ReadAtLeast
  • Memory<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 的情况
  1. 运行时行为差异: API 在两个 TFM 上都存在,但运行时行为不同(例如,GC.Collect 模式、HttpClient 连接池行为)。
  2. 没有可用的 Polyfill: BCL API 不在 SimonCropp/Polyfill 的覆盖范围内,且无法通过存根实现。
  3. 性能关键路径: 你希望使用针对特定 TFM 优化的 API 路径(例如,SearchValues<T>、FrozenDictionary<K,V>)。
  4. 平台 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 注意事项

  1. 不要使用 #if 实现语言功能的 Polyfill。 如果差异在于编译器特性或语法功能(例如 required、init、SetsRequiredMembers),请使用 PolySharp。为语言功能编写 #if 代码块会造成不必要的代码重复并增加维护负担。

  2. 不要在 Polyfill 包上省略 <PrivateAssets>all</PrivateAssets>。 PolySharp 和 SimonCropp/Polyfill 是仅供编译时使用的源生成器。若不设置 PrivateAssets=all,Polyfill 类型会泄漏到包的依赖关系图中,并可能与使用者自己的 Polyfill 冲突。

  3. 不要在条件编译中硬编码 TFM 版本。 请使用 NET10_0_OR_GREATER 这类范围符号,而不是精确的 NET10_0 符号。添加新 TFM 时,精确符号会造成问题(例如,net11.0 会跳过 net10.0 专用路径)。范围符号会自动涵盖未来的 TFM。

  4. 不要按 TFM 设置 <LangVersion>。 应一次性将其设为所有 TFM 所需的最高版本(例如,<LangVersion>14</LangVersion>)。PolySharp 和 Polyfill 会处理向后移植。按 TFM 设置 LangVersion 会导致难以理解的语法错误。

  5. 多目标 NuGet 包不要跳过 EnablePackageValidation。 若不启用此项,不同 TFM 可能会意外公开不同的 API 表面,导致使用者切换 TFM 时构建失败。

  6. 不要在 MSBuild 条件中使用 $(TargetFramework) 字符串相等判断来检查范围。 请使用 $([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net10.0')) 执行向前兼容的范围检查。字符串相等判断(例如 == 'net10.0')会漏掉 net11.0 及更高版本。

  7. 不要重新实现 TFM 检测。 此技能使用 [skill:dotnet-version-detection] 提供的结构化输出。绝不要通过解析 .csproj 文件来确定 TFM;请使用检测技能的输出(TFM、C# 版本、SDK 版本、警告)。

  8. 不要认为 Polyfill 能涵盖运行时行为。 PolySharp 和 Polyfill 提供编译时存根和源生成的实现。需要运行时更改的功能(例如 runtime-async、GC 改进、JIT 优化)无法通过 Polyfill 实现——请为这些功能使用 #if。

  9. 平台目标不要使用特定版本的 TFM Glob。 在文档和工具中使用 net*-android 模式匹配(与版本无关),而不是 net10.0-android,以避免用户面向不同 .NET 版本时出现误报。


前置条件

  • .NET 8.0+ SDK(多目标需要安装版本最高的目标 SDK)
  • PolySharp NuGet 包(用于语言功能的多态填充)
  • Simon Cropp 提供的 Polyfill NuGet 包(用于 BCL API 的多态填充)
  • Microsoft.DotNet.ApiCompat.Tool(可选,用于独立的 API 兼容性检查)
  • [skill:dotnet-version-detection] 的输出(TFM、C# 版本、SDK 版本)

参考资料

最后验证时间:2026-02-12

评论

支持 Markdown