7.8 KiB
ADR-001: Multi-Target Framework Support
Status
Accepted
Context
The WPF UI library needs to support a wide range of .NET implementations to maximize compatibility with existing projects while leveraging modern .NET features where available.
Supported Frameworks
- .NET 10, 9, 8 - Modern .NET with Windows-specific APIs
- .NET Framework 4.8.1, 4.7.2, 4.6.2 - Legacy enterprise applications
- .NET Standard 2.0, 2.1 - Abstractions library only (maximizes compatibility)
Decision
Core Library (Wpf.Ui)
Target frameworks: net10.0-windows;net9.0-windows;net8.0-windows;net481;net472;net462
Rationale:
- Windows-specific project requires
-windowsTFM suffix for .NET 5+ - .NET Framework support ensures compatibility with legacy WPF applications
- Multi-version .NET support provides upgrade path for consumers
Abstractions Library (Wpf.Ui.Abstractions)
Target frameworks: net10.0;net9.0;net8.0;net462;netstandard2.1;netstandard2.0
Rationale:
- No WPF dependencies allows broader compatibility
- .NET Standard 2.0 enables use in class libraries shared between .NET Framework and .NET Core/5+
- AOT-compatible (aot_compatible: true) for modern deployment scenarios
DI Integration (Wpf.Ui.DependencyInjection)
Target frameworks: Same as Abstractions
Rationale:
- Depends only on Microsoft.Extensions.DependencyInjection.Abstractions (version 3.1.0 for broad compatibility)
- No WPF dependencies
- Enables DI integration in non-WPF contexts (e.g., background services)
Central Package Management
All package versions are managed in Directory.Packages.props:
<Project>
<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<ItemGroup>
<!-- Package versions defined here -->
<PackageVersion Include="Microsoft.Windows.CsWin32" Version="0.3.242" />
<PackageVersion Include="System.Memory" Version="4.6.3" />
</ItemGroup>
</Project>
Individual projects reference packages without version attributes:
<PackageReference Include="Microsoft.Windows.CsWin32" />
Benefits:
- Single source of truth for package versions
- Prevents version conflicts across projects
- Simplifies dependency updates
Conditional Compilation
Framework Detection
#if NET5_0_OR_GREATER
// Modern .NET APIs (Environment.OSVersion)
#else
// .NET Framework fallback (registry)
#endif
#if NET6_0_OR_GREATER
// DisposeAsync for CancellationTokenRegistration
#endif
#if NET8_0_OR_GREATER
// Latest .NET 8+ features
#endif
Framework-Specific Dependencies
<!-- Only for .NET Framework 4.6.2 -->
<PackageReference Include="System.ValueTuple" Condition="'$(TargetFramework)' == 'net462'" />
<!-- Only for .NET Core/5+ (not .NET Framework 4.6.2) -->
<PackageReference Include="System.Drawing.Common" Condition="'$(TargetFramework)' != 'net462'" />
PolySharp Integration
Package: PolySharp (build-time source generator)
Provides polyfills for newer C# features on older target frameworks:
- C# 11+ features on .NET 6/7/8
- C# 12+ features on older .NET versions
Configuration:
<ItemGroup>
<CompilerVisibleProperty Include="PolySharpExcludeGeneratedTypes" />
</ItemGroup>
<PropertyGroup>
<!-- Exclude specific polyfills if needed -->
<PolySharpExcludeGeneratedTypes>
System.Runtime.CompilerServices.OverloadResolutionPriorityAttribute;
System.Diagnostics.CodeAnalysis.UnscopedRefAttribute
</PolySharpExcludeGeneratedTypes>
</PropertyGroup>
Benefits:
- Use modern C# syntax across all target frameworks
- Init-only properties on .NET Framework
- Required members support
- CallerArgumentExpression on older frameworks
Language Version
Set to C# 14.0 across all projects:
<LangVersion>14.0</LangVersion>
Note:
Wpf.Ui.csprojoverrides this to<LangVersion>preview</LangVersion>to enable C# preview features.
Combined with PolySharp, this enables:
- Latest C# language features
- Polyfills generated at compile-time for older frameworks
- No runtime dependencies
Enforcement
MUST Follow
- All package versions in
Directory.Packages.propsonly — Central Package Management is the single source of truth for NuGet versions - Use
-windowsTFM suffix for projects with WPF dependencies (e.g.,net10.0-windows, notnet10.0) - Use
netstandard2.0/netstandard2.1for abstraction-only packages that have no WPF or Windows dependency - Guard framework-specific code with
#if NET{X}_0_OR_GREATERdirectives — never use runtime version checks for compile-time API differences - Use PolySharp for C# polyfills — never hand-write polyfill classes for language features (e.g.,
IsExternalInit,CallerArgumentExpression) - Set
LangVersioninDirectory.Build.props— override in individual.csprojonly when justified (currently onlyWpf.Ui.csprojoverrides topreview)
MUST NOT Do
- Never add
Versionattribute toPackageReferencein.csprojfiles — all versions must be inDirectory.Packages.props - Never add a new TFM without updating all projects that share the same TFM set — all projects in a TFM group must stay in sync
- Never use
#ifwith specific patch versions (e.g.,NET8_0_10) — only use_OR_GREATERsuffixed symbols - Never remove a TFM from a shipping package without a major version bump — removing a TFM is a breaking change for consumers on that framework
Verification
- CPM enforcement:
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>inDirectory.Build.propscauses build errors ifVersionis specified in.csproj - TFM validation: Build compiles all target frameworks on every
dotnet build— missing APIs surface as compile errors immediately - PolySharp coverage: PolySharp source generator runs at build time and provides polyfills automatically; manual polyfills would cause duplicate symbol errors
Consequences
Positive
- Broad Compatibility: Supports applications from .NET Framework 4.6.2 through .NET 10
- Modern Development: C# 14 preview features via PolySharp
- Simplified Maintenance: Central package management
- Clear Upgrade Path: Consumers can upgrade .NET version without changing library
- AOT Ready: Abstractions library supports AOT scenarios
Negative
- Increased Build Complexity: Each commit builds 6+ framework variants
- Larger Package Size: Multi-target packages include assemblies for all frameworks
- Conditional Compilation: Requires
#ifdirectives for framework-specific code - Testing Burden: Features should be tested on multiple framework versions
Implementation Details
Directory.Build.props
Central build properties defined once:
<PropertyGroup>
<Version>4.2.0</Version>
<LangVersion>14.0</LangVersion>
<Nullable>enable</Nullable>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
Conditional Property Groups
<PropertyGroup Condition="$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net8.0'))">
<DefineConstants>$(DefineConstants);NET8_0_OR_GREATER</DefineConstants>
</PropertyGroup>
<PropertyGroup Condition="!$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net8.0'))">
<DefineConstants>$(DefineConstants);BELOW_NET8</DefineConstants>
</PropertyGroup>