In this article, we’re going to explain the importance and benefits of targeting multiple frameworks in a single .NET project.

To download the source code for this article, you can visit our GitHub repository.

Let’s start.

Understand Targeting Multiple Frameworks in .NET

Targeting multiple frameworks in a .NET project is important and beneficial: It helps make sure that our application is compatible with a wide range of platforms and devices, so we can reach more users. It also makes it easier to support older systems while gradually moving towards newer versions of .NET. Each framework version comes with its own set of cool features and performance improvements.

Support Code Maze on Patreon to get rid of ads and get the best discounts on our products!
Become a patron at Patreon!

By targeting multiple frameworks, we can take full advantage of these capabilities. Plus, it ensures that our application plays well with third-party libraries that might require specific framework versions. Another great thing is the flexibility in deploying the application. We can optimize performance for each system by choosing the right framework. By targeting multiple frameworks, we future-proof our application and make it easier to adapt to the changes in the .NET ecosystem.

Set Up a Project Targeting Multiple Frameworks

Let’s create a console application and configure it to target multiple frameworks. 

When we create a console application project targeting .NET 7.0 and inspect the project file, we can see that the <TargetFramework> is set to net7.0:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net7.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

</Project>

We can include multiple monikers here, to target multiple frameworks.

For this, we will change <TargetFramework> to plural <TargetFrameworks> and include monikers for different targeted frameworks separated by ;:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFrameworks>net7.0;netcoreapp3.1</TargetFrameworks>
    <ImplicitUsings>enable</ImplicitUsings>
    <LangVersion>10.0</LangVersion>
  </PropertyGroup>

</Project>

Here, we want to support one more framework .NET Core 3.1, so we include the netcoreapp3.1  moniker.

Now, since we are targeting different versions of the .NET framework here, we can get some errors related to functionality only usable when we set the language version to C# 10. To resolve that error we include the <LangVersion>10.0</LangVersion> element in the project file.

Once we save project file changes, Visual Studio will include the reference for netcoreapp3.1 in the Dependencies section:

Visual Studio project file showing multiple target frameworks in dependencies

API Differences and Compatibility

When dealing with various .NET versions, API differences might arise, causing unexpected behavior in our project. Understanding these differences is crucial to avoid potential pitfalls.

To deal with API differences, we have a tool called the .NET Portability Analyzer. It can analyze our code and help us identify any potential issues caused by API differences across various .NET frameworks. With its comprehensive analysis, we can be confident that our code will work smoothly across different .NET versions.

The .NET team has also set up a dedicated website, complete with APIs for compatibility checking. 

We still need to make APIs compatible with multiple .NET frameworks, but how?  Well, one clever trick is to use conditional statements in our code. By putting in these checks based on the targeted .NET version, we can handle API variations and keep things running smoothly across different frameworks.

We should also focus on embracing good coding practices like encapsulation and abstraction. By keeping our code modular and reducing direct dependencies on specific APIs, we can build applications that can adapt to any compatibility challenge that comes our way.

Conditional Compilation

The conditional compilation is the key to flexibility in .NET multi-framework targeting. With preprocessor directives, we can signal to the compiler which code to include or exclude during compilation, based on predefined conditions.

Let’s add an example of conditional compilation using preprocessor directives in our project. We will add this framework-specific code using preprocessor conditions #if and #elif into the Program.cs file:

using System;

class Program
{
    static void Main()
    {
#if NET7_0
        Console.WriteLine(".NET 7 - Hello from the latest .NET framework!");
#elif NETCOREAPP3_1
        Console.WriteLine(".NET Core 3.1 - Hello from a previous LTS version of .NET Core!");
#else
        Console.WriteLine("Hello from an unsupported framework or runtime!");
#endif

        Console.WriteLine("Hello, World!");
    }
}

We use the #if directives to conditionally include specific code blocks based on the targeted framework. The corresponding symbols NET7_0 and  NETCOREAPP3_1 are defined based on the targeted frameworks we add.  Hello, World! is the common code across all the targeted frameworks.

NuGet Package References Targeting Multiple Frameworks

NuGet packages are an essential part of any .NET project. When targeting multiple frameworks in a single .NET project, there is a possibility that some packages we want to use are not compatible with all the targeted frameworks.

That’s where the Condition attribute comes to the rescue! The Condition attribute allows us to specify under which conditions a particular package should be included in our project. Consequently, we can ensure that the appropriate packages are utilized for each targeted framework, making our project more efficient and effective.

Let’s take a look at an example of how we can implement this conditional inclusion of NuGet packages in the .csproj file:

<ItemGroup>
  <PackageReference Include="System.Net.Http" Version="4.3.4" />
</ItemGroup>

<ItemGroup Condition="'$(TargetFramework)'=='net7.0'">
  <PackageReference Include="NuGet.Package.ForNET7" Version="1.0.0" />
</ItemGroup>

<ItemGroup Condition="'$(TargetFramework)'=='netcoreapp3.1'">
  <PackageReference Include="NuGet.Package.ForNETCore3" Version="4.0.0" />
</ItemGroup>

Here, the System.Net.Http package is included for all the targeted frameworks. The other statements provide an example of using the Condition attribute to include NuGet packages specific to particularly targeted frameworks.

It is also possible to add the Condition attribute to the PackageReference directly:

<ItemGroup>
  <PackageReference Include="System.Net.Http" Version="4.3.4" />
  <PackageReference Include="NuGet.Package.ForNET7" Version="1.0.0" Condition="'$(TargetFramework)'=='net7.0'" />
  <PackageReference Include="NuGet.Package.ForNETCore3" Version="4.0.0" Condition="'$(TargetFramework)'=='netcoreapp3.1'" />
</ItemGroup>

Best Practices For Targeting Multiple Frameworks

Let’s talk about the importance of keeping the shared codebase as large as possible. This involves aiming for a common codebase that’s shared among all the targeted frameworks. By maximizing the amount of shared code, we can simplify the management and maintenance of the project. Additionally, it enhances collaboration and reduces duplication of efforts among the development teams working on different framework versions.

By creating abstractions and using interfaces, we can define a common set of functionalities that can be implemented differently for each framework. This allows to keep the core logic shared, while still addressing framework-specific requirements.

It is also important to minimize the use of #if directives. Even though, #if directives can help conditionally compile code for specific frameworks, excessive use of them can lead to code clutter and maintenance challenges. Instead, we can focus on keeping the code clean and straightforward, using the shared codebase approach.

One best practice is to create separate test projects for each targeted framework. This ensures that we have dedicated tests for each framework, allowing us to catch any framework-specific issues during testing. We can also use testing frameworks that support multi-targeting. Some popular testing frameworks offer great support for testing across multiple frameworks. This means we can write tests once and run them against different frameworks effortlessly.

Conclusion

Targeting multiple frameworks in a single .NET project offers a powerful way to expand the application’s reach and compatibility. By utilizing the features like .NET Standard and conditional compilation, we can create a versatile codebase that adapts to various frameworks seamlessly.

Additionally, using the right tools, such as Visual Studio and NuGet packages, we can streamline the development process and ensure optimal performance across different platforms. Embracing these techniques empowers us to deliver robust, future-proof applications that serve a broader audience and maximize the impact in the ever-evolving world of software development.

Liked it? Take a second to support Code Maze on Patreon and get the ad free reading experience!
Become a patron at Patreon!