I have this code which tries to load all assemblies in a folder and then for each assembly, also check if all of its dependencies too can be found in the same folder.
private static string searchDirectory = @"C:\Users\Anon\Downloads\AssemblyLoadFilePOC_TESTFOLDER";
private static readonly List<string> errors = new List<string>();
static void Main(string[] args)
{
if (args.Length > 1)
searchDirectory = args[1];
var files = Directory
.GetFiles(searchDirectory, "*.*", SearchOption.AllDirectories)
.Where(s => s.EndsWith(".dll", StringComparison.OrdinalIgnoreCase) || s.EndsWith(".exe", StringComparison.OrdinalIgnoreCase));
foreach (var file in files)
{
try
{
AssemblyLoadHandler.Enable(Path.GetDirectoryName(file));
Assembly assembly = null;
if (File.Exists(file) && Path.GetExtension(file) == ".dll")
{
assembly = Assembly.LoadFrom(file);
}
else
{
var fileInfo = new FileInfo(file);
assembly = Assembly.LoadFile(fileInfo.FullName);
}
ValidateDependencies(Path.GetDirectoryName(file), assembly, errors);
}
catch (BadImageFormatException e)
{
errors.Add(e.Message);
}
catch (Exception ex)
{
errors.Add(ex.Message); ;
}
}
}
Here's the AssemblyLoadHandler class which contains an event handler for Assembly Resolve event.
public static class AssemblyLoadHandler
{
/// <summary>
/// Indicates whether the load handler is already enabled.
/// </summary>
private static bool enabled;
/// <summary>
/// Path to search the assemblies from
/// </summary>
private static string path;
/// <summary>
/// Enables the load handler.
/// </summary>
public static void Enable(string directoryPath)
{
path = directoryPath;
if (enabled)
{
return;
}
AppDomain.CurrentDomain.AssemblyResolve += LoadAssembly;
enabled = true;
}
/// <summary>
/// A handler for the <see cref="AppDomain.AssemblyResolve"/> event.
/// </summary>
/// <param name="sender">The sender of the event.</param>
/// <param name="args">The event arguments.</param>
/// <returns>
/// A <see cref="Assembly"/> instance fot the resolved assembly, or <see langword="null" /> if the assembly wasn't found.
/// </returns>
private static Assembly LoadAssembly(object sender, ResolveEventArgs args)
{
// Load managed assemblies from the same path as this one - just take the DLL (or EXE) name from the first part of the fully qualified name.
var filePath = Path.Combine(path, args.Name.Split(',')[0]);
try
{
if (File.Exists(filePath + ".dll"))
{
return Assembly.LoadFile(filePath + ".dll");
}
}
catch (Exception)
{
}
try
{
if (File.Exists(filePath + ".exe"))
{
return Assembly.LoadFile(filePath + ".exe");
}
}
catch (Exception)
{
}
return null;
}
}
Here's the ValidateDependencies method which tries to load all dependencies of an assembly:
private static void ValidateDependencies(string searchDirectory, Assembly assembly, List<string> errors)
{
var references = assembly.GetReferencedAssemblies();
foreach (var r in references)
{
var searchDirectoryPath = Path.Combine(searchDirectory, r.Name + ".dll");
var runtimeDirectoryPath = Directory.GetFiles(RuntimeEnvironment.GetRuntimeDirectory(), r.Name + ".dll", SearchOption.AllDirectories);
try
{
if (!File.Exists(searchDirectoryPath) && (runtimeDirectoryPath == null || runtimeDirectoryPath.Length == 0))
{
throw new FileNotFoundException("Dependency " + r.Name + " could not be found.", r.FullName);
}
else
{
Assembly foundAssembly = null;
if (File.Exists(searchDirectoryPath))
{
foundAssembly = Assembly.LoadFrom(searchDirectoryPath);
if (foundAssembly.GetName().Version != r.Version && !r.Flags.HasFlag(AssemblyNameFlags.Retargetable))
foundAssembly = null;
}
else
{
foundAssembly = Assembly.LoadFrom(runtimeDirectoryPath[0]);
}
if (foundAssembly == null)
{
throw new FileNotFoundException("Required version of dependency " + r.Name + " could not be found.", r.FullName);
}
}
}
catch (Exception e)
{
errors.Add(e.ToString());
}
}
}
For testing, I have only put 2 assemblies in my test path: C:\Users\Anon\Downloads\AssemblyLoadFilePOC_TESTFOLDER:
Microsoft.AspNetCore.DataProtection.dll having assembly version 5.0.17.0 and ONE of its dependency: Microsoft.Extensions.Logging.Abstractions.dll having asembly version 5.0.0.0
Note that Microsoft.AspNetCore.DataProtection.dll version 5.0.17.0 has a dependency on Microsoft.Extensions.Logging.Abstractions.dll version 5.0.0.0
Just to test if my event handler for Assembly Resolve event is working correctly, I added this assembly binding redirect in my app config for this console application.
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="Microsoft.Extensions.Logging.Abstractions" publicKeyToken="adb9793829ddae60" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-7.0.0.0" newVersion="7.0.0.0" />
</dependentAssembly>
</assemblyBinding>
This application is a .NET 4.8 console application, when I run it without the binding redirect in the app config, as expected, the Assembly Resolve event is not fired. However, when I do add the redirect, Assembly Resolve event is fired multiple times and the application eventually crashes with Stackoverflow exception.
Quoting from MSDN docs here: https://learn.microsoft.com/en-us/dotnet/standard/assembly/resolve-loads#the-correct-way-to-handle-assemblyresolve
When resolving assemblies from the AssemblyResolve event handler, a StackOverflowException will eventually be thrown if the handler uses the Assembly.Load or AppDomain.Load method calls. Instead, use LoadFile or LoadFrom methods, as they do not raise the AssemblyResolve event.
As you can see, I am using Assembly.LoadFile method in my event handler and yet, the event keeps getting fired multiple times. Is the documentation incorrect? Or am I doing something wrong?
Nope, the documentation you mentioned is just fine! Pay attention that in the documentation, they create a new AppDomain called "Test";
If you create a new AppDomain and use it instead of
AppDomain.CurrentDomain, your problem will be solved.But let's say that for some reason you want to use the current AppDomain for some reason. We know that there is an uncontrolled recursive call hell which causes the stack overflow exception. How can we get rid of it? Simple! Just unsubscribe your handler method from
AssemblyResolveevent before loading assembly in the handler;This is the minimal code that I've derived from your code;
Output when no assembly redirecting is used - which commenting out unsubscribing line in
AssemblyResolveHandlermethod has no effect on;Output when assembly redirecting is used and commented out that line;
Output when assembly redirecting is used and uncommented out that line;
Edit: I just saw your comment about ResolveEventArgs.RequestingAssembly is being null. I'd like to explain it but it's a different thing and I don't want to make my answer longer. For more information about that, click me.