2023-07-21

Migrating a project from DotNet Framework 4.7.2 to DotNet 7

I recently did this at work, and I decided to document the process here in the form of a how-to guide. Please note that I am not an expert, I am learning as I go along, so there may be mistakes.

Step 1: Convert all projects to sdk-style.

This is necessary for net7, and also a very useful thing to do even if we were staying in net472.  I cover it in another post: michael.gr - Converting MSBuild project files from legacy-style to SDK-style and it actually represents most of the work needed to migrate to net7.

Step 2: Change the actual version.

You might want to start migrating the projects one at a time, so that you do not migrate the entire solution at once. This will allow you to keep ensuring at each step that the entire solution still works.

A dotnet project may depend on dotnet-framework projects, but a dotnet-framework project may not depend on dotnet projects; therefore, if we want to migrate projects one at a time instead of all of them at the same time, then the first project that we migrate must be one which constitutes a root of a project dependency tree.

In our case, we are making a WPF application; so, in our solution we have one project which is a windows executable, and a multitude of other projects that are class libraries. The executable project directly or indirectly depends on the class libraries, but no class library depends on the executable project; therefore, the executable project is a root in the project dependency tree. So, that's the first project to migrate.

In your project file, replace the following:

<TargetFramework>net472</TargetFramework>

with the following:

<TargetFramework>net7.0</TargetFramework>

or, for a WPF project:

<TargetFramework>net7.0-windows</TargetFramework>

That's it, you can now build. Of course, it will not build. There is a number of issues that will need to be fixed. The issues that I encountered and had to fix are as follows:

Build Problem: More nullability issues

Net7 complains about nullability issues there were net472 did not. For example:

  • In a class you may have had `public override string Equals( object other )` and it may have worked fine, but you can't do that anymore in net7: the base `Equals()` method accepts a nullable parameter, and you cannot just waive the nullability of the original parameter in an override. So, it will now have to be `public override string Equals( object? other )`.

Annoyingly, the same applies to the `Equals` method of `IEquatable<T>`, but in this case for absolutely no good reason. That's just how it is, and we have to make do.

  • At some place I was invoking `new System.Threading.Thread( threadProcedure );` where `threadProcedure` was defined as `void threadProcedure( object data )`. The error was:
    `error CS8622: Nullability of reference types in type of parameter 'data' of 'void ServerThread.threadProcedure(object data)' doesn't match the target delegate 'ParameterizedThreadStart' (possibly because of nullability attributes).`
    As you can see, the error message even includes a hint which points to the exact problem, and the fix is to simply declare `data` as nullable: `void threadProcedure( object? data )`.
  • When declaring a new dictionary type of <K,V> you have to add `where K : notnull`.
  • Methods like `Dictionary.TryGetValue ( key, out T value )` need to be changed to `Dictionary.TryGetValue( key, out T? value )`.

Build Problem: GlobalSuppressions

I used to have a GlobalSuppressions.cs file with a bunch of [assembly: SuppressMessage( ... )] attributes for things like "ENC1003", "IDE0063", "IDE1006", etc. I did not know what to pass as "category", so I used to pass `null`. This does not work anymore, due to global nullability checking. 

