Last year, when Microsoft announced that .NET Framework 4.8 would be the last major release of the full .NET Framework and given that .NET Core was already largely used for cross-platform development, we thought the time to migrate from Framework to Core had definitely arrived.

Besides being cross-platform, .NET Core has better performance, it has several enhancements over the .NET Framework, and it’ll be with us for many years to come. Also, it’s open-source and not proprietary, as Framework used to be.

After .NET Core 3.x, the next major release would be .NET 5 (jumping over “version 4” only as a cosmetic change and dropping “Core” for simplicity’s sake, since there won’t be a “Framework” anymore). The .NET 5 full-version would be arriving  at the end of 2020, so we’d decided to start preparing our projects for what’s coming.

To help out anyone who might be going through the same, we decided to share things that have worked for us, as well as some lessons learned.

.NET Core Migration Recommendations

We’ve gone through the process, and we can split it into four main considerations.

  • Code analysis: learn how compatible your solution is with .NET Core framework.
  • Migration types: choose how to migrate based on how your software is developed and released. Understand where each project fits in the migration strategy, depending on its complexity and amount of Windows-specific code.
  • Migration of the .csprojs files: convert your project files to the mandatory Microsoft.NET.Sdk format.
  • Code migration: convert your C# code to .NET Core/.NET Standard.

A lot of migration processes will depend not just on your solution, but also on how your software is developed and released. So let’s dig in.

Code Analysis

Think about your solution in .NET Framework. Most of the codebase is platform-agnostic and will work in any operating system. Some of it, however, isn’t and will work only on Windows. Before any migration starts, you’ll want to know which are your problematic libraries.

Microsoft has developed a Portability Analyzer tool that focuses on analyzing your code and giving you a thorough report afterward regarding the compatibility between your current framework and the selected target frameworks (something along the lines of what’s shown in the image below).

Portability Analyzer tool

While the tool can give you a great starting point, don’t trust the results blindly. There are some false negatives, mostly regarding third-party libraries, so be sure to always validate with other tools, such as the .NET API Catalog that lets you check which .NET libraries are available and the Nuget alternatives for the ones that aren’t.

Migration Types

Migrating solutions with over 60 projects, some of them shared with other solutions, may be tricky… If you choose to do a direct migration from Framework to Core, you’ll have to do it completely, meaning that once you migrate one of the projects, all of them need to be in .NET Core. Since the codebases of .NET Framework and .NET Core are different, you cannot import Framework projects within Core projects and vice-versa.

If your software isn’t currently being distributed, you have a great opportunity of migrating in a single go. If, however, you have your software moving forward with ongoing development and releases, the one-go migration isn’t advisable.

At OutSystems, and specifically with our software Service Studio, with new development happening as we speak, we would need to make sure that:

  • Our current Windows software keeps working and keeps getting released as usual.
  • Teams continue developing and adding new developments to the current solution (Windows only) and the new solution (.NET Core cross-platform).

In our case, it would be impossible to migrate everything at once since it would compromise our current solution. One way to go is the shared API between Framework and Core — shown in the image below.

.net core and .net framework relationship
Image Link: Wikipedia

The Shared API (known as .NET Standard) is a subset of both the Framework and Core platforms, and it allowed us to have a more careful approach when migrating, by following these steps:

1. Divide the projects into two categories: those that only need some fine-tuning (easy to migrate) and those that need partial/full re-work, for example, projects with Windows Presentation Foundation (WPF) graphical components.

2. For projects that are easy to migrate:

  • You’ll need to migrate from .NET Framework to .NET Standard. They’ll constitute the shared codebase between the old Windows-only solution and the new .NET Core one, and both solutions will be able to consume them. Be sure to check a dependency graph of the solution and start with the projects at the base of the pyramid, meaning, projects which don’t depend on others.
  • If one of these projects belongs to multiple solutions, you may need to keep building the current framework version, and the .NET Standard one, for several reasons. You have two options here: multi-target or conditional building.
<!--Multi-target option-->

<!--Conditional building option-->
    <TargetFramework Condition="'$(Configuration)' != 'CustomConfiguration'">net461</TargetFramework>
    <TargetFramework Condition="'$(Configuration)' == 'CustomConfiguration'">netstandard2.0</TargetFramework>

