TL;DR: I want to find a way in C# to call Assembly.Load or an equivalent, and have it ignore any binding redirects from config files.
I've been running into a lot of DLL Hell issues lately with .NET, and the latest one has sent me down a bit of a rabbit hole, and now I want to know if I can write some C# code to load assemblies but force it to ignore some faulty binding redirects.
So, the basic problem is this: We've got some C# projects that have Nuget package references, and some of those packages point to different versions of another package (System.Collections.Immutable in this case, but it's come up with other stuff too).
The first step to fixing those is typically to turn on the VS option to automatically generate binding redirects. Normally works just great. I did that, this time the problem persisted.
The next step is to get into AppDomain.AssemblyResolve and handle that, which worked for most situations, but not this time. It was looking for a specific version of the assembly, and no matter what I tried with Assembly.Load or Assembly.LoadFrom the binding redirect kicked in and it went looking for a different version, which doesn't exist.
I ended up finding a random place where someone (probably me) had hard-coded a faulty binding redirect into an app.config file, and once I removed that, the AssemblyResolve handler could work just fine. So the immediate problem is fixed.
What I'm trying to figure out if there's a way I can load an assembly without it sticking with the faulty binding redirect. Obviously fixing that is the first step! But if something comes up in the future where that doesn't help or for some reason isn't feasible, I'd like the ability to switch on some failsafe code to basically go "Yeah, I know it looks like the wrong version, but trust me, load it and use it anyway." Not something I'd want to use all the time in production code (obviously!) but one thing I've learned over my time in this industry, it's always good to have ways to turn off the "smart"/"helpful" stuff if they start misfiring, like in this case.
Here's the current code (cut apart, moved around, etc for readability) for the AssemblyResolve handler I've been tinkering with:
// Used to avoid stack overflows
static List<string> stuffBeingLoaded = new List<string>();
/// <summary>
/// Handle assembly resolution failures
/// </summary>
/// <remarks>
/// Heavily adapted from https://weblog.west-wind.com/posts/2016/dec/12/loading-net-assemblies-out-of-seperate-folders
/// </remarks>
static Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args)
{
try
{
// Ignore missing resources
if (args.Name.Contains(".resources"))
return null;
// check for assemblies already loaded
AssemblyName targetAssemblyName = new AssemblyName(args.Name);
// Tries to find an already-loaded assembly with the same simple name, e.g. "System.Collections.Immutable"
Assembly assembly = AppDomain.CurrentDomain.GetAssemblies().FirstOrDefault(a => a.GetName().Name == targetAssemblyName.Name);
if (assembly != null)
return assembly;
// Try to load by filename - split out the filename of the full assembly name
// and append the base path of the original assembly (ie. look in the same dir)
string filename = new AssemblyName(args.Name).Name + ".dll";
string folder = Path.GetDirectoryName(new Uri(Assembly.GetExecutingAssembly().Location).LocalPath) ?? @".\";
string assemblyFilePath = Path.Combine(folder, filename);
if (!File.Exists(assemblyFilePath) || stuffBeingLoaded.Contains(assemblyFilePath))
{
Console.WriteLine("Unable to load {0}, requested by {1}", args.Name, args.RequestingAssembly?.Location ?? "(requester missing)");
return null;
}
// This is just so we can recognise if we get into a loop where we fail to load assembly X, so we try
// in here, and that fails so we loop back into here to try again... Stack overflows are annoying! Let's
// not have any!
stuffBeingLoaded.Add(assemblyFilePath);
try
{
// Option 1 - use Assembly.LoadFrom
//var currentDomainAssemblyResolve = Assembly.LoadFrom(assemblyFilePath);
// Option 2 - use Assembly.Load with the AssemblyName from the file on disk
// AssemblyName assemblyToLoad;
// try
// {
// assemblyToLoad = AssemblyName.GetAssemblyName(assemblyFilePath);
// }
// catch (Exception ex)
// {
// Console.WriteLine(ex.ToString());
// return null;
// }
//var currentDomainAssemblyResolve = Assembly.Load(assemblyToLoad);
// Option 3 - read the contents of the assembly into a byte[] and load the assembly from that
Stream assemblyFileStream = File.OpenRead(assemblyFilePath);
FileInfo assemblyFileInfo = new FileInfo(assemblyFilePath);
byte[] contents = new byte[assemblyFileInfo.Length];
assemblyFileStream.Read(contents, 0, (int)assemblyFileInfo.Length);
var currentDomainAssemblyResolve = Assembly.Load(contents);
return currentDomainAssemblyResolve;
}
catch (Exception ex)
{
Console.WriteLine("Unexpected {0} attempting to resolve {1} from {2}: {3}",
ex.GetType().Name, assemblyFilePath, args.RequestingAssembly?.Location ?? "(requester missing)", ex.Message);
return null;
}
finally
{
stuffBeingLoaded.Remove(assemblyFilePath);
}
}
catch (Exception catchall)
{
Console.WriteLine("Unexpected exception {0}", catchall);
}
return null;
}
All three options I have listed (plus a few variations that I played with along the way), I say "Load this file with v1.2.3" and it comes back and says "I can't load v1.2.4", which is the version that the faulty binding redirect said to use.
Can anyone point me at a way to disable/bypass/ignore the binding redirects in code?