P3.NET

Porting a .NET Framework Library to .NET Standard

If you aren’t aware, Kraken is my personal class library that I’ve been using since the early days of .NET. It has evolved over the years to support new featuers as .NET and my needs have changed. The entire source code is available on Github and I have recently been pushing builds to NuGet. Prior to that I ran my own private NuGet server where I hosted the packages and used them in my projects. Now that .NET Standard 2.0 is out and my company is looking at moving to .NET Core it is time for me to upgrade Kraken to .NET Standard. I’m going to document that process here because I think my upgrade path is going to be very similar for others.

Before You Begin

Before you begin you should do a couple preliminary steps. Firstly you should evaluate whether this is even a good idea. Some libraries are clearly .NET Framework only, at least right now. This would include any library that specifically targets WCF or ASP.NET. These libraries are not supported in .NET Core and therefore would not benefit from being replatformed to .NET Standard. As discussed here libraries built against newer versions of the .NET Framework can be used with .NET Core code. So just because you are not using .NET Standard doesn’t mean you cannot use older libraries.

If your code is a candidate for .NET Standard then you are ready to start making changes. But first backup your code. If you’re using GitHub, Azure DevOps (VSTS/TFS) or any other source control system then you should be fine. Be sure to use branching or some other versioning system so you can get back to the original code if things aren’t working out.

Creating a New Project vs Using an Existing One

You need to decide whether you want to update the existing library or create a new one. There are advantages and disadvantages of each approach.

For a new project the advantages include:

  • Legacy code and features can be removed.
  • No need to use compiler directives around code.

For a new project the disadvantages include:

  • Should have a different name than the current library so solutions can reference both if needed.
  • Will need to create a new package for the new library so that older applications can continue to use the older version.
  • Depending upon how the code is laid out there will likely be duplicate source files for both the old and new versions.
  • Bug fixes may require both the old and new libraries to be updated which introduces extra work.

For an existing project the advantages include:

  • Can continue with the same infrastructure and naming conventions as before.
  • Can conditionally adjust code based upon compiler directives if needed.
  • Code is shared between both versions.

For an existing project the disadvantages include:

  • Some old code may not fit in the new library and will need to be conditionally compiled.
  • Code will likely need to be reorganized and possibly moved to another project.

For this article we will be reusing the existing projects.

Upgrading the Framework

Note: This is an optional step only if you intend to continue to support .NET Framework and .NET Standard in the same codebase and want to be able to update both versions.

Now you should ensure you are targeting the appropriate version of the .NET Framework for the version of .NET Standard you want. Microsoft recommends .NET 4.6.2 as this version of the framework is heavily used and suppots .NET Standard 2.0. But there are some issues with that version of the framework when trying to use a single assembly for both .NET Framework and .NET Standard. For this article we will target .NET Standard 2.0 which means we need to be using .NET 4.7.1.

Compile the code against the current framework and resolve any issues. Be sure to clean up any code relying on obsolete functionality. At this point is a good idea to back up again. If things go wrong you are at least using the current version of the framework.

Analyze the Code

The next step is to analyze the projects for compatibility issues. Microsoft provides the Portability Analzyer to help identify issues. You will want to use the appropriate settings for your target framework. In some cases you can rely on compatibility libraries like Windows Compatibility Pack but this will add additional dependencies to your code. Ideally you should separate out your code that has these dependencies.

Another important thing to pay attention to is .NET Standard vs .NET Core. For libraries you should target .NET Standard, not .NET Core. Libraries shouldn’t care about which platform they run on. Hence use .NET Standard. If you are replatforming an application then .NET Core would be the better option.

Analyze Dependencies

After the code is analyzed for compatibility you then need to analyze any dependencies you have. Every dependency must either already support .NET Standard or at least support a version of the framework that is compatibile. In each case be sure to upgrade the dependencies to the latest version.

If you find dependencies that will not work then you need to decide how to proceed. One option is to use a different library that provides the same functionality. If this isn’t doable then consider moving the impacted code to its own project and simply not update that project to .NET Standard. A good example here is the Oracle Managed Provider. A version supporting .NET Standard is in beta at this time. If you need to support it then break the Oracle code into its own project and leave it in the .NET Framework until the dependency has been updated.

Note: Moving types between assemblies is a breaking change. Take a look at the TypeForwardedToAttribute to see if you might be able to use it to ease the transition.

