--- name: dotnet-multi-targeting description: >- Targets multiple TFMs via polyfills and conditional compilation. PolySharp, API compat. metadata: short-description: .NET skill guidance for foundation tasks --- # dotnet-multi-targeting Comprehensive guide for .NET multi-targeting strategies with a polyfill-first approach. This skill consumes the structured output from [skill:dotnet-version-detection] (TFM, C# version, preview flags) and provides actionable guidance on backporting language features, handling runtime gaps, and validating API compatibility across target frameworks. ## Scope - Decision matrix: polyfill vs conditional compilation - PolySharp for compiler-synthesized polyfills - SimonCropp/Polyfill for BCL API backporting - Conditional compilation with TFM-based preprocessor symbols - Multi-targeting .csproj patterns and TFM-specific source files - API compatibility validation (EnablePackageValidation, ApiCompat tool) ## Out of scope - TFM detection logic -- see [skill:dotnet-version-detection] - Version upgrade lane selection -- see [skill:dotnet-version-upgrade] - Platform-specific UI frameworks (MAUI, Blazor) -- see respective framework skills - Cloud deployment configuration Cross-references: [skill:dotnet-version-detection] for TFM resolution and version matrix, [skill:dotnet-version-upgrade] for upgrade lane guidance and migration strategies. --- ## Decision Matrix: Polyfill vs Conditional Compilation Use this matrix to select the correct strategy for each type of gap between your highest and lowest TFMs. | Gap Type | Strategy | When to Use | Example | | --------------------------- | ------------------------------------------------------- | ---------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- | | Language/syntax feature | Polyfill (PolySharp) | Compiler needs attribute/type stubs to emit newer syntax on older TFMs | `required` modifier, `init` properties, `SetsRequiredMembers` on net8.0 | | BCL API addition | Polyfill (SimonCropp/Polyfill) if available, else `#if` | A newer BCL type or method is missing on older TFMs | `System.Threading.Lock` on net8.0, `Index`/`Range` on netstandard2.0 | | Runtime behavior difference | Conditional compilation (`#if`) or adapter pattern | Behavior differs at runtime regardless of compilation | Runtime-async (net11.0 only), different GC modes, `SearchValues` runtime optimizations | | Platform API divergence | Conditional compilation with `[SupportedOSPlatform]` | API exists only on specific OS targets | Windows Registry APIs, Android-specific intents, iOS keychain | **Decision flow:** 1. Can a compile-time polyfill satisfy the gap? Use PolySharp or SimonCropp/Polyfill. 2. Is the gap a missing BCL API with no polyfill available? Use `#if` with TFM-specific code. 3. Is the gap a runtime behavior difference? Use `#if` or the adapter pattern to isolate divergent code paths. 4. Is the gap platform-specific? Use `#if` with `[SupportedOSPlatform]` attributes. --- ## PolySharp (Compiler-Synthesized Polyfills) PolySharp is a source generator that synthesizes the attribute and type stubs the C# compiler needs to emit newer language features when targeting older TFMs. It operates entirely at compile time -- no runtime dependencies are added. ### What PolySharp Provides - `required` modifier support (C# 11+) - `init` property accessors (C# 9+) - `SetsRequiredMembers` attribute - `CompilerFeatureRequired` attribute - `IsExternalInit` type - `CallerArgumentExpression` attribute - `StackTraceHidden` attribute - `UnscopedRef` attribute - `InterpolatedStringHandler` attributes - `ModuleInitializer` attribute - Index and Range support types ### Setup ````xml net8.0;net10.0 14 all runtime; build; native; contentfiles; analyzers ```text ### How It Works PolySharp detects which polyfill types are missing for the current TFM and generates source for only those types. On net10.0, where `required` is natively supported, the generator emits nothing -- zero overhead. ```csharp // 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; } } ```text ### PolySharp Limitations - PolySharp provides **compiler stubs only**. It does not backport runtime behavior. - Features that require runtime support (e.g., runtime-async, `SearchValues` hardware acceleration) cannot be polyfilled. - If a feature needs both a compiler attribute AND a BCL API (e.g., collection expressions with `Span` overloads), you may need both PolySharp and SimonCropp/Polyfill. --- ## SimonCropp/Polyfill (BCL API Polyfills) SimonCropp/Polyfill provides source-generated implementations of newer BCL APIs for older TFMs. Unlike PolySharp (which provides compiler attribute stubs), Polyfill provides actual method and type implementations. ### What Polyfill Provides Key polyfilled APIs (non-exhaustive): - `System.Threading.Lock` (C# 13 / net9.0+) - `String.Contains(char)`, `String.Contains(string, StringComparison)` - `String.ReplaceLineEndings()` - `HashCode` struct - `SkipLocalsInit` attribute - `TaskCompletionSource` (non-generic) - `Stream.ReadExactly`, `Stream.ReadAtLeast` - `Memory` and `Span` extensions - `IReadOnlySet` interface - Various LINQ additions (`TryGetNonEnumeratedCount`, `DistinctBy`, `Chunk`, etc.) ### Setup ```xml net8.0;net10.0 14 all runtime; build; native; contentfiles; analyzers ```text ### Usage Example ```csharp // 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}"); } } } ```text ### Combining PolySharp and Polyfill For maximum compatibility, use both packages together. They are complementary and do not conflict: ```xml all runtime; build; native; contentfiles; analyzers all runtime; build; native; contentfiles; analyzers ```text With both installed, you get full language feature support (PolySharp) **and** BCL API backporting (Polyfill) on older TFMs. --- ## Conditional Compilation Use conditional compilation (`#if`) when the gap is a runtime behavior difference or a platform API that cannot be polyfilled at compile time. ### TFM-Based Conditionals The compiler defines preprocessor symbols for each TFM. Use `NET8_0_OR_GREATER`-style symbols (available since .NET 5) for version range checks: ```csharp public static class PerformanceHelper { #if NET10_0_OR_GREATER // net10.0+ has optimized SearchValues with hardware acceleration private static readonly SearchValues s_vowels = SearchValues.Create("aeiouAEIOU"); public static int CountVowels(ReadOnlySpan text) => text.Count(s_vowels); #else // Fallback for net8.0: manual loop public static int CountVowels(ReadOnlySpan text) { int count = 0; foreach (char c in text) { if ("aeiouAEIOU".Contains(c)) count++; } return count; } #endif } ```text ### Available Preprocessor Symbols | Symbol | True When | |--------|-----------| | `NET8_0` | Exactly net8.0 | | `NET8_0_OR_GREATER` | net8.0 or any higher version | | `NET9_0_OR_GREATER` | net9.0 or any higher version | | `NET10_0_OR_GREATER` | net10.0 or any higher version | | `NET11_0_OR_GREATER` | net11.0 or any higher version | | `NETSTANDARD2_0` | Exactly netstandard2.0 | | `NETSTANDARD2_0_OR_GREATER` | netstandard2.0 or higher | ### When #if Is Correct 1. **Runtime behavior gap:** The API exists on both TFMs but behaves differently at runtime (e.g., `GC.Collect` modes, `HttpClient` connection pooling behavior). 2. **No polyfill available:** The BCL API is not covered by SimonCropp/Polyfill and cannot be stubbed. 3. **Performance-critical path:** You want to use a TFM-specific optimized API path (e.g., `SearchValues`, `FrozenDictionary`). 4. **Platform API:** The API is available only on a specific OS platform target. ### When #if Is Wrong - **Language syntax feature** (e.g., `required`, `init`): Use PolySharp instead. - **Missing BCL method** that has a polyfill (e.g., `System.Threading.Lock`): Use SimonCropp/Polyfill instead. - Wrapping entire files in `#if` blocks -- use TFM-specific source files instead (see below). --- ## Multi-Targeting .csproj Patterns ### Basic Multi-Targeting ```xml net8.0;net10.0 14 ```csharp ### Conditional Package References Some packages are only needed on specific TFMs: ```xml all runtime; build; native; contentfiles; analyzers ```json ### TFM-Specific Source Files For large blocks of TFM-specific code, use dedicated source files instead of `#if` blocks: ```xml ```csharp Directory structure: ```text MyLibrary/ Compatibility/ Net8/ SearchValuesCompat.cs Net10/ SearchValuesNative.cs Services/ TextAnalyzer.cs # shared code, references interface MyLibrary.csproj ```csharp ### Platform-Specific TFMs For projects targeting platform-specific TFMs (MAUI, Uno): ```xml net10.0;net10.0-android;net10.0-ios;net10.0-windows10.0.19041.0 ```text ### Shared Properties via Directory.Build.props For multi-project solutions, centralize multi-targeting configuration: ```xml 14 all runtime; build; native; contentfiles; analyzers all runtime; build; native; contentfiles; analyzers ## Detailed Examples See [references/detailed-examples.md](references/detailed-examples.md) for complete code samples and advanced patterns.