Why change an assembly version when making a new assembly release, or how to break Visual Studio with a single command
Usually, when developers make a new release of an assembly, they also change its version. Changing the version is particularly important when developing a library on which other projects depend. But what happens when developers don’t change the library version? Let me tell you a short story about problems we encountered when using Microsoft libraries.
I’m closely involved in the development of the PVS-Studio C# analyzer. The analyzer uses ‘Microsoft.Build.dll’, ‘Microsoft.Build.Framework.dll’, etc. These libraries allow PVS-Studio to get various information from the project file. Thus, the analyzer can perform a deeper analysis of the projects.
Microsoft developers decided that the version for all these assemblies should always be 15.1. It doesn’t matter if the public interface changes, if new types appear, or if old types change, the version should always be the same. The corresponding NuGet packages change versions, but the assemblies do not. “Why?” I inquired. The msbuild repository maintainer responded:
This is intentional, and allows API client applications to work with multiple versions of MSBuild.
Well, Microsoft developers are running the show. Anyway, this approach doesn’t seem to bother anyone.
Or it does?
Der Typ “Microsoft.Build.Framework.Traits” konnte nicht geladen werden
We received the message from our client, who reported a crash of the analyzer. With the help of a translator, we figured out that the required Microsoft.Build.Framework.Traits type was missing from one of our dependencies — ‘Microsoft.Build.Framework.dll’.
As expected, we failed to reproduce the crash. The needed assembly is always installed in the folder with PVS-Studio.exe, and the required type is always included in this assembly. So, what was the matter?
Not long ago, we updated all the ‘Microsoft.Build.*’ dependencies to support the analysis of .NET 7 projects. We examined the previous versions of the analyzer and found out that the ‘Microsoft.Build.Framework.dll’ library doesn’t have the required type. Could our client’s analyzer have been updated incorrectly in some way?
Not at all. We asked our client to send us the dll file, and it was fine — the file contained the required type. Then we sent our client a special version of the analyzer that would log the paths used to load the assemblies at runtime. It turned out that the wrong library was loaded at runtime. That was not the library next to the executable file, but its ‘analog’ from the global assembly cache (GAC).
It’s finally making sense!
That’s what we have:
- the analyzer relies on the new release of Microsoft.Build.Framework 15.1;
- the GAC has an old release of this assembly, but its version is also 15.1;
- at runtime, the analyzer asks the CLR for Microsoft.Build.Framework version 15.1;
- CLR graciously provides the analyzer with the assembly of the “right” version from the GAC and ignores the one that lies next to the executable file;
- as a result, an old assembly is loaded. When we access it, it turns out that the assembly doesn’t have Microsoft.Build.Framework.Traits.
Testing this hypothesis was also quite simple: we needed to add an older release assembly to the GAC to reproduce the crash. Then something even more interesting happened: Visual Studio 2022 suddenly stopped working. When we opened it up, we got a message stating that something went wrong:
As a professional and experienced developer, I choose to ignore it and just clicked “yes”, and the message was gone. Good thing. Then I opened the simplest console project, but it was in a dismal state:
I’m a simple man. I see ‘unloaded’, I click ‘reload’.
Immediately I got a familiar message (not in German, though):
This was the exception that our client encountered.
This exception is also quite simple to reproduce:
- download the Microsoft.Build.Framework 17.0 package;
- find there an assembly for net472;
- add this assembly to the GAC in one of the ways described (for example, you can use the gacutil command);
- Congrats! You’ve just broken your Visual Studio 2022, and in particular — MSBuild.
Our client certainly had no intention of breaking their Visual Studio 2022. They even didn’t have VS 2022, so they didn’t notice its crash. Instead, the analyzer, which depends on the new MSBuild libraries and therefore always keeps them around, took the brunt of the crash.
Apparently, this assembly should be present in the GAC for some environment to work, so this assembly cannot be removed. Instead, our client updated the assembly in the GAC, and nothing seemed to break :).
In this issue, MSBuild maintainer Rainer Sigwald took some time to answer my questions, for which I am very grateful. In short, he told me the following:
- the versions of the assemblies are intentionally the same because it is convenient for some purposes;
- do not install Microsoft.Build.* assemblies in the GAC.
We couldn’t find any ways to bypass the GAC when loading an assembly at runtime :(
Let’s sum it up
First, it’s better not to add Microsoft.Build.* version 15.1 in the GAC. This may break MSBuild (and, just as importantly, this may break PVS-Studio).
Second, if possible, it is always better to update the assembly version if it has been changed. This way, you can protect your users from potential issues like those described in the article.
Third, if you are already using PVS-Studio to search for errors in your project and you encounter the problem described, you already know what the issue is and what to do.
That’s all for today. Good luck!