I was recently tasked to migrate an application with around 150 projects from the "old"It is still supported .NET Framework 4.8 to a recent .NET 6. As the application is still under development and used, the migration should be done step by step over time in iterative steps rather than a big bang refactoring. This blog post will go a bit into more detail about how I approached the situation and what I learned.
This is a highly subjective post, and there are plenty of ways of approaching it - so take it with a grain of salt and mileage may vary.
The application
The application itself isn't that special - at least not from the point of view of the migration. It is a typical 3-tier application with an Angular Frontend and a .NET Framework 4.8 backend. The backend is split into multiple projects, most of them are smaller tools like console applications or library projects. And then there are two ASP.NET Core WebAPI's and some Windows services that are done in TopShelf.
The task
As said earlier, the application is still under development - doing the Big Bang was not a viable option. And even if there is no one currently working on the application, it is still not a good approach in this scenario. So I needed a concept to go from .NET Framework 4.8 to .NET 6 step-by-step and in an iterative way. Best case scenario: You have multiple JIRA Tickets that can be done from time to time until the whole application is migrated.
Before we go into detail we have to clarify something really important: Some terminology, because this will be important throughout the whole post.
.NET Standard / .NET Framework / .NET
We have to understand what are these 3 different words. Let's start with the official picture from Microsoft:
Source: https://learn.microsoft.com/en-us/dotnet/standard/library-guidance/cross-platform-targeting
- .NET Framework 4.8 describes the "old" .NET that runs only under Windows (I excluded Mono here). It is not cross-platform capable.
- .NET 6 The "modern" .NET that is cross-platform capable. It is the successor of .NET Core 3.1 and .NET 5.0. It is the future of .NET and the default for new projects.
- .NET Standard is a specification that describes the API surface of a .NET implementation. It is a subset of the .NET Framework and .NET. It is used to create libraries that can be used by both .NET Framework and .NET. It is not a runtime, it is just a specification.
The last part sounds "odd". But imagine .NET Standard like an interface (or multiple thousand interfaces and classes) that a client has to implement. The client would be .NET Framework or .NET.
interface ITask
{
void Execute();
}
class NetFramework48 : ITask
{
...
}
class Net6 : ITask
{
...
}
Obviously, it is not like that - but from an oversimplified mental model, you can think of it like that. Why is this important? Well, there is a thing called .NET Standard 2.0 and the beauty is that .NET Framework 4.8 and .NET 6 implement that. That means if you have a library that is .NET Standard 2.0, you can use this library in both .NET Framework 4.8 and .NET 6. And that is the key to the migration (at least in this scenario). Microsoft official supports .NET Standard 2.0 but they will not create new standards. So you should not rely on them anymore and migrate sooner or later to .NET 6 or later.
Groundwork
Before we can start the migration itself, we hvae to do some groundwork. And this is independent of the next steps - but big benefit: You already gain some benefits from it. Migrate to the SDK-style csproj format. If you forgot how the old csproj format looks like, here is a reminder:
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{YOUR-PROJECT-GUID}</ProjectGuid>
<OutputType>Exe</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>YourProjectNamespace</RootNamespace>
<AssemblyName>YourProjectName</AssemblyName>
<TargetFrameworkVersion>v4.8</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
<Deterministic>true</Deterministic>
</PropertyGroup>
<ItemGroup>
<!-- Add your references here. An example to add System.Data.dll is shown -->
<Reference Include="System.Data" />
<!-- Add your source files here. An example to add Program.cs is shown -->
<Compile Include="Program.cs" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
Other similar extension points exist, see Microsoft.Common.targets.
<Target Name="BeforeBuild">
</Target>
<Target Name="AfterBuild">
</Target>
-->
</Project>
And this example is almost the smallest you can get - look at the modern sdk style and this is a reasonable big project file! On top, in the old world you also have a packages.config file that contains all the NuGet packages. It seems cumbersome to migrate projects, but it is not really. There are plenty of good tools doing the heavy lifting for you. The main tool I used is the Upgrade Assistant from Microsoft. This tool can migrate your projects to the new format and also look for NuGet packages that are incompatible.
Yes that is something you also have to do sooner or later: Migrate your NuGet packages. For the sole migration of the project format it is not necessary but for the migartion process as a whole it is.Basically I went through all projects and checked whether or not the NuGet package is compatible with the .NET Standard 2.0. If so, you don't have to do anything. Again we want to do this step by step. You can move to a new major version or replace it after your migration is done. It is really important to stay on track!
For packages that are not compatible, you have to find a replacement. And this is the tricky part. There are plenty of packages that are not compatible with .NET Standard 2.0. Sometimes you have to find a new major version and sometimes a completely new approach. For example, we had TopShelf in use - but that isn't maintained anymore. More to that later. For now, you should have all references up to date or at least compatible with .NET Standard 2.0.
By the way the Upgrade Assistant can also help you to get rid of the <Reference Include="" />
tags, as they are also not supported anymore. More often than not, they can be replaced by a NuGet package from Microsoft. If you are only interested in migrating your project files without anything else, there is also a tool called CsprojToVs2017 that can do the job for you - at least to some extend. For example we had some pre- and postbuild tasks. Unforunately, it didn't work out from the getgo and I had to migrate them by hand afterwards. But hey - still tons of time saved!
Independent of the tools you are using - make a concrete plan first! Don't just rush into a migration or you will get lost.
Console Application
In our case we could start very easy - we had some console applications that had literally no references to any library (if so you have to start with the next step, I will describe afterwards). It was plain simple to replace:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
- <TargetFramework>net48</TargetFramework>
+ <TargetFramework>net6.0</TargetFramework>
<OutputType>Exe</OutputType>
</PropertyGroup>
And we were done. Yes the compiler might complain, but issues were easy to fix! If you have a console application that references libraries, you have to do the next step first.
Migrate library project to .NET Standard 2.0
Here we can use the power of .NET Standard 2.0. When we migrate a library to .NET Standard 2.0, we can use it in both .NET Framework 4.8 and .NET 6. As with the console application described above you can set the TargetFramework
to netstandard2.0
. Now there can be things that will not work. In our case the biggest issue was HttpContext.Current
. HttpContext.Current
was used in a library project, and that thing doesn't exist anymore in the modern .NET world - which is good! But for a migration that goes step by step, we need that thing for the time being.
We can make an easy transition for that. The library project aka .NET Standard 2.0 was already using Dependency Injection - so we can leverage that. Instead of HttpContext.Current
we now inject the IHttpContextAccessor
that is the default in the modern world. But wait? Someone has to register that thing? What I did is this, in my ASP.NET Web API project I created an implementation and registered it into the Autofac container:
public class HttpContextAccessor : IHttpContextAccessor
{
public HttpContext HttpContext
{
get => HttpContext.Current;
set => throw new NotSupportedException("This is only for migration purposes");
}
}
A consuming service now only gets the IHttpContextAccessor
injected and can use it like this:
public class MyService
{
private readonly IHttpContextAccessor _httpContextAccessor;
public MyService(IHttpContextAccessor httpContextAccessor)
{
_httpContextAccessor = httpContextAccessor;
}
public void DoSomething()
{
var context = _httpContextAccessor.HttpContext;
// do something with the context
}
}
You can do this step by step until all of your library projects are migrated.
Unit tests
Unit test projects are a bit tricky. You can't just migrate them to .NET Standard 2.0. Basically .NET Standard is no runtime - so you can not execute your tests. There are two ways - if they only depend on now migrated projects that target .NET Standard 2.0, you can upgrade them to .NET 6. If they are still referencing .NET Framework 4.8 projects, you have to wait until those are migrated as well!
ASP.NET Web API - Migrate to ASP.NET Core 6
This is the biggest part of the migration. Well - at least to some extend. I would suggest create a new project from scratch and add the files you had before. Some things like Global.asax
don't exist anymore and you have to migrate them manually.
Global.asax has some entry points like Begin_Request
, Application_Start
and so on. Application_Start
is now in your Program.cs
where you configre the application. Begin_Request
and End_Request
are now middlewares. Here an example of such a middleware:
public class MyMiddleware
{
private readonly RequestDelegate _next;
public MyMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task Invoke(HttpContext context)
{
// Begin_Request
await _next(context);
// End_Request
}
}
Basically all of those events are mapable to the new .NET 6 world. Another thing you will discover is that the WebApiConfig
doesn't exist anymore either - this is now your Startup.cs
or in the newest template also part of your Program.cs
. Another change we had to make while migrating is the SignalR
package - they are not compatible between .NET Framework 4.8 and .NET 6. This also had direct impact on our frontend as we had to switch here as well.
TopShelf
We used TopShelf for creating Windows services. But this is not maintained anymore. So we had to find a replacement. The goto approach is to use a BackgroundService
. There is a whole article from Microsoft about that. But it is your everyday BackgroundService
so I will not go too much into detail here.
Migrate to .NET 6
Once all our projects are on either .NET Standard 2.0 or .NET 6 it is time to migrate the .NET Standard 2.0 ones to .NET 6. As I said earlier, .NET Standard 2.0 shouldn't be used anymore and .NET 6 has a richer API. So all of your libraries should be migrated to .NET 6. But this is a very easy step - just change the TargetFramework
to net6.0
and you are done.
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
- <TargetFramework>netstandard2.0</TargetFramework>
+ <TargetFramework>net6.0</TargetFramework>
</PropertyGroup>
It should also compile - because as said, they are compatible!
Outlook
Now that you migration is completly done and well-tested, you can think of all the new things you can do. For example we used Autofac and decided to go with the default of ASP.NET Core. Anyway, I would argue do the migration without much refactoring or introduction of new concepts as it makes it much harder than it could be! Afterward, you can improve whatever you want.
Conclusion
I hope this article helps you to migrate your .NET Framework 4.8 application to .NET 6. Or it helps you to get a better understanding of the steps and underlying mechanism!