Wednesday, July 29, 2009

Assembly Resolution with AppDomain.AssemblyResolve

Plugin frameworks often rely on runtime assembly resolution, and sometimes the assemblies to be resolved don't lie within the directory structure of the executable, nor are they located in the GAC. In these cases, you can use the AppDomain.AssemblyResolve event to resolve assemblies at runtime. The key with the AssemblyResolve method is that it must be put into place before attempting to instantiate any of the classes where an assembly needs to be loaded. Sample code exists on the MSDN Documentation for System.Assembly.AssemblyResolve.

Before implementing runtime assembly resolution, it's important to understand how methods are loaded at runtime.

When a method is first called, that method is JITted, or "Just-In-Time Compiled". Essentially, all the IL code for that method is loaded and then compiled into CPU instructions. Any types that are used within that method are also resolved when the method is JIT compiled, in order to ensure that the entire method is "safe" for execution. Essentially, what this means to Assembly Resolution using the AppDomain.AssemblyResolve event is that the attachment to the event handler cannot reside within the same method that calls the type that will have to be resolved using the event handler. The reason for this is simple... the event handler must be attached before the type is resolved. If the event handler is attached within the same method, then the JIT compiler will attempt to resolve the type before executing the first instruction in the method, including the instruction to attach the event handler to the AssemblyResolve event, causing the whole application to fail.

Thus, a good example of attaching an assembly at runtime would be the following. The Widget class is the class to load, and it does not reside within the same directory as the executable. Instead, it resides in a directory that is only identified with a relative path. This relative path can be searched at runtime to find the specific assembly that is needed to load.


using System;
using System.IO;
using System.Reflection;
using ClassLibrary1;

namespace ConsoleApplication6
{
internal class Program
{
private const string ResolutionPath = @"..\..\..\ClassLibrary1\bin\debug\";

private static void Main()
{
AppDomain.CurrentDomain.AssemblyResolve += CurrentDomainAssemblyResolve;
RunWidget();
}

private static void RunWidget()
{
var widget = new WidgetInOtherAssembly();
Console.WriteLine(widget);
}

private static Assembly CurrentDomainAssemblyResolve(object sender, ResolveEventArgs args)
{
Assembly[] currentAssemblies = AppDomain.CurrentDomain.GetAssemblies();

for (int i = 0; i < currentAssemblies.Length; i++)
{
if (currentAssembliesi.FullName == args.Name)
{
return currentAssembliesi;
}
}

return FindAssembliesInDirectory(args.Name, ResolutionPath);
}

private static Assembly FindAssembliesInDirectory(string assemblyName, string directory)
{
foreach (string file in Directory.GetFiles(directory))
{
Assembly assm;

if (TryLoadAssemblyFromFile(file, assemblyName, out assm))
return assm;
}

return null;
}

private static bool TryLoadAssemblyFromFile(string file, string assemblyName, out Assembly assm)
{
try
{
// Convert the filename into an absolute file name for
// use with LoadFile.
file = new FileInfo(file).FullName;

if (AssemblyName.GetAssemblyName(file).FullName == assemblyName)
{
assm = Assembly.LoadFile(file);
return true;
}
}
catch
{
/* Do Nothing */
}
assm = null;
return false;
}
}
}


Changes To ResolveEventArgs in .NET 4.0

In .NET 4.0, the ResolveEventArgs class has a new property called "RequestingAssembly", which returns the name of the assembly requesting resolution. This has been done in order to enable the ability to execute specific code based on which assembly was requesting the resolution.

You can see more information on these changes here.