The end result of each option is the same, but there’s a key difference between them. Multi-target always builds N framework versions, causing compilation to become slower. Also, you may end up with locked files due to N projects being built at the same time and accessing projects’ resources — this is why we have chosen the conditional building strategy whenever possible.

Tip: You can use Directory.Build.props file to declare global properties for your projects, namely a conditional building option for all .csprojs. The Directory.Build.props file affects all projects in the solution directory.

3. For projects which have either too many Windows-specific code, such as WPF components, or cumbersome code to migrate, such as SOAP introspection, you can keep them in .NET Framework while you define the strategy for porting them later on. You can also consider scratching them and starting a new one, based on other compatible libraries. When you do decide to port them, you’ll need to make new .NET Core projects where you start implementing their altered version.

4. Make a new .NET Core solution, where you’ll gradually import every migrated project. When the last piece of the puzzle fits you’re good to go and you’ll have your full software in .NET Core.

Tip: If you’re going to follow the multi-step strategy, you should also upgrade your projects which remain in .NET Framework to version 4.7.2. While you can still have .NET Standard projects alongside Framework 4.6.1 projects, it’s better to upgrade in our experience. It’s a trivial step most of the time and one which has the potential of saving you a lot of time due to frameworks’ incompatibilities — Microsoft also recommends it.

Migrating the .csproj Files

When you migrate a project to .NET Standard/.NET Core you’ll have to adopt the “Microsoft.NET.Sdk” csproj format. The process of changing your .csproj to SDK-style can be cumbersome, depending on the specifics of your project.

Here are the main differences (and some problems) that we have stumbled upon:

  • Files within the project are automatically added to the @(Compile) list. There’s no need for those long lists of <Compile Include=”..> items anymore.
  • If you are generating files during compilation time and wish to include them in your project, here comes trouble… First of all, you need to explicitly include said files, as you used to do. However, on the second or any subsequent build of the project you’ll end up with errors related to duplicates in the @(Compile) list. This is due to:

1. Files compiled and generated during the first build will be automatically added to the @(Compile) list because of the SDK-type csproj.

2. Files generated at compile-time by the Nth-build will be added to the @(Compile) list by the code in your .csproj where you explicitly include them.

To make this work, include the generated files only if they aren’t in the list yet:

<Compile Include="@(GeneratedFiles)" Exclude="@(Compile)" />

However, yet again, you may encounter a problem. The included files may have their full paths, while files from @(Compile) have relative paths only. Ultimately, we ended up with this solution for these cases:

<Target Name="IncludeGeneratedFiles">
        <CompileList Include="@(Compile->'%(FullPath)')" />
       <CompileList Include="@(GeneratedFiles)" Exclude="@(CompileList)" />
        <Compile Remove="@(Compile)" />
        <Compile Include="@(CompileList)" />

Also, note that if you’re generating typescript files at compile time that need to be embedded in the .dll, you’ll need to add this to the EmbeddedResource tag:

<EmbeddedResource Include="FileGenerated.ts" Type="Non-Resx" WithCulture="false" />


  • Assembly info files are now automatically generated. If you already have assembly info files and wish to maintain them, just make sure to add <GenerateAssemblyInfo>false</GenerateAssemblyInfo> to the PropertyGroup.
  • By default, the output path will now have the name of the target framework appended to. To disable it, use the following property:


  • Since the SDK-style implicitly imports Sdk.targets at the bottom of the file, extending MSBuild predefined targets such as <BuildDependsOn>, or <CompileDependsOn> at the beginning of the csproj will be overwritten and won’t work. You can explicitly add the import and add your extension afterward or you can change the way the targets are defined:
<!--Original. Won't work-->
<Project Sdk="Microsoft.NET.Sdk">
    <Target Name="CustomTarget">
<!--Solution one: add import explicitly -->
    <Import Project="Sdk.props" Sdk="Microsoft.NET.Sdk" />
    <PropertyGroup>        <TargetFramework>netstandard2.0</TargetFramework>
    <Target Name="CustomTarget">
    <Import Project="Sdk.targets" Sdk="Microsoft.NET.Sdk" />
