2023-09-05

Converting MSBuild project files from legacy-style to SDK-style

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.

Sdk-style project files have existed since net5. The kind of project files we were using before that can now be called legacy-style project files.

  • Legacy-style project files begin with <Project ToolsVersion="...
  • Sdk-style project files begin with <Project Sdk="Microsoft.NET.Sdk">.

Sdk-style project files are necessary if you want to:

  • Start using the `dotnet` command-line utility and all the functionality that it provides.
  • Eventually migrate to a modern version of dotnet.

Note: If your legacy project files are using packages.config, they first need to be converted to PackageReference-style. We live in the 3rd millennium, we should act like it. Converting from packages.config to PackageReference is beyond the scope of this guide.

Here are the steps I followed:

I replaced the <Project ToolsVersion=... tag with <Project Sdk="Microsoft.NET.Sdk">.

I replaced the <TargetFrameworkVersion>v4.7.2</... tag with <TargetFramework>net472</...

I removed the following tags:

<ProjectGuid>

<TargetFrameworkProfile>

<FileAlignment>

<AutoGenerateBindingRedirects>

<Deterministic>

<NuGetPackageImportStamp>

<AssemblyName>

<AppDesignerFolder>

<ProjectTypeGuids>

<XamlDebuggingInformation>

<Prefer32Bit>

<ErrorReport>

I left the following tags as they were:

<RootNamespace> (This is only necessary if the name of the project file does not exactly match the name of the root namespace.)

<OutputType>

<StartupObject> (if any)

<ApplicationIcon> (if any)

<PlatformTarget>

I added the following tags:

<Platforms>AnyCPU;x64</...

<ImplicitUsings>disable</...

<Nullable>enable</...

<TreatWarningsAsErrors>True</...

<NoWarn>NU1701;NU1702</...

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

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

Then, I arrived at the most enjoyable part:

  • Dozens upon dozens of <Reference Include=...> items for things like "System", "System.Data", "System.Xml" etc. were removed. A few had to stay, for example:
    • System.Printing
    • ReachFramework
    • System.IO.Compression
    • Microsoft.VisualBasic
    • System.ServiceProcess
  • 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 then reloading it.
  • 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.xaml.cs" and "App.xaml"; if you rename it, the magic does not work anymore, or perhaps it needs an <ApplicationDefinition Include="MyCustomApp.xaml" /> in order to work.
  • 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.
  • You may encounter different problems, or even if you encounter problems that seem similar, you may need different magical incantations to overcome them.

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 SDK-style projects not only we do not have to reference standard assemblies anymore, but we must actually refrain from referencing them. In our case this was fixed 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: Duplicate attributes in AssemblyInfo.cs

Most of the assembly attributes defined in AssemblyInfo.cs were causing duplicate attribute errors, because in SDK-style projects these are automagically generated for us. The solution was to remove those attributes.

Build Problem: PresentationUI assembly not found

This is a very strange problem which I was unable to either understand or properly solve. It may be related to the following discussions:

github/dotnet/wpf: "PresentationUI ref-assembly missing: Build fails because cannot find type PresentationUIStyleResources"

github/dotnet/runtime: "WPF has removed PresentationUI ref assembly"

Luckily, the assembly was not necessary, so I was able to remove it without losing any functionality.

Build Problem: Other assemblies not found

This is another strange thing which I was also unable to understand.  Examples of assemblies that could not be found anymore: Microsoft.Bcl.HashCode, System.Collections.Immutable, and JetBrains.Annotations.

I solved this problem by simply avoiding the use of those assemblies and either forfeiting their functionality or implementing it by myself.  

  • System.HashCode was very easy to re-implement.
  • System.Collections.Immutable turned out to be unnecessary.
  • JetBrains.Annotations turned out to also be unnecessary.


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 few natives DLLs which would fail to load under the SDK-style project.

  • In one case, the solution was to modify the code that loads the DLL to look for it not only in bin\x64\Debug but also in bin\x64\Debug\runtimes\win-x64\native and in bin\x64\Debug\runtimes\win-x64.
  • In another case, the solution was to add <AppendTargetFrameworkToOutputPath>False</... to the project file.
  • In another case, the solution was to add the following tags to the project file:
<RuntimeIdentifiers>win-x64</... 
<RuntimeIdentifier>win-x64</...
  • And in an especially difficult case, the solution was to add a post-build step which copies everything from bin\x64\Debug\runtimes\win-x64\native to bin\x64\Debug.

Runtime Problem: Custom-built resources

In my application I have icons in SVG format. WPF has no built-in support for SVG, so conversion of SVG 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 change the style of the project files. 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: no splash-screen

The solution to this problem, (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 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" />)


No comments:

Post a Comment