If moving the impacted code is not going to work then use conditional compilation to include the code only for the .NET Framework.

#if NET47

public class OnlyAvailableInNetFramework
{}
#endif

The problem with this approach is that you are tying your code to a specific version of the framework (or .NET Standard). If you update to a newer version then the symbol will no longer be valid. To work around this consider defining a custom framework name in your project file (or better yet a shared properties file). You can then update it in one place as needed.

<PropertyGroup Condition="'$(TargetFramework)'=='net47' Or '$(TargetFramework)'=='net471'">
   <DefineConstants>$(DefineConstants);NET_FRAMEWORK</DefineConstants>
</PropertyGroup>
#if NET_FRAMEWORK

public class OnlyAvailableInNetFramework
{}
#endif

One dependency that you will want to add is Microsoft.DotNet.Analyzers.Compatibility. This NuGet package will help identify dependencies in code that are obsolete. You’ll want to fix/replace this before migrating.

Convert to SDK Project Format

Once you’ve identified and resolve any issues in the code and upgraded to the appropriate framework version it is time to start the conversion. But first, backup again.

You need to be using the SDK project format so now is a good time to migrate if you haven’t yet. This is mandatory for .NET Standard projects. I provided my guidelines here. If you are creating new projects instead of reusing existing projects then you can simply create a new Class Library (.NET Standard) project instead and copy all the code from the existing project.

In my article I talked about how to generate NuGet packages. As mentioned there each project builds its own package. At this time project references are converted to NuGet references in the package so you cannot have a single package with multiple assemblies with a hack. If you need that functionality then consider sticking with the non-project approach for now. Changing the package contents is a breaking change to clients.

For this article we will use a separate package for each project.

Multi-Targeting vs .NET Standard

One final decision you need to make is whether you will simply target .NET Standard or use multi-targeting. As mentioned in the article on using the SDK project format, there are currently issues with using just .NET Standard. My recommendation is to use multi-targeting with the knowledge that one day this won’t be necessary. Since the bulk of the code out there is still .NET Framework, having those applications bring in all the .NET Standard thunk assemblies (even if they aren’t used) is confusing and wastes space. Multi-targeting solves that problem and doesn’t introduce a lot of problems.

For this article we will use multi-targeting. But remember that the tooling isn’t there yet to support it well so you will be editing the project file a lot. One of the most common changes you’ll be making is adding dependencies for one target but not the other.

<ItemGroup Condition="'$(TargetFramework)'=='net472'">
   <Reference Include="System.ComponentModel.DataAnnotations" />
   <Reference Include="System.configuration" />
</ItemGroup>

<ItemGroup Condition="'$(TargetFramework)'=='netstandard2.0'">
   <PackageReference Include="System.ComponentModel.Annotations" Version="4.5.0" />
</ItemGroup>

Retargeting to .NET Standard

Now it is time to retarget to .NET Standard. At this point it is almost anti-climactic.

  1. Open the project file.
  2. Change TargetFramework to TargetFrameworks.
  3. Add the framework moniker to the element, separated by a semicolon.
<TargetFrameworks>net472;netstandard20</TargetFrameworks>

Save the project file and recompile your code now. Visual Studio will build two different versions of your code, one for each framework. Go to the output directory and notice it separates the outputs by framework. For packages you should be able to see multiple framework support automatically added as well. Clients using .NET Framework will use your .NET Framework version while .NET Standard/Core projects will use your .NET Standard version instead.

Updating Unit Tests

Currently .NET Core tests use dotnet while .NET Framework tests use vstest. At this time Visual Studio does not support multi-targeting tests. You have to choose a platform. By default it will be the first framework in the list. So if you want to run the .NET Framework tests normally then ensure it is the first framework listed. In a future version of Visual Studio multi-targeting support should be added for tests.

To help ensure you don’t run into issues be sure to add the AutoGenerateBindingRedirects element to each unit test so they get the appropriate redirects.

<PropertyGroup>
   <AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
</PropertyGroup>

Final Thoughts

Overall replatforming to .NET Standard is not particularly difficult for most libraries that are already being kept up to date. The biggest hurdles revolve around the tooling, project updates and reorganizing of the code to fit into the newer approach to packaging and builds. Microsoft has some porting guidelines available here.