Software developmentVisual Studio

Resolving cached NuGet packages at runtime

Roughly seven months ago I came across an odd behaviour of Visual Studio that I thought wasn’t possible and that broke my code at runtime. I asked on Stack Overflow hoping that someone could point me in the right direction, but that didn’t happen and Google wasn’t very helpful either. Curiosity finally made me tackle the issue myself once I found the time for it. This article is basically Helmuth von Moltke’s theory of war set in a programming scenario, and it serves as a good example of how important it is to adapt your design during development.

No design or architecture extends with certainty beyond the first encounter with a user or a working prototype.

Helmuth von Moltke the Elder (if he had been a nerd)

Before I talk about the solution I should probably explain the problem at hand. Usually, when Visual Studio compiles a project it will generate the binary along with any generated debug symbols and documentation files. All referenced libraries that the project depends on are also copied, so that the .NET runtime is able to find and load all relevant dependencies. In my little world, this behaviour has been a hard rule that I could depend on at all times. As it turns out, my little world has been .NETFramework for the better part of my career and all is fine there. In the worlds of .NETCore and .NETStandard however, there be dragons, and we are sailing past the edge of the map. Here, the output isn’t supposed to be deployable and calling dotnet publish is required to copy dependencies. The solution seems easy enough. Attach to the AppDomain.AssemblyResolve event and have a resolving method to look for a library file that matches the given assembly name.

public interface IResolver {

	Boolean TryResolve(ResolveEventArgs e, out Assembly asm);

}

// ...

private Assembly OnAssemblyResolve(Object sender, ResolveEventArgs args) {
	IResolver resolver = new Resolver();

	if(resolver.TryResolve(args, out Assembly asm)) {
		return asm;
	}

	// try other things ...

	return null;
}

Both the NuGet cache location and the assembly name are known and the resolving method will gather an assembly instance that can be returned. The interface is looking good enough and is ready to be implemented.

What if there is more than one match?

Versioning of assemblies is inherently hard and there is an ongoing debate on how it should be done. Some developers only use the major in the assembly version which will eventually result in two libraries using the exact same name and assembly version but different package versions. The files will also be different but a resolver looking for just the name and the assembly version is going to find two identical candidates. In reality there is a good chance that a developer made a breaking change but only bumped the minor or patch numbers. This only adds to the mayhem since there may be just one compatible assembly, but the resolver can’t know which one it is.

A good idea is to get the one with the highest package version hoping that people try to stay up to date. An even better idea might be to let the consumer decide and give them the whole list sorted by package version in descending order. But we can’t have the resolving method return a bunch of assembly objects. I think it’s safe to assume that there is at least one common type in each assembly, and it may be unnecessarily difficult to load them all at once. Also, I really don’t want to load assemblies that I don’t need, so I’d prefer to stick to just one assembly object.

This is a good time to think about separation of concerns. A method that resolves an assembly may not be the best place to actually load what it resolved. Loading is a required second step that may fail so the Try-Pattern may be in order. Only if both steps succeed can a valid assembly object be returned.

public interface IResolver {

	Boolean TryResolve(ResolveEventArgs e, out IEnumerable<FileInfo> files);

}

// ...

private Boolean TryLoadFrom(FileInfo file, out Assembly assembly) { /* details */ }

private Assembly OnAssemblyResolve(Object sender, ResolveEventArgs args) {
	IResolver resolver = new Resolver();

	if(defaultResolver.TryResolve(args, out files)) {
		foreach(FileInfo file in files) {
			if(TryLoadFrom(file, out Assembly assembly)) {
				return assembly;
			}
		}		
	}

	// try other things ...

	return null;
}

The resolving method is now responsible for resolving only, which means it should search for files that match certain criteria. A candidate file must be named <assembly name>.dll and be part of a package with the name of <assembly name>. The AssemblyName must match the request and both ProcessorArchitecture and TargetFramework must suit the currently running process.

I realize that resolving and loading package references at runtime is nothing more than a very rare edge case and I would argue that it’s most likely a very bad idea as well. You probably shouldn’t assume that anyone running your code is going to have a specific NuGet package cached on their system but not GAC’ed. If you need the wacky way of resolving cached NuGet packages, then you can find the released resolver on NuGet and GitHub.

Leave a Reply

Your email address will not be published.