In the previous part we learned how to make a single executable run on different CLR versions. This time we’ll examine how library versioning and dependency management works in the .NET world. We’ll later need this as a basis to understand the different techniques for targeting multiple .NET Framework versions.
When you compile a C#/VB/etc. project the final output file is called an assembly. Assembly files have the ending
.exe if they contain an entry point (a
Main method) and are intended for direct execution or
.dll if they are intended to be referenced as libraries. They are somewhat similar in their function to
.jar files from the Java world.
Assemblies are uniquely identified by their “full name”. This consists of the file name without the file ending, the version number and optionally a public key for strong naming. Each assembly contains a list of all other assemblies it references, identified by their full names. These references are resolved at runtime when a type located in an external assembly is accessed for the first time.
The runtime first tries to find referenced assemblies in the Global Assembly Cache (GAC). This is a system-wide directory used to store shared libraries. All libraries distributed as a part of the .NET Framework are installed here. Third-party libraries can also be added to the GAC, however they are required to strictly adhere to proper versioning.
Next, the runtime looks for assemblies in the directory the
.exe was started from. By placing all third-party libraries here rather then depending on them being in the GAC an application becomes “xcopy deployable”. This means simply copying a directory to a new machine is enough to “install” the application. This of course negates the advantages of shared libraries, such as conserving disk space and allowing for centralized updating of libraries.
In Visual Studio we can specify for each library reference whether we want a local copy of the library to be added to the build output directory. We are basically determining whether the library is expected to be in the GAC or not.
NuGet is a library package manager for .NET. Unlike build management tools like Maven for Java, NuGet only operates at development time and not at build time. When a NuGet package is installed the library it contains is added as a reference with
Copy Local set to
True. The underlying build system MSBuild has no knowledge about packages and treats these references like any other. The provocatively named post NuGet: Broken By Design on NuGet’s own blog nicely explains the reasoning behind this design and future plans relating to .NET Core.
One of the results of this design is that installing a NuGet package in your project also installs all of its dependencies. At first glance this might seem excessive. After all, when we install package
A we might not care that it uses package
X transitively and certainly don’t want an implicit reference to
X to be added to our project. However, if we let MSBuild compile a project that only references
Copy Local to
True) it would have no way of knowing which of
A‘s assembly references point to libraries not expected to be in the GAC on target machines. So
X would not be included in the output directory. By making sure a project references the “transitive closure” of all of its explicit dependencies MSBuild is guaranteed to create a complete and “portable” output.
But what happens if multiple libraries reference different versions of the same library? Let’s say we add
A 1.0 to our project which references
X 1.0 and
B 1.0 which references
X 2.0. This would result in references to
B 1.0 and
X 2.0 (replacing
X 1.0) in our project. However, at runtime the CLR would see that
A 1.0 specifically references
X 1.0 and not
X 2.0 (remember, the full name includes the version number).
NuGet gets around this by adding a so called binding redirect to the project’s
web.config file. A binding redirect tells the CLR to consider one version of an assembly as a replacement for another. This could look something like this:
<dependentassembly> <assemblyidentity name="X" publickeytoken="123abc"> <bindingredirect oldversion="1.0" newversion="2.0"> </bindingredirect></assemblyidentity></dependentassembly>
Now that we know how library versioning works in the .NET world, we are ready to tackle versioning of the .NET Framework itself one last time. In the final part of this series we will learn how to build NuGet packages that target multiple .NET Framework versions.