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.

Step 0: Converting from packages.config to PackageReference

If the project files are using packages.config, they need to be converted to PackageReference-style. We live in the 3rd millennium, we should act like it.

Step 1: Fixing nullability

Nullability should be handled correctly in the entire solution. You can begin by adding `#nullable enable` to individual source files and fixing them; eventually, you remove `#nullable enable` from the individual source files and add <Nullable>Enable</Nullable> to the project files.

If fixing the nullability is difficult in some source files, you can add `#nullable disable` to those files only; however, I strongly advice against this; handle nullability correctly everywhere, and do not proceed unless you have gotten it right.

Step 2: Converting projects to sdk-style projects (but staying in net472 for now)

For net7 we will need sdk-style projects. As it turns out, we can have sdk-style projects even in net472, so it is wise to convert all projects to sdk-style before upgrading to net7.

I replaced the following tag:

<Project ToolsVersion="14.0" DefaultTargets="Build" xmlns="http://schemas...

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

I replaced the following tag:


with <TargetFramework>net472</TargetFramework>

Then, I removed the following tags:













I left the following tags as they were:



<StartupObject> (if any)

<ApplicationIcon> (if any)


And I added the following tags:






<UseWPF>true</UseWPF> (for a WPF project)

<UseWindowsForms>True</UseWindowsForms> (for a WPF project -- don't ask.)

Then, I arrived at the most enjoyable part:

  • Most <Reference...> items for things like "System", "System.Data", "System.Xml" etc. were removed.
  • All <ProjectReference...> items became one-liners since neither project guid nor name is necessary anymore.
  • All <PackageReference...> items also became one-liners since the version does not have to be a nested tag, it can be an XML attribute.
  • Hundreds of lines of XML that reference individual .cs and .xaml files, as well as the associations between them, were removed. In our case this resulted in a 12:1 reduction in project file size.
  • References to included resources stayed of course, as well as references to anything else that needs special handling.
  • The importing of Microsoft.common.props and Microsoft.CSharp.targets was removed.

Once this is done, or even while doing it, various problems might pop up.  For example:

  • At some point Visual Studio started skipping the building of a project, so the projects that depended on it would fail. If I tried to clean that project, Visual Studio would again skip that project, so it would not do any cleaning. Forcibly cleaning by deleting all the bin and obj directories had no effect; restarting Visual Studio had no effect; enabling more verbose build output (even diagnostic-level) did not reveal the slightest hint as to why Visual Studio was skipping the project. That was very frustrating. After some googling around, gathering a list of magical incantations, and trying them one after the other, the one that worked for me was unloading the project and reloading.
  • At some point I was receiving an error telling me that one of my WPF applications was missing a "Main" entry point. However, its project file was for all practical purposes identical to the project file of another WPF application that was building just fine, and suffice it to say, neither of the two  applications had a "Main" entry point. As it turns out, the application object must be called "App.cs" and "App.xaml"; if you rename it, the magic does not work anymore.
  • At some point Visual Studio was launching one of my console applications passing it all of its command-line arguments twice. Visual Studio stopped doing that after it was restarted.
  • Some native libraries could not be loaded anymore.
  • You may encounter different problems, or even if you encounter problems that seem similar, you may need different magical incantations to overcome them.

After all this, the "dotnet build" command should also start working for the solution. Unfortunately, this may and may not happen, again due to a variety of reasons.

Step 3: Actually migrating from net472 to net7

We want to start migrating the projects one at a time, so that we do not have to migrate the entire solution at once, and so that we can keep ensuring that the entire solution still works at each step.

A dotnet project may depend on dotnet-framework projects, but a dotnet-framework project may not depend on dotnet projects; therefore, 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 the project file, I replaced the following tag:


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

(for a WPF application)

Once the above was done, it was time to try building. I did not really expect it to build, and in fact it did not build. There were a number of problems that needed to addressed on a case-by-case basis.

Here are the build problems that I encountered, and how I solved them.

Build Problem: Referencing standard assemblies.

It turns out that in modern dotnet not only you do not have to reference standard assemblies anymore, but actually you should not reference them.

In our case this was simply done by editing our project file and removing the entire <ItemGroup> with items like <Reference Include="System" /> and <Reference Include="System.Xml" /> and the like.

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 I had 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: AssemblyInfo.cs

Most of the assembly attributes defined in AssemblyInfo.cs were causing duplicate attribute errors, because in modern dotnet these are automagically generated for us.

The solution was to very happily remove all those attributes.

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: App.config/system.data

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.

Build Problem: Empty macros in Post-Build-Step

My post-build-step was failing, because $(ProjectDir) was empty. It turns out that in the post-build-step, the correct magical incantation to use is $(MSBuildProjectDirectory)\ instead of $(ProjectDir).

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: 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:


Runtime Problem: System.Diagnostics.Debug.Listeners

For some unfathomable reason, code accessing the System.Diagnostics.Debug.Listeners property compiles correctly, but fails at runtime with a "no such method" exception when trying to invoke the getter of that property.

The solution to this is simple enough: replace all references to System.Diagnostics.Debug.Listeners with references to System.Diagnostics.Trace.Listeners.

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.

Runtime Problem: Custom-built resources

In my application I have icons in SVG format. WPF has no support for SVG, so conversion to XAML is necessary. A long time ago I decided to handle this as follows:

I added a custom build target that would convert the SVG files to XAML during build, then these XAML files would be included as resources into my application in a kind of mysterious way which I did not quite understand myself, and then my application would have access to the icons as XAML.

It is no surprise that clunky tricks like this break when you try to make a significant change, such as upgrade to another version of dotnet. In our case, the custom build target did in fact run, but the XAML files that it generated were not being magically included as resources in our application anymore, so all of those icons failed to load, and they were completely blank on the screen. Furthermore, I had no idea how to fix this, and becoming an expert in this monstrosity known as MSBuild was not in my immediate or even long-term goals.

The solution was to ditch the svg-to-xaml build target, to include the original SVG files as resources into the application, and to do the necessary conversions from SVG to XAML at runtime.

Runtime Problem: non-null EventArgs

I am a control freak, so my System.AppDomain.CurrentDomain.ProcessExit event handler contained an assertion to ensure 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.

Once all of the runtime problems were resolved, I was able to perform the following additional improvements:

Replaced a whole bunch of resource-include statements like <Resource Include="Art/Icon/Checkmark.svg" /> with a single resource include statement: <Resource Include="Art/Icon/*.svg" />. (And if I wanted to include all SVG files under `Art`, I could have used <Resource Include="Art/**/*.svg" />)

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.

So, it seems that we are going to have to abandon ConfuserEx and start using a new tool called Dotfuscator, a free version of which is included with Visual Studio. I am currently working on this, as soon as I am done 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