<!--Solution two: adapt target. We recommend this approach-->
<Project Sdk="Microsoft.NET.Sdk">
    <PropertyGroup>  <TargetFramework>netstandard2.0</TargetFramework>
    <Target Name="CustomTarget" AfterTargets="Build">


  • Management of Nuget packages, which was done in Nuget 2 via package.config, is now achieved in a cleaner way via PackageReference:
      <PackageReference Include="System.IO.FileSystem.AccessControl" Version="4.5.0" />
      <PackageReference Include="System.Reflection.Emit.Lightweight" Version="4.3.0" />


  • Lots of unnecessary code in the SDK-format. While the garbage code usually still allows you to correctly build the project, it hinders the readability and maintenance of your .csprojs. We advise that you try and remove all unnecessary code. Example of code easily left behind:
<!--Old non-SDK format-->
<Project ToolsVersion="14.0" DefaultTargets="Build" xmlns="">
<!--New SDK-format, with unnecessary code-->
<Project Sdk="Microsoft.NET.Sdk" ToolsVersion="14.0" DefaultTargets="Build" xmlns="">
<!--New SDK-format, clean way-->
<Project Sdk="Microsoft.NET.Sdk">

To convert .csprojs to SDK-style format, there is an easy-to-use tool that can greatly help you in this task: CsProjToVS2017.

Migrating the Code

For the migration of the code per se, there isn’t a lot of generic advice to give since each project has its specifications and its share of non-compatible code. At this point, you should have a pretty good idea of the problematic third-party libraries which aren’t cross-platform and you’ll be looking at the alternatives for each.

If you’re migrating to .NET Standard, when looking for library replacements, if there’s no substitute in .NET Standard, that doesn’t mean that there isn’t a good candidate for library replacement.

As an example, we did a small test to confirm that System.Diagnostics.Activity class wasn’t a problem. In fact, this namespace doesn’t exist in .NET Standard 2.0 but it exists in .NET Core and in a Nuget Package, via the System.Diagnostics.DiagnosticSource class. The test consisted of generating a .NET Standard 2.0 library using the referred Nuget Package and of later importing said library in a .NET Core console app. The results were the same when running the console app on Windows and Mac.

Thus, when a library does not exist in .NET Standard, you must ask yourselves “Does it exist in .NET Core and via Nuget?”, which is a more relevant question to answer before starting to look into alternatives for non-existing libraries. Remember, .NET Standard is an intermediate step and the ultimate goal is to migrate your software to .NET Core.

There’ll also be occasions where you won’t find a library that works for all operating systems and you won’t be able to free your code from some operating system-dependent conditional region. An example of such is the code to manage the permissions of File/Folders. As of this moment, you don’t have any library in .NET that’s cross-platform and thus you need something on the lines of:

private static void SetPermissionsToEveryone(string directory) {
    if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) {
    } else {

private static void SetPermissionsToEveryoneUnix(string directory) {
    var unixFileInfo = new UnixFileInfo(directory) {
        // set file permission to 777
        FileAccessPermissions =
            | FileAccessPermissions.GroupReadWriteExecute
            | FileAccessPermissions.OtherReadWriteExecute

In the future, .NET Core will probably present us with an abstraction that deals with this in the background, but for now, that’s how we managed to get over the issue.

To finalize, there’s a package available to install in your project which tells you while coding if a given method exists for all target frameworks that your project has selected. It may come in handy.

Wrapping Up

In the end, even if you do everything right and the code is compiling, the process can still break at runtime. This will probably be caused by one of two reasons: either a method from one of your Nuget references isn’t implemented for the operating system you’re working on (even .NET Core Nuget References have unfinished i.e. NotImplemented methods) or the implementation for that operating system is faulty while for other one is stable.

For those reasons, make sure that you run your tests against the migrated codebase in all operating systems that your application is intended to be distributed to: don’t expect to have a stable version of your application running on MacOS just because all the Windows tests were successful!

So this is what worked for us, and the many lessons learned. It wasn’t always easy, and it took us some time, but we eventually got there. We hope you get there too!