From c97006ccecae99be7caf60148d6182531fc28694 Mon Sep 17 00:00:00 2001
From: Kaha Mason <41455076+KahaMason@users.noreply.github.com>
Date: Wed, 4 Feb 2026 15:41:36 +1000
Subject: [PATCH 1/8] Update rule
---
.../rule.mdx | 172 +++++++++++++++++-
1 file changed, 164 insertions(+), 8 deletions(-)
diff --git a/public/uploads/rules/manage-compatibility-for-different-tfms/rule.mdx b/public/uploads/rules/manage-compatibility-for-different-tfms/rule.mdx
index c4d730fa179..ef63ea34283 100644
--- a/public/uploads/rules/manage-compatibility-for-different-tfms/rule.mdx
+++ b/public/uploads/rules/manage-compatibility-for-different-tfms/rule.mdx
@@ -1,5 +1,5 @@
---
-seoDescription: Learn how to manage compatibility between different Target Framework Monikers (TFMs) using #if pragma statements and MSBuild conditions for efficient migrations.
+seoDescription: Learn how to manage compatibility between different Target Framework Monikers (TFMs) using multi-targeting, C# preprocessor directives, and MSBuild conditions.
type: rule
title: Do you know how to manage compatibility between different Target Framework Monikers (TFMs)?
categories:
@@ -17,9 +17,23 @@ Here are some essential tips for managing changes that are not compatible with b
-### Using #if Pragma Statements
+### Multi-targeting your project
-You can use #if pragma statements to compile code exclusively for a specific TFM. This technique also simplifies the removal process during post-migration cleanup, especially for incompatible code segments.
+If you need to support both the old and new TFMs for a period of time, multi-target the project so you can build/test both:
+
+```xml
+
+ net48;net10.0
+
+```
+
+This lets you keep one codebase while you migrate, and it makes compatibility issues visible in CI (because both TFMs compile).
+
+Once the migration is complete, you can remove the old TFM from `TargetFrameworks`.
+
+### Using C# preprocessor directives (`#if`, `#elif`, `#endif`)
+
+You can use C# preprocessor directives to compile code exclusively for a specific TFM. This technique also simplifies the removal process during post-migration cleanup, especially for incompatible code segments.
Whenever possible, consider using dependency injection or factory patterns to inject the appropriate implementation based on the TFM you are targeting. This approach promotes code flexibility and maintainability, as it abstracts away TFM-specific details.
@@ -28,7 +42,7 @@ public static class WebClientFactory
{
public static IWebClient GetWebClient()
{
-#if NET472
+#if NET48
return new CustomWebClient();
#else
return new CustomHttpClient();
@@ -36,17 +50,159 @@ public static class WebClientFactory
}
}
```
-**✅ Code: Good example - Using #if Pragma statements and factory pattern**
+**✅ Code: Good example - Using preprocessor directives and a factory pattern**
+
+Tip: Prefer explicit version checks when possible, as they scale better than `#else`:
+
+```cs
+#if NET10_0_OR_GREATER
+// Use the .NET 10+ implementation
+#else
+// Use the .NET Framework implementation
+#endif
+```
+
+If you want `#if NET48` to work reliably, define it in your project file (so it’s available when compiling the `net48` target):
+
+```xml
+
+ $(DefineConstants);NET48
+
+```
### Using MSBuild conditions
You can use MSBuild conditions to add references to different libraries that are only compatible with a specific TFM. This enables you to manage references dynamically based on the TFM in use.
-```cs
-
+```xml
+
```
-**✅ Code: Good example - Using MSBuild conditions**
\ No newline at end of file
+**✅ Code: Good example - Using MSBuild conditions**
+
+#### Conditional NuGet packages per TFM
+
+In SDK-style projects, you can use a similar approach to separate target-specific NuGet packages:
+
+```xml
+
+
+
+
+
+
+
+
+
+
+
+```
+
+#### Optional: Include different source files per TFM
+
+If the implementation differs a lot, keep the code in separate files and include them conditionally:
+
+```xml
+
+
+
+
+
+
+
+```
+
+### CI: Build & Test matrix (GitHub Actions example)
+
+Use a small matrix job to build and test each TFM on an appropriate runner (Windows for `net48`, Linux for `net10.0`). Adjust the `dotnet-version` and steps for your repository layout.
+
+```yaml
+name: .NET Multi-TFM CI
+on: [push, pull_request]
+jobs:
+ build:
+ runs-on: ${{ matrix.os }}
+ strategy:
+ matrix:
+ include:
+ - os: windows-latest
+ tfm: net48
+ - os: ubuntu-latest
+ tfm: net10.0
+ steps:
+ - uses: actions/checkout@v4
+ - name: Setup .NET
+ uses: actions/setup-dotnet@v3
+ with:
+ dotnet-version: 10.0.x
+ - name: Restore
+ run: dotnet restore
+ - name: Build
+ run: dotnet build -c Release -f ${{ matrix.tfm }}
+ - name: Test
+ run: dotnet test -c Release -f ${{ matrix.tfm }} --no-build
+```
+
+Note: `net48` requires a Windows runner; the matrix `include` pairs each TFM with a compatible OS. If your repo has multiple projects or solution-level builds, adjust the `dotnet` commands accordingly and add caching or solution-specific restore steps.
+
+### Azure Pipelines example
+
+If you use Azure DevOps pipelines, a simple approach is two jobs: one on Windows for `net48` and one on Linux for `net10.0`.
+
+```yaml
+trigger:
+ branches:
+ include: [ main ]
+
+jobs:
+- job: Build_net48
+ displayName: Build & Test (net48) - Windows
+ pool:
+ vmImage: 'windows-latest'
+ steps:
+ - task: UseDotNet@2
+ inputs:
+ packageType: 'sdk'
+ version: '10.0.x'
+ - script: dotnet restore
+ displayName: Restore
+ - script: dotnet build -c Release -f net48
+ displayName: Build net48
+ - script: dotnet test -c Release -f net48 --no-build
+ displayName: Test net48
+
+- job: Build_net10
+ displayName: Build & Test (net10.0) - Linux
+ pool:
+ vmImage: 'ubuntu-latest'
+ steps:
+ - task: UseDotNet@2
+ inputs:
+ packageType: 'sdk'
+ version: '10.0.x'
+ - script: dotnet restore
+ displayName: Restore
+ - script: dotnet build -c Release -f net10.0
+ displayName: Build net10.0
+ - script: dotnet test -c Release -f net10.0 --no-build
+ displayName: Test net10.0
+```
+
+Notes: adjust the `version` in `UseDotNet@2`, add solution paths if needed, and include caching or artifact publishing as appropriate for your pipeline.
+
+### Post-migration cleanup
+
+Once the main branch has fully moved to the new TFM:
+
+- Remove the old TFM from `TargetFrameworks`
+- Delete the old implementation paths (or per-TFM files)
+- Remove conditional MSBuild blocks and legacy package references
+
+### Definition of done
+
+- The project builds for all target TFMs
+- Tests run for each TFM in CI
+- Compatibility code is isolated (minimal `#if` in domain/business logic)
\ No newline at end of file
From dc235eae7909bd8d112171b68141811ee615981b Mon Sep 17 00:00:00 2001
From: "Kaha Mason [SSW]" <41455076+KahaMason@users.noreply.github.com>
Date: Wed, 4 Feb 2026 15:59:36 +1000
Subject: [PATCH 2/8] TinaCMS content update
Co-authored-by: Kaha Mason
---
.../rule.mdx | 34 ++++++++++++-------
1 file changed, 22 insertions(+), 12 deletions(-)
diff --git a/public/uploads/rules/manage-compatibility-for-different-tfms/rule.mdx b/public/uploads/rules/manage-compatibility-for-different-tfms/rule.mdx
index ef63ea34283..2685c132868 100644
--- a/public/uploads/rules/manage-compatibility-for-different-tfms/rule.mdx
+++ b/public/uploads/rules/manage-compatibility-for-different-tfms/rule.mdx
@@ -1,16 +1,24 @@
---
-seoDescription: Learn how to manage compatibility between different Target Framework Monikers (TFMs) using multi-targeting, C# preprocessor directives, and MSBuild conditions.
type: rule
title: Do you know how to manage compatibility between different Target Framework Monikers (TFMs)?
+uri: manage-compatibility-for-different-tfms
categories:
- category: categories/software-engineering/rules-to-better-net10-migrations.mdx
-uri: manage-compatibility-for-different-tfms
authors:
- title: Yazhi Chen
- url: https://www.ssw.com.au/people/yazhi-chen
-created: 2023-09-01T01:39:10.755Z
+ url: 'https://www.ssw.com.au/people/yazhi-chen'
+ - title: Kaha Mason
+ url: 'https://ssw.com.au/people/kaha-mason'
guid: 05a2bdf2-d173-4c46-a700-b2b3b83408e2
+seoDescription: 'Learn how to manage compatibility between different Target Framework Monikers (TFMs) using multi-targeting, C# preprocessor directives, and MSBuild conditions.'
+created: 2023-09-01T01:39:10.755Z
+createdBy: Kaha Mason
+createdByEmail: kahamason@ssw.com.au
+lastUpdated: 2026-02-04T05:59:35.203Z
+lastUpdatedBy: Kaha Mason
+lastUpdatedByEmail: kahamason@ssw.com.au
---
+
Migrating your project to a new Target Framework Moniker (TFM) can be a complex task, especially when you're dealing with compatibility issues between different Target Framework Monikers (TFMs). It is suggested to handle your migration PBIs (Product Backlog Items) collectively and transition your main branch to the new TFM. Making this judgment call requires careful consideration of factors like the number of PBIs and their estimated completion time.
Here are some essential tips for managing changes that are not compatible with both the old and new TFMs:
@@ -50,7 +58,8 @@ public static class WebClientFactory
}
}
```
-**✅ Code: Good example - Using preprocessor directives and a factory pattern**
+
+**✅ Code: Good example - Using preprocessor directives and a factory pattern**
Tip: Prefer explicit version checks when possible, as they scale better than `#else`:
@@ -81,7 +90,8 @@ You can use MSBuild conditions to add references to different libraries that are
```
-**✅ Code: Good example - Using MSBuild conditions**
+
+**✅ Code: Good example - Using MSBuild conditions**
#### Conditional NuGet packages per TFM
@@ -197,12 +207,12 @@ Notes: adjust the `version` in `UseDotNet@2`, add solution paths if needed, and
Once the main branch has fully moved to the new TFM:
-- Remove the old TFM from `TargetFrameworks`
-- Delete the old implementation paths (or per-TFM files)
-- Remove conditional MSBuild blocks and legacy package references
+* Remove the old TFM from `TargetFrameworks`
+* Delete the old implementation paths (or per-TFM files)
+* Remove conditional MSBuild blocks and legacy package references
### Definition of done
-- The project builds for all target TFMs
-- Tests run for each TFM in CI
-- Compatibility code is isolated (minimal `#if` in domain/business logic)
\ No newline at end of file
+* The project builds for all target TFMs
+* Tests run for each TFM in CI
+* Compatibility code is isolated (minimal `#if` in domain/business logic)
From 71c94207f7d37ff50622fb903f478b1457adda83 Mon Sep 17 00:00:00 2001
From: Kaha Mason <41455076+KahaMason@users.noreply.github.com>
Date: Wed, 4 Feb 2026 16:03:31 +1000
Subject: [PATCH 3/8] Remove CI options and add example ratings
---
.../rule.mdx | 83 +------------------
1 file changed, 3 insertions(+), 80 deletions(-)
diff --git a/public/uploads/rules/manage-compatibility-for-different-tfms/rule.mdx b/public/uploads/rules/manage-compatibility-for-different-tfms/rule.mdx
index 2685c132868..5f580a520fe 100644
--- a/public/uploads/rules/manage-compatibility-for-different-tfms/rule.mdx
+++ b/public/uploads/rules/manage-compatibility-for-different-tfms/rule.mdx
@@ -58,7 +58,6 @@ public static class WebClientFactory
}
}
```
-
**✅ Code: Good example - Using preprocessor directives and a factory pattern**
Tip: Prefer explicit version checks when possible, as they scale better than `#else`:
@@ -78,6 +77,7 @@ If you want `#if NET48` to work reliably, define it in your project file (so it
$(DefineConstants);NET48
```
+**✅ Code: Good example - Defining custom preprocessor symbols per TFM**
### Using MSBuild conditions
@@ -90,7 +90,6 @@ You can use MSBuild conditions to add references to different libraries that are
```
-
**✅ Code: Good example - Using MSBuild conditions**
#### Conditional NuGet packages per TFM
@@ -110,6 +109,7 @@ In SDK-style projects, you can use a similar approach to separate target-specifi
```
+**✅ Code: Good example - Conditional NuGet packages per TFM**
#### Optional: Include different source files per TFM
@@ -124,84 +124,7 @@ If the implementation differs a lot, keep the code in separate files and include
```
-
-### CI: Build & Test matrix (GitHub Actions example)
-
-Use a small matrix job to build and test each TFM on an appropriate runner (Windows for `net48`, Linux for `net10.0`). Adjust the `dotnet-version` and steps for your repository layout.
-
-```yaml
-name: .NET Multi-TFM CI
-on: [push, pull_request]
-jobs:
- build:
- runs-on: ${{ matrix.os }}
- strategy:
- matrix:
- include:
- - os: windows-latest
- tfm: net48
- - os: ubuntu-latest
- tfm: net10.0
- steps:
- - uses: actions/checkout@v4
- - name: Setup .NET
- uses: actions/setup-dotnet@v3
- with:
- dotnet-version: 10.0.x
- - name: Restore
- run: dotnet restore
- - name: Build
- run: dotnet build -c Release -f ${{ matrix.tfm }}
- - name: Test
- run: dotnet test -c Release -f ${{ matrix.tfm }} --no-build
-```
-
-Note: `net48` requires a Windows runner; the matrix `include` pairs each TFM with a compatible OS. If your repo has multiple projects or solution-level builds, adjust the `dotnet` commands accordingly and add caching or solution-specific restore steps.
-
-### Azure Pipelines example
-
-If you use Azure DevOps pipelines, a simple approach is two jobs: one on Windows for `net48` and one on Linux for `net10.0`.
-
-```yaml
-trigger:
- branches:
- include: [ main ]
-
-jobs:
-- job: Build_net48
- displayName: Build & Test (net48) - Windows
- pool:
- vmImage: 'windows-latest'
- steps:
- - task: UseDotNet@2
- inputs:
- packageType: 'sdk'
- version: '10.0.x'
- - script: dotnet restore
- displayName: Restore
- - script: dotnet build -c Release -f net48
- displayName: Build net48
- - script: dotnet test -c Release -f net48 --no-build
- displayName: Test net48
-
-- job: Build_net10
- displayName: Build & Test (net10.0) - Linux
- pool:
- vmImage: 'ubuntu-latest'
- steps:
- - task: UseDotNet@2
- inputs:
- packageType: 'sdk'
- version: '10.0.x'
- - script: dotnet restore
- displayName: Restore
- - script: dotnet build -c Release -f net10.0
- displayName: Build net10.0
- - script: dotnet test -c Release -f net10.0 --no-build
- displayName: Test net10.0
-```
-
-Notes: adjust the `version` in `UseDotNet@2`, add solution paths if needed, and include caching or artifact publishing as appropriate for your pipeline.
+**✅ Code: Good example - Conditional source files per TFM**
### Post-migration cleanup
From 27468491f0a99ae8b99937d3366e361d321d65c9 Mon Sep 17 00:00:00 2001
From: Kaha Mason <41455076+KahaMason@users.noreply.github.com>
Date: Fri, 6 Feb 2026 10:27:32 +1000
Subject: [PATCH 4/8] Update
---
.../rule.mdx | 53 ++++++++++---------
1 file changed, 28 insertions(+), 25 deletions(-)
diff --git a/public/uploads/rules/manage-compatibility-for-different-tfms/rule.mdx b/public/uploads/rules/manage-compatibility-for-different-tfms/rule.mdx
index 5f580a520fe..b0206c42a09 100644
--- a/public/uploads/rules/manage-compatibility-for-different-tfms/rule.mdx
+++ b/public/uploads/rules/manage-compatibility-for-different-tfms/rule.mdx
@@ -1,6 +1,6 @@
---
type: rule
-title: Do you know how to manage compatibility between different Target Framework Monikers (TFMs)?
+title: Do you know how to multi-target your .NET projects?
uri: manage-compatibility-for-different-tfms
categories:
- category: categories/software-engineering/rules-to-better-net10-migrations.mdx
@@ -10,7 +10,7 @@ authors:
- title: Kaha Mason
url: 'https://ssw.com.au/people/kaha-mason'
guid: 05a2bdf2-d173-4c46-a700-b2b3b83408e2
-seoDescription: 'Learn how to manage compatibility between different Target Framework Monikers (TFMs) using multi-targeting, C# preprocessor directives, and MSBuild conditions.'
+seoDescription: 'Learn how to manage compatibility between different Target Frameworks by using multi-targeting, C# preprocessor directives, and MSBuild conditions.'
created: 2023-09-01T01:39:10.755Z
createdBy: Kaha Mason
createdByEmail: kahamason@ssw.com.au
@@ -19,15 +19,19 @@ lastUpdatedBy: Kaha Mason
lastUpdatedByEmail: kahamason@ssw.com.au
---
-Migrating your project to a new Target Framework Moniker (TFM) can be a complex task, especially when you're dealing with compatibility issues between different Target Framework Monikers (TFMs). It is suggested to handle your migration PBIs (Product Backlog Items) collectively and transition your main branch to the new TFM. Making this judgment call requires careful consideration of factors like the number of PBIs and their estimated completion time.
+Migrating your project to the latest version of .NET can be a difficult task, especially when you need to maintain compatibility with older versions of .NET during the transition. Ultimately, we want to get our projects to be running on the newest and latest .NET, but this process often requires careful planning and execution so as to not impact our existing application.
-Here are some essential tips for managing changes that are not compatible with both the old and new TFMs:
+Multi-targeting target framework allows you to build and test your project against multiple versions of .NET simultaneously, which can help you maintain your current application while progressively migrating to the latest .NET version. This will also allow you to identify and resolve compatibility issues early in the migration process, which will save time and effort overall.
+
+This rule provides guidance on how to manage compatibility when transitioning between target frameworks.
+Here are some strategies to help you manage compatibility between different frameworks:
+
### Multi-targeting your project
-If you need to support both the old and new TFMs for a period of time, multi-target the project so you can build/test both:
+You can specify multiple target frameworks in your project file using the `TargetFrameworks` property:
```xml
@@ -35,15 +39,16 @@ If you need to support both the old and new TFMs for a period of time, multi-tar
```
-This lets you keep one codebase while you migrate, and it makes compatibility issues visible in CI (because both TFMs compile).
+This approach lets you continue to maintain your current application while also building and testing for the desired target framework you aim to migrate to. Being able to build and test against both target frameworks makes compatibility issues more visible in CI (because both target frameworks will compiled).
-Once the migration is complete, you can remove the old TFM from `TargetFrameworks`.
+One big downside of this approach is that it increases build and test times, since each target framework will be built and tested separately. This can essentially multiply the build / test times by a multitude of each target framework specified in the project.
-### Using C# preprocessor directives (`#if`, `#elif`, `#endif`)
+This is a temporary overhead during migration and will be fixed once the full migration is complete and you can remove the old target framework from `TargetFrameworks`.
-You can use C# preprocessor directives to compile code exclusively for a specific TFM. This technique also simplifies the removal process during post-migration cleanup, especially for incompatible code segments.
+### Using C# preprocessor directives (`#if`, `#elif`, `#endif`)
-Whenever possible, consider using dependency injection or factory patterns to inject the appropriate implementation based on the TFM you are targeting. This approach promotes code flexibility and maintainability, as it abstracts away TFM-specific details.
+You can use C# preprocessor directives to compile code exclusively for a specific target framework. This technique also simplifies the removal process during post-migration cleanup, especially for incompatible code segments.
+Whenever possible, consider using dependency injection or factory patterns to inject the appropriate implementation based on the target framework you are targeting. This approach promotes code flexibility and maintainability, as it abstracts away target framework-specific details.
```cs
public static class WebClientFactory
@@ -63,8 +68,8 @@ public static class WebClientFactory
Tip: Prefer explicit version checks when possible, as they scale better than `#else`:
```cs
-#if NET10_0_OR_GREATER
-// Use the .NET 10+ implementation
+#if NET
+// Use the modern .NET implementation
#else
// Use the .NET Framework implementation
#endif
@@ -77,12 +82,11 @@ If you want `#if NET48` to work reliably, define it in your project file (so it
$(DefineConstants);NET48
```
-**✅ Code: Good example - Defining custom preprocessor symbols per TFM**
+**✅ Code: Good example - Defining custom preprocessor symbols per target framework**
### Using MSBuild conditions
-You can use MSBuild conditions to add references to different libraries that are only compatible with a specific TFM. This enables you to manage references dynamically based on the TFM in use.
-
+You can use MSBuild conditions to add references to different libraries that are only compatible with a specific target framework. This enables you to manage references dynamically based on the target framework in use.
```xml
@@ -92,7 +96,7 @@ You can use MSBuild conditions to add references to different libraries that are
```
**✅ Code: Good example - Using MSBuild conditions**
-#### Conditional NuGet packages per TFM
+#### Conditional NuGet packages per target framework
In SDK-style projects, you can use a similar approach to separate target-specific NuGet packages:
@@ -109,9 +113,9 @@ In SDK-style projects, you can use a similar approach to separate target-specifi
```
-**✅ Code: Good example - Conditional NuGet packages per TFM**
+**✅ Code: Good example - Conditional NuGet packages per target framework**
-#### Optional: Include different source files per TFM
+#### Optional: Include different source files per target framework
If the implementation differs a lot, keep the code in separate files and include them conditionally:
@@ -124,18 +128,17 @@ If the implementation differs a lot, keep the code in separate files and include
```
-**✅ Code: Good example - Conditional source files per TFM**
+**✅ Code: Good example - Conditional source files per target framework**
### Post-migration cleanup
-Once the main branch has fully moved to the new TFM:
-
-* Remove the old TFM from `TargetFrameworks`
-* Delete the old implementation paths (or per-TFM files)
+Once the main branch has fully moved to the new target framework:
+* Remove the old target framework from `TargetFrameworks`
+* Delete the old implementation paths (or per-target framework files)
* Remove conditional MSBuild blocks and legacy package references
### Definition of done
-* The project builds for all target TFMs
-* Tests run for each TFM in CI
+* The project builds for all target frameworks
+* Tests run for each target framework in CI
* Compatibility code is isolated (minimal `#if` in domain/business logic)
From 1d09912701de94e4e5e157f6109927224af11b66 Mon Sep 17 00:00:00 2001
From: Kaha Mason <41455076+KahaMason@users.noreply.github.com>
Date: Fri, 6 Feb 2026 13:14:51 +1000
Subject: [PATCH 5/8] Added window-specific target frameworks section
---
.../rule.mdx | 38 +++++++++++++++++++
1 file changed, 38 insertions(+)
diff --git a/public/uploads/rules/manage-compatibility-for-different-tfms/rule.mdx b/public/uploads/rules/manage-compatibility-for-different-tfms/rule.mdx
index b0206c42a09..67be2ac7a37 100644
--- a/public/uploads/rules/manage-compatibility-for-different-tfms/rule.mdx
+++ b/public/uploads/rules/manage-compatibility-for-different-tfms/rule.mdx
@@ -45,6 +45,44 @@ One big downside of this approach is that it increases build and test times, sin
This is a temporary overhead during migration and will be fixed once the full migration is complete and you can remove the old target framework from `TargetFrameworks`.
+### Windows-specific target frameworks
+
+When targeting Windows-only APIs (WinForms, WPF), it is preferred to use explicit Windows target frameworks so you can opt into platform-specific tooling and runtime behavior.
+
+```xml
+
+ net48;net10.0-windows
+ true
+ true
+
+```
+**✅ Good example - Using Windows-specific target frameworks**
+
+The main pros and cons of this approach are dependent on whether your goal is to migrate to a cross-platform framework (Linux/macOS) or to remain Windows-only.
+
+✅ **Pros:**
+- Minimal rewrite: UI and native interop require fewer changes and can continue to use Windows-specific APIs without needing to abstract them for cross-platform compatibility.
+- Native interop preserved: existing COM/Win32 interop and native wrappers continue to work, reducing rewrite effort.
+- Access modern Windows-only APIs on `net10.0-windows` (App SDK, WinRT wrappers) without losing the old build.
+- Avoids the need to remove Windows-specific code immediately as both target frameworks are Windows-only.
+- Lower migration surface for Windows apps: fewer cross-platform abstractions required, making incremental migration faster for Windows-first apps.
+
+❌ **Cons:**
+- Migrating from windows-only to cross-platform frameworks (e.g `net48 or net10.0-windows -> net10.0`)
+ - Project cannot share Windows-specific code, so you may need to refactor or isolate Windows-specific code into a separate project.
+ - Requires more condition code and MSBuild conditions to manage the differences between Windows and cross-platform target frameworks.
+ - Longer migration timeline as you may need to maintain the Windows-specific target framework until the full migration is complete.
+- Platform-compatibility analyzers and `SupportedOSPlatform` warnings may appear; developers must handle or suppress appropriately.
+- Artifacts built for Windows-only target frameworks are not portable to Linux/macOS.
+
+When the Windows-specific codebase is small, you can isolate it with `#if` guards or partial classes; when large, it is recommended to separate into Windows-only projects to keep the shared libraries clean.
+
+❔**Decision guidance:**
+- **Windows Specific Framework** - Windows-first / heavy UI or native interop: prefer `net10.0-windows` (or a Windows-only project) to minimise rewrites, keep tooling and installers, and reduce short-term migration risk.
+- **Cross-platform Framework** - Need portability, cloud/containerisation or broader hosting: prefer migrating shared libraries to cross-platform TFMs (`net10`) and extract Windows UI/interop into a separate Windows-only project.
+
+Ultimately, the decision to maintain Windows-specific target frameworks or migrate directly to cross-platform depends on the goals, timelines, and constraints of your migration project.
+
### Using C# preprocessor directives (`#if`, `#elif`, `#endif`)
You can use C# preprocessor directives to compile code exclusively for a specific target framework. This technique also simplifies the removal process during post-migration cleanup, especially for incompatible code segments.
From 12bb8ef40e480253a534484a5c79b3ddf16b2f18 Mon Sep 17 00:00:00 2001
From: Kaha Mason <41455076+KahaMason@users.noreply.github.com>
Date: Fri, 6 Feb 2026 14:03:11 +1000
Subject: [PATCH 6/8] Converted to a pros and cons section for `Multi-targeting
your project`
---
.../rule.mdx | 21 +++++++++++++++++--
1 file changed, 19 insertions(+), 2 deletions(-)
diff --git a/public/uploads/rules/manage-compatibility-for-different-tfms/rule.mdx b/public/uploads/rules/manage-compatibility-for-different-tfms/rule.mdx
index 67be2ac7a37..a49e0ee8340 100644
--- a/public/uploads/rules/manage-compatibility-for-different-tfms/rule.mdx
+++ b/public/uploads/rules/manage-compatibility-for-different-tfms/rule.mdx
@@ -41,9 +41,26 @@ You can specify multiple target frameworks in your project file using the `Targe
This approach lets you continue to maintain your current application while also building and testing for the desired target framework you aim to migrate to. Being able to build and test against both target frameworks makes compatibility issues more visible in CI (because both target frameworks will compiled).
-One big downside of this approach is that it increases build and test times, since each target framework will be built and tested separately. This can essentially multiply the build / test times by a multitude of each target framework specified in the project.
+When multi-targeting to migrate to `net10`, teams should weigh the pros and cons in regards of performance, maintenance and CI.
-This is a temporary overhead during migration and will be fixed once the full migration is complete and you can remove the old target framework from `TargetFrameworks`.
+✅ **Pros:**
+- Incremental migration: allows you to migrate one project at a time, reducing the scope and risk of changes.
+- Compatibility visibility: CI builds and tests both target frameworks, making it easier to identify and fix compatibility issues early in the migration process.
+- Parallel development: allows you to develop and test new features on `net10` while maintaining the existing codebase on `net48`, enabling a smoother transition for developers and users.
+- Risk mitigation / Rollback: if issues arise with the new target framework, you can quickly switch back to the legacy target framework to keep the application running while you address the problems.
+- User impact reduction: allows you to continue delivering value to users on the existing target framework while you work on the migration, minimizing disruption and maintaining user satisfaction.
+- Gradual API adoption: lets you adopt `net10` APIs in isolated files or projects without breaking existing consumers.
+- Lower immediate risk: reduces pressure to rewrite large UI/native codebases all at once.
+- Incremental cleanup: once the migration is complete, you can remove the old target framework and related compatibility code in a single step, simplifying the cleanup process.
+
+❌ **Cons:**
+- Increased complexity: maintaining multiple target frameworks can lead to more complex project files, build configurations, and code with conditional compilation.
+- Increased build / test time: each target framework builds and runs tests separately, which can significantly increase CI times and local development iteration speed.
+- Increased testing coverage: you need to test all code paths for each target framework, which can increase the testing effort and make it harder to ensure comprehensive test coverage.
+- Maintenance overhead: you need to maintain compatibility code, conditional references, and potentially duplicate code paths for each target framework until the migration is complete.
+- Longer migration timeline: maintaining multiple target frameworks can slow down the migration process, as you need to ensure compatibility and stability across all target frameworks until the old one can be removed.
+
+These are temporary overheads during migration and will be fixed once the full migration is complete and you can remove the old target framework from `TargetFrameworks`.
### Windows-specific target frameworks
From a9fe7b9b08a149591c21cc64354df952351b210b Mon Sep 17 00:00:00 2001
From: Kaha Mason <41455076+KahaMason@users.noreply.github.com>
Date: Fri, 6 Feb 2026 15:02:26 +1000
Subject: [PATCH 7/8] Added Directory Builds Props file suggestions
---
.../rule.mdx | 61 ++++++++++++++++++-
1 file changed, 59 insertions(+), 2 deletions(-)
diff --git a/public/uploads/rules/manage-compatibility-for-different-tfms/rule.mdx b/public/uploads/rules/manage-compatibility-for-different-tfms/rule.mdx
index a49e0ee8340..d1b8d644a81 100644
--- a/public/uploads/rules/manage-compatibility-for-different-tfms/rule.mdx
+++ b/public/uploads/rules/manage-compatibility-for-different-tfms/rule.mdx
@@ -7,8 +7,12 @@ categories:
authors:
- title: Yazhi Chen
url: 'https://www.ssw.com.au/people/yazhi-chen'
+ - title: Kosta Madorsky
+ url: 'https://www.ssw.com.au/people/kosta-madorsky/'
- title: Kaha Mason
- url: 'https://ssw.com.au/people/kaha-mason'
+ url: 'https://www.ssw.com.au/people/kaha-mason/'
+ - title: Sylvia Huang
+ url: 'https://www.ssw.com.au/people/sylvia-huang/'
guid: 05a2bdf2-d173-4c46-a700-b2b3b83408e2
seoDescription: 'Learn how to manage compatibility between different Target Frameworks by using multi-targeting, C# preprocessor directives, and MSBuild conditions.'
created: 2023-09-01T01:39:10.755Z
@@ -62,6 +66,59 @@ When multi-targeting to migrate to `net10`, teams should weigh the pros and cons
These are temporary overheads during migration and will be fixed once the full migration is complete and you can remove the old target framework from `TargetFrameworks`.
+### Centralise Target Frameworks in the Directory.Build.props file
+
+To keep `TargetFrameworks` and related project properties consistent across many projects, define shared properties in a `Directory.Build.props` (or a repo-level `.props`) and reference them from each project. This centralises updates and avoids copy/paste drift.
+
+Directory.Build.props:
+
+```xml
+
+
+ net48
+ net10.0
+ net10.0-windows
+
+ $(LegacyTargetFramework);$(ModernTargetFramework)
+ $(LegacyTargetFramework);$(ModernWindowsTargetFramework)
+
+
+ true
+ true
+ true
+
+
+```
+
+Project file usage:
+
+```xml
+
+ $(CommonTargetFrameworks)
+
+
+
+
+
+ $(DefineConstants);LEGACY_COMPATIBILITY
+
+
+
+ $(DefineConstants);MODERN_COMPATIBILITY
+
+
+
+
+
+
+
+
+
+
+```
+
+This approach provides a single place to declare common project properties, reduces merge conflicts, and makes bulk migrations easier.
+
### Windows-specific target frameworks
When targeting Windows-only APIs (WinForms, WPF), it is preferred to use explicit Windows target frameworks so you can opt into platform-specific tooling and runtime behavior.
@@ -96,7 +153,7 @@ When the Windows-specific codebase is small, you can isolate it with `#if` guard
❔**Decision guidance:**
- **Windows Specific Framework** - Windows-first / heavy UI or native interop: prefer `net10.0-windows` (or a Windows-only project) to minimise rewrites, keep tooling and installers, and reduce short-term migration risk.
-- **Cross-platform Framework** - Need portability, cloud/containerisation or broader hosting: prefer migrating shared libraries to cross-platform TFMs (`net10`) and extract Windows UI/interop into a separate Windows-only project.
+- **Cross-platform Framework** - Need portability, cloud/containerisation or broader hosting: prefer migrating shared libraries to cross-platform target frameworks (`net10`) and extract Windows UI/interop into a separate Windows-only project.
Ultimately, the decision to maintain Windows-specific target frameworks or migrate directly to cross-platform depends on the goals, timelines, and constraints of your migration project.
From 7ef4ee20664150e2eada287a08046f6414304c4e Mon Sep 17 00:00:00 2001
From: Kaha Mason <41455076+KahaMason@users.noreply.github.com>
Date: Fri, 6 Feb 2026 16:50:39 +1000
Subject: [PATCH 8/8] Added method to disable building the legacy framework
---
.../rule.mdx | 20 +++++++++++++++++++
1 file changed, 20 insertions(+)
diff --git a/public/uploads/rules/manage-compatibility-for-different-tfms/rule.mdx b/public/uploads/rules/manage-compatibility-for-different-tfms/rule.mdx
index d1b8d644a81..748de5ba4f6 100644
--- a/public/uploads/rules/manage-compatibility-for-different-tfms/rule.mdx
+++ b/public/uploads/rules/manage-compatibility-for-different-tfms/rule.mdx
@@ -119,6 +119,26 @@ Project file usage:
This approach provides a single place to declare common project properties, reduces merge conflicts, and makes bulk migrations easier.
+### Temporarily disable building the legacy target framework
+
+During a migration you may want to keep the legacy TFM (for reference) but stop building it in CI and reduce local build time once approaching the end of the migration process. You can use a small MSBuild flag to opt-in/opt-out the legacy target framework without the need to remove code.
+
+Directory.Build.props:
+
+```xml
+
+ net48
+ net10.0
+ true
+
+
+ $(LegacyTargetFramework);$(ModernTargetFramework)
+
+
+ $(ModernTargetFramework)
+
+```
+
### Windows-specific target frameworks
When targeting Windows-only APIs (WinForms, WPF), it is preferred to use explicit Windows target frameworks so you can opt into platform-specific tooling and runtime behavior.