To resolve this problem, there are three options:

  • Find the proper values to pass for category. (#AintNoBodyGotNoTimeFoDat)
  • Specify `#nullable disable` for this particular file. (Meh.)
  • Just delete this file, since we can now start making use of EditorConfig.

Needless to say, I picked the last option.

Build Problem: Types "forwarded" to nuget assemblies

I had a piece of code which was obtaining a windows service in order to restart it, with a line like this:

var serviceController = new SysServiceProcess.ServiceController( serviceName );

For this line, MSBuild started giving me the following error:

error CS1069: The type name 'ServiceController' could not be found in the namespace 'System.ServiceProcess'. This type has been forwarded to assembly 'System.ServiceProcess.ServiceController, Version=0.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a' Consider adding a reference to that assembly.

As you can see, the error message is quite descriptive, and even suggests a fix, which is almost correct. In this case, I had to add the following to my project file:

<PackageReference Include="System.ServiceProcess.ServiceController" Version="4.1.0"/>

In another instance, I had a piece of code that played a sound, with a line like this:

var player = new System.Media.SoundPlayer( soundPathName );

For this line, MSBuild started giving me the following error:

error CS1069: The type name 'SoundPlayer' could not be found in the namespace 'System.Media'. This type has been forwarded to assembly 'System.Windows.Extensions, Version=0.0.0.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51' Consider adding a reference to that assembly.

The fix was to add the following to my project file:

<PackageReference Include="System.Windows.Extensions" />

In some other piece of code, MSBuild started complaining that there exists no "Bitmap" type, even though it was entirely unclear why it was looking for type "Bitmap".  In any case, again it suggested to reference a particular assembly, and the problem went away.

Build Problem: System.Range

In our solution we used to have a type called `Range`. In modern dotnet a new type called `System.Range` has been introduced, and this caused ambiguous reference errors.

This can be solved either by renaming our own types, or by never directly importing external namespaces, and always using aliases instead. In other words, `using Sys = System;` instead of `using System;`

Build Problem: warnings about assembly conflicts

Not really an error, but I like my build to be issuing no warnings. 

MSBuild started complaining the following:

warning MSB3243: No way to resolve conflict between "System.IO.Compression, Version=7.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" and "System.IO.Compression". Choosing "System.IO.Compression, Version=7.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" arbitrarily.

One way to solve this problem is to find all occurrences of the following:

<Reference Include="System.IO.Compression" />

and replace them with the following:

<Reference Include="System.IO.Compression" Version="7.0.0.0" />

However, there is a better way to solve this problem: Just remove the reference! The build system is reporting a conflict between the assembly as referenced in the project file and the already-existing assembly in net7, so obviously, the assembly already exists, so the project does not need to explicitly reference it anymore.

Build Problem: System.Diagnostics.Debug.Listeners

I had a line like this:

System.Diagnostics.TraceListener listener = System.Diagnostics.Debug.Listeners[0];
MSBuild started complaining as follows:
error CS0117: 'Debug' does not contain a definition for 'Listeners'

The solution was to replace the above line with the following:

System.Diagnostics.TraceListener listener = System.Diagnostics.Trace.Listeners[0];

Build Problem: Thread.Abort() is obsolete

Code that makes use of 'Thread.Abort()' started giving the following warning:

warning SYSLIB0006: 'Thread.Abort()' is obsolete: 'Thread.Abort is not supported and throws PlatformNotSupportedException.' (https://aka.ms/dotnet-warnings/SYSLIB0006)

The solution was to fix the code so that it does not use `Thread.Abort()`. (It was a bad idea anyway.)

Build Problem: Empty macros in Post-Build-Step

My post-build-step was failing, because the macro $(ProjectDir) was empty. There are two possible solutions to this:

  • In the post-build-step, the correct magical incantation to use is $(MSBuildProjectDirectory)\ instead of $(ProjectDir).
  • Better yet, drop post-build steps, and instead go to project settings, and add a post-build target, which is the new dotnet way of specifying post-build steps.


Once the build problems were resolved, it was time to try running. Again, I did not expect the application to run, and in fact it did not run.

Here are the runtime problems I encountered, and how I resolved them.

Runtime Problem: App.config/system.data

My application would fail during startup with a "System.Windows.Data Error 17" saying that it could not get some value from some settings file. The stack trace was followed by the good old familiar nonsense: `TargetInvocationException:'System.Reflection.TargetInvocationException: Exception has been thrown by the target of an invocation` which essentially means "please keep reading". The next line was `System.Configuration.ConfigurationErrorsException: Configuration system failed to initialize`, which again says pretty much nothing, bringing us, finally, to the next line that mentions the actual problem: `System.Configuration.ConfigurationErrorsException: Unrecognized configuration section system.data. (my-application.config line 12)

So, it turns out that modern dotnet does not like the `<system.data>` section in App.config. In my case this section was empty, so all I had to do was remove the section.

Runtime Problem: non-null EventArgs

I am a control freak, so my System.AppDomain.CurrentDomain.ProcessExit event handler contained an assertion that the eventArgs parameter of that event is null, because I had observed it to be null under dotnet-framework.

As it turns out, in modern dotnet this parameter is not null anymore; it is a default instance of `EventArgs`.

The fix for this was to change the assertion to expect a non-null eventArgs from now on.

Runtime Problem: Accessing native DLLs

In our project we have a native DLL, which used to be placed in the same directory as the executable, but under dotnet-framework native DLLs are, by default, placed in special locations. In our case, our DLL was placed in <executable-location>/runtimes/win-x64/native, so it could not be loaded.

I could update the code to go looking for the DLL in that new location, but I decided to do something more simple; I added the following line to the project file:

<AppendTargetFrameworkToOutputPath>False</AppendTargetFrameworkToOutputPath>

Runtime Problem: Splash-screen weirdness

As soon as I managed to get my WPF application to run, I noticed something weird with the splash-screen: as our application was loading, the splash-screen would first appear stretched (in an ugly way) to a size that was larger than normal, and then it would shrink to its normal size but it would move to a location slightly to the left, and slightly above the center of the screen, where it would stay until our application would finally complete loading and the splash-screen would disappear.

As it turns out, this is a known bug in WPF see github dotnet wpf issue 947 and github dotnet wpf issue 5070.

The solution, (which is described here: https://stackoverflow.com/a/62141464/773113) was to write a couple of lines of code in `Application.OnStartup()` to create the splash-screen myself.


Once all of the above was done, it was time to try building the release version of our application. Here, I ran into the biggest problem:

Release Build Problem: ConfuserEx does not work anymore

In our application we use obfuscation. So far, this has caused us a lot of trouble: first we used to employ a tool called Confuser, which was later abandoned and re-incarnated as ConfuserEx, so we had to start using that one. Then, that one was abandoned too, so we had to find a fork of it that was still being maintained by someone.

Now with DotNet 7, ConfuserEx does not work for us anymore: it fails with a message saying that it does not know the executable file format. There is a question-and-answer where someone asks the author for a solution, and the author points them to an alpha build of some version 2.0 of that tool, but that was a long time ago, and the artifacts of that alpha build do not exist anymore.

There is a new tool called Dotfuscator, which is bundled with Visual Studio, but there are all kinds of serious problems with it, see another post of mine about that: michael.gr - On the "Dotfuscator" tool by PreEmptive Solutions. I am currently working on this, when I find a solution I will update this article with my findings.


This guide will be updated during the following days as I complete the migration of the entire application from DotNet Framework 4.6.2 to DotNet 7.

No comments:

Post a Comment