dotnet-multi-targeting について
複数のターゲットフレームワークに対応する .NET アプリやライブラリ向けのガイドです。Polyfill、条件付きコンパイル、API 互換性検証を通じて、TFM 間のサポートを維持する方法を説明します。
主な機能
- 01PolySharp による言語機能スタブ
- 02BCL API の Polyfill ガイダンス
- 03条件付きコンパイルの方針
- 04マルチターゲットプロジェクト設定
- 05API 互換性の検証
活用シーン
- 01古い TFM への言語機能のバックポート
- 02ランタイムやプラットフォーム API 差異への対応
- 03複数 TFM 対応ライブラリの保守
- 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 キーチェーン |
判断フロー:
- コンパイル時のポリフィルでギャップを解消できますか?PolySharp または SimonCropp/Polyfill を使用してください。
- ギャップは、利用可能なポリフィルがない BCL API の不足ですか?TFM 固有のコードで
#ifを使用してください。 - ギャップはランタイムの動作の違いですか?異なるコードパスを切り分けるために、
#ifまたはアダプター パターンを使用してください。 - ギャップはプラットフォーム固有ですか?
[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.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 を利用できます。
条件付きコンパイル
不足している機能がランタイムの動作の違い、またはコンパイル時にポリフィルできないプラットフォーム 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の接続プールの動作)。 - 利用可能なポリフィルがない: BCL API が SimonCropp/Polyfill の対象ではなく、スタブ化もできません。
- パフォーマンスが重要な処理: TFM 固有の最適化された API パスを使用したい場合です(例:
SearchValues<T>、FrozenDictionary<K,V>)。 - プラットフォーム 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
エージェントが注意すべき点
-
言語機能のポリフィルに
#ifを使用しないでください。 不足しているのがコンパイラー属性や構文機能(例:required、init、SetsRequiredMembers)の場合は、PolySharp を使用してください。言語機能に#ifブロックを使うと、不要なコードの重複とメンテナンス負荷が生じます。 -
ポリフィルパッケージで
<PrivateAssets>all</PrivateAssets>を省略しないでください。 PolySharp と SimonCropp/Polyfill は、コンパイル時のみ使用するソースジェネレーターです。PrivateAssets=allがないと、ポリフィルの型がパッケージの依存関係グラフに漏れ出し、利用者独自のポリフィルと競合する可能性があります。 -
条件付きコンパイルで TFM のバージョンをハードコードしないでください。
NET10_0のような完全一致のシンボルではなく、NET10_0_OR_GREATERのような範囲を表すシンボルを使用してください。完全一致のシンボルは、新しい TFM が追加されると動作しなくなります(例:net11.0 では net10.0 専用の処理がスキップされます)。範囲を表すシンボルなら、今後追加される TFM も自動的に対象になります。 -
TFM ごとに
<LangVersion>を設定しないでください。 すべての TFM で必要となる最も高いバージョンを一度だけ設定します(例:<LangVersion>14</LangVersion>)。バックポートは PolySharp と Polyfill が処理します。TFM ごとに LangVersion を設定すると、分かりにくい構文エラーの原因になります。 -
複数の TFM を対象とする NuGet パッケージで
EnablePackageValidationを省略しないでください。 これを有効にしないと、TFM ごとに異なる API サーフェスを誤って公開してしまい、利用者が TFM を切り替えたときにビルドエラーが発生する可能性があります。 -
MSBuild 条件で範囲チェックに
$(TargetFramework)の文字列一致を使用しないでください。 将来のバージョンとの互換性を保った範囲チェックには、$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net10.0'))を使用してください。文字列一致(例:== 'net10.0')では、net11.0 以降が対象になりません。 -
TFM の検出を再実装しないでください。 このスキルは [skill:dotnet-version-detection] の構造化された出力を使用します。TFM を判断するために
.csprojファイルを解析してはいけません。検出スキルの出力(TFM、C# バージョン、SDK バージョン、警告)を使用してください。 -
ポリフィルがランタイムの動作もカバーすると考えないでください。 PolySharp と Polyfill は、コンパイル時のスタブとソース生成による実装を提供します。ランタイムの変更が必要な機能(例:runtime-async、GC の改善、JIT の最適化)はポリフィルできないため、こうした機能には
#ifを使用してください。 -
プラットフォームターゲットにバージョン固有の TFM グロブを使用しないでください。 利用者が異なる .NET バージョンをターゲットにした場合の誤検出を避けるため、ドキュメントやツールでは
net10.0-androidではなく、バージョンに依存しないnet*-androidパターンマッチングを使用してください。
前提条件
- .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