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. 04NuGet パッケージ公開前の 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)、クラウドへのデプロイ設定。

相互参照:TFM の解決とバージョン マトリックスについては [skill:dotnet-version-detection]、アップグレード レーンのガイダンスと移行戦略については [skill:dotnet-version-upgrade] を参照してください。


判断マトリックス:ポリフィルと条件付きコンパイル

最高および最低の TFM 間にあるギャップの種類ごとに、適切な戦略を選択するにはこのマトリックスを使用してください。

ギャップの種類戦略使用する状況例
言語/構文機能ポリフィル(PolySharp)古い TFM で新しい構文を出力するために、コンパイラが属性/型のスタブを必要とする場合net8.0 での required 修飾子、init プロパティ、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] を使った条件付きコンパイル特定の OS ターゲットでのみ API が利用できる場合Windows レジストリ API、Android 固有のインテント、iOS キーチェーン

判断フロー:

  1. コンパイル時のポリフィルでギャップを解消できますか?PolySharp または SimonCropp/Polyfill を使用してください。
  2. ギャップは、利用可能なポリフィルがない BCL API の不足ですか?TFM 固有のコードで #if を使用してください。
  3. ギャップはランタイムの動作の違いですか?異なるコードパスを切り分けるために、#if またはアダプター パターンを使用してください。
  4. ギャップはプラットフォーム固有ですか?[SupportedOSPlatform] 属性とともに #if を使用してください。

PolySharp(コンパイラー合成ポリフィル)

PolySharp は、古い TFM をターゲットにする際に、新しい言語機能を出力するために C# コンパイラーが必要とする属性および型のスタブを合成するソース ジェネレーターです。すべてコンパイル時に動作し、ランタイム依存関係は追加されません。

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 が提供する機能

主なポリフィル対象 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 を利用できます。


条件付きコンパイル

不足している機能がランタイムの動作の違い、またはコンパイル時にポリフィルできないプラットフォーム 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_0net8.0 と完全に一致
NET8_0_OR_GREATERnet8.0 またはそれ以降のバージョン
NET9_0_OR_GREATERnet9.0 またはそれ以降のバージョン
NET10_0_OR_GREATERnet10.0 またはそれ以降のバージョン
NET11_0_OR_GREATERnet11.0 またはそれ以降のバージョン
NETSTANDARD2_0netstandard2.0 と完全に一致
NETSTANDARD2_0_OR_GREATERnetstandard2.0 またはそれ以降
#if を使用すべき場合
  1. ランタイムの動作の違い: API は両方の TFM に存在しますが、ランタイムでの動作が異なります(例:GC.Collect のモード、HttpClient の接続プールの動作)。
  2. 利用可能なポリフィルがない: BCL API が SimonCropp/Polyfill の対象ではなく、スタブ化もできません。
  3. パフォーマンスが重要な処理: TFM 固有の最適化された API パスを使用したい場合です(例:SearchValues<T>、FrozenDictionary<K,V>)。
  4. プラットフォーム API: 特定の OS プラットフォームを対象とする場合にのみ利用できる API です。
#if を使用すべきでない場合
  • 言語構文機能(例:required、init):代わりに PolySharp を使用してください。
  • ポリフィルがある BCL メソッドの不足(例:System.Threading.Lock):代わりに SimonCropp/Polyfill を使用してください。
  • ファイル全体を #if ブロックで囲む場合。代わりに TFM 固有のソースファイルを使用してください(以下を参照)。

複数の 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>

これにより、ソリューション内のすべてのプロジェクトで同じポリフィル設定が共有されます。各プロジェクトでは、それぞれの <TargetFrameworks> を設定します。


API 互換性の検証

複数の TFM を対象とする NuGet パッケージを公開する際は、公開 API の構成が一貫していること、および利用側を誤って破壊していないことを検証してください。

EnablePackageValidation

パッケージの検証は 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

エージェントが注意すべき点

  1. 言語機能のポリフィルに #if を使用しないでください。 不足しているのがコンパイラー属性や構文機能(例:required、init、SetsRequiredMembers)の場合は、PolySharp を使用してください。言語機能に #if ブロックを使うと、不要なコードの重複とメンテナンス負荷が生じます。

  2. ポリフィルパッケージで <PrivateAssets>all</PrivateAssets> を省略しないでください。 PolySharp と SimonCropp/Polyfill は、コンパイル時のみ使用するソースジェネレーターです。PrivateAssets=all がないと、ポリフィルの型がパッケージの依存関係グラフに漏れ出し、利用者独自のポリフィルと競合する可能性があります。

  3. 条件付きコンパイルで TFM のバージョンをハードコードしないでください。 NET10_0 のような完全一致のシンボルではなく、NET10_0_OR_GREATER のような範囲を表すシンボルを使用してください。完全一致のシンボルは、新しい TFM が追加されると動作しなくなります(例:net11.0 では net10.0 専用の処理がスキップされます)。範囲を表すシンボルなら、今後追加される TFM も自動的に対象になります。

  4. TFM ごとに <LangVersion> を設定しないでください。 すべての TFM で必要となる最も高いバージョンを一度だけ設定します(例:<LangVersion>14</LangVersion>)。バックポートは PolySharp と Polyfill が処理します。TFM ごとに LangVersion を設定すると、分かりにくい構文エラーの原因になります。

  5. 複数の TFM を対象とする NuGet パッケージで EnablePackageValidation を省略しないでください。 これを有効にしないと、TFM ごとに異なる API サーフェスを誤って公開してしまい、利用者が TFM を切り替えたときにビルドエラーが発生する可能性があります。

  6. MSBuild 条件で範囲チェックに $(TargetFramework) の文字列一致を使用しないでください。 将来のバージョンとの互換性を保った範囲チェックには、$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net10.0')) を使用してください。文字列一致(例:== 'net10.0')では、net11.0 以降が対象になりません。

  7. TFM の検出を再実装しないでください。 このスキルは [skill:dotnet-version-detection] の構造化された出力を使用します。TFM を判断するために .csproj ファイルを解析してはいけません。検出スキルの出力(TFM、C# バージョン、SDK バージョン、警告)を使用してください。

  8. ポリフィルがランタイムの動作もカバーすると考えないでください。 PolySharp と Polyfill は、コンパイル時のスタブとソース生成による実装を提供します。ランタイムの変更が必要な機能(例:runtime-async、GC の改善、JIT の最適化)はポリフィルできないため、こうした機能には #if を使用してください。

  9. プラットフォームターゲットにバージョン固有の TFM グロブを使用しないでください。 利用者が異なる .NET バージョンをターゲットにした場合の誤検出を避けるため、ドキュメントやツールでは net10.0-android ではなく、バージョンに依存しない net*-android パターンマッチングを使用してください。


前提条件

  • .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

コメント