Share via


Custom Resolution of Assembly References in .NET

Right now I am helping out a team with an assembly resolution conflict bug. I thought I’d share the details out, because I am sure others have landed in this situation.

In a complex managed applications, especially in cases where the application uses dynamically loaded plugins/addons, it’s common that all the assemblies required by these addons are not present in the assembly probing path. In the perfect world the closure set of all assembly references of an application is strong-named and the CLR binder uses standard probing order to find all assemblies and everything works perfectly. However, the world is not ideal.

Requirement of having to resolve assemblies from different locations do arise and .NET has support for that. E.g. this stackoverflow question https://stackoverflow.com/questions/1373100/how-to-add-folder-to-assembly-search-path-at-runtime-in-net has been rightly been answered by pointing to AssemblyResolve event. When .NET fails to find an assembly after probing (looking through) the various folders .NET uses for assemblies, it raises an AssemblyResolve event. User code can subscribe to this event and supply assemblies from whatever path it wants to.

This simple mechanism can be abused and results in major system issues. The main problem arises from over-eager resolution code. Consider an application A that uses two modules (say plugins) P1 and P2. P1 and P2 is somehow registered to A, and A uses Assembly.Load to load P1 and P2. However, P1 and P2 ships with various dependencies which it places in various sub-directories which A is unaware of and the CLR obviously doesn’t look into those folders to resolve the assemblies. To handle this situation both P1 and P2 has independently decided to subscribe to the AssemblyResolve event.

The problem is that for all cases CLR fails to locate an assembly it will call these resolve-event handlers sequentially. So based on the order of registering these handlers, it is possible that for a missing dependency of P2 the resolution handler of P1 gets called. Coincidentally it is possible that the assembly CLR is failing to resolve is known to both P1 and P2. Could be because the name is generic or because maybe it’s a widely used 3rd party assembly which a ton of plugins use. So P1 loads this missing assembly P2 is referring to and returns it. CLR goes ahead and binds P2 to the assembly P1 has returned. This is when bad things start to happen because maybe P2 needs a different version of it. Crash follows.

The MSDN documentation has already called out how to handle these issues in https://msdn.microsoft.com/en-us/library/ff527268.aspx. Essentially follow these simple rules

  1. Follow the best practices for assembly loading https://msdn.microsoft.com/en-us/library/dd153782.aspx
  2. Return null if you do not recognize the referring assembly
  3. Do not try to resolve assemblies you do not recognize
  4. Do not use Assembly.Load or AppDomain.Load to resolve the assemblies because that can result in recursive calls to the same ResolveEvent finally leading to stack-overflow.

The skeleton code for the resolve event handler can be something like

 static Assembly currentDomain_AssemblyResolve(object sender, ResolveEventArgs args)
{
    // Do not try to resolve assemblies which your module doesn't explicitly handle
    // There will be other resolve handlers that are called in-sequence, let them
    // do their job
    if (args.RequestingAssembly == null || !IsKnownAssembly(args.RequestingAssembly))
        return null;

    // parse and create name of the assembly being requested and then use your own logic
    // to locate the assembly
    AssemblyName aname = new AssemblyName (args.Name);
    string path = FindAssemblyByCustomLogic(aname);

    if (!File.Exists(path))
        return null;
            
    Assembly assembly = Assembly.LoadFile(path);
    return assembly;
}

Here you need to fill in the implementation of IsKnownAssembly which only returns true for assemblies that belong to your module.