Using Loading contexts effectively

March 24th, 2011

Long long time ago I promised a few people in my twitter that someday I’d post somewhere a sample on how to deal with “dependency hell”.

By that I mean something you’ve probably experienced yourself. Suppose you’re happily using log4net, and then you start using NHibernate which happens to use a different version of log4net. Hmmm. Easy. You can switch your own copy to the NHibernate uses. Then you add another dependency to your project, say SupperCoolWidget, and it happens to use yet another version of log4net.

You can rely on binding redirects, as long as you’re damn sure the API surfaced touched by these projects haven’t changed on the dependency (in this case log4net), otherwise you’ll get exceptions in runtime (cannot find member).

IMHO it’s especially problematic to rely on binding redirects because some code, somewhere, isn’t used very often, and in some special circumstance it may try to use an API that isn’t there.

log4net is an interesting example, but the problem applies to any scenario where you have common dependencies in different versions.

Looking at another camp – Java in particular – OSGi brought some interesting ideas to this very situation. There it’s even worse since jars are way more loose than assemblies. OSGi’s solution is to either have independent versioned bundles which your bundle may explicit say it depends upon, _or_ your bundle carries everything it needs to work. Multiple bundles can be loaded and executed in a single VM, and they are guarantee to not step on each others foot.

Java’s enabler to this magic is the ClassLoader.

In .NET there isn’t a concrete equivalent of the Class loader, but we have a loader. And it has different contexts. In fact, as many as you want. The Load and LoadFrom contexts are the typical ones you’re exposed to. More resources: Choosing a Binding Context and LoadFile vs LoadFrom

By using a combination of the right loading context and the AssemblyResolve event, you can achieve the behavior of isolated silos loading the same assemblies (with different versions) in the same AppDomain.

I created a sample to demonstrate the idea and you take it from there. I’ve tried to minimize the concepts, so no MEF, no Windsor, and it’s not a web app. The file structure is like the following

Capture

The build folder contains the app, which doesn’t do much:

private static readonly CustomBinder _binder = new CustomBinder();

static void Main()
{
    var curDir = AppDomain.CurrentDomain.BaseDirectory;

    // Each module is loaded in its own isolated context
    // so they can have conflict dependencies and work
    var modules = LoadModules(_binder, curDir);

    foreach (var module in modules)
    {
        Console.WriteLine(module);
    }
}

So it loads “modules” in a kind of late bound way, using a well-known contract: IModule.

Each module implementation depends on – guess what – log4net. But different versions of it. Each implementation of IModule looks pretty much the same, but the dependency version is quite different:

namespace FakeMod1
{
    using WellKnownContracts;

    public class Mod1Impl : IModule
    {
        private static log4net.ILog logger = log4net.LogManager.GetLogger(typeof(Mod1Impl));

        public Mod1Impl()
        {
            logger.Info("constructed");
        }
    }
}

namespace FakeMod2
{
    using WellKnownContracts;

    public class Mod2Impl : IModule
    {
        private static log4net.ILog logger = log4net.LogManager.GetLogger(typeof(Mod2Impl));

        public Mod2Impl()
        {
            logger.Info("Mod2Impl constructed");
        }
    }
}

When we run the app we expect the following to happen

  • module 1 is found
  • A logical context is created for it
  • Each dependency within the module 1 is satisfied within the boundary
  • module 2 is found
  • ditto ditto..

Running the app and watching the debugger confirms the expected behavior:

‘ParallelContexts.vshost.exe’: Loaded ‘ParallelContexts.exe’, Symbols loaded.

‘ParallelContexts.vshost.exe’: Loaded ‘WellKnownContracts.dll’, Symbols loaded.

‘ParallelContexts.vshost.exe’: Loaded ‘modules\mod1\FakeMod1.dll’, Symbols loaded.

‘ParallelContexts.vshost.exe’: Loaded ‘modules\mod1\log4net.dll’

log4net:ERROR No appenders could be found for logger (FakeMod1.Mod1Impl).

log4net:ERROR Please initialize the log4net system properly.

‘ParallelContexts.vshost.exe’: Loaded ‘modules\mod2\FakeMod2.dll’, Symbols loaded.

‘ParallelContexts.vshost.exe’: Loaded ‘modules\mod2\log4net.dll’

Notice that WellKnownContracts.dll isn’t loaded more than once, since it first loaded in the Load context, it’s always found – I’m a bit unsure if this one is even probed after it’s loaded for the first time.

 

How it works?

The code is simple. The class CustomBinder takes a “module folder”, and starts a new logical context for it.

public partial class CustomBinder : IDisposable
{
    ...


    public BindingContext Add(string modFolder)
    {
        var ctx = new BindingContext(this);
        var files = Directory.GetFiles(modFolder);

        string entryPointFromManifest = null;

        foreach (var file in files)
        {
            // manifest has an entry point which is the first type/assembly we load
            // this is just an optimization, so we dont have to load all assemblies found within a package/module
            if (Path.GetFileName(file).Equals("manifest.xml", StringComparison.InvariantCultureIgnoreCase))
            {
                entryPointFromManifest = GetEntryPointFromManifest(file);
                continue;
            }

            if (!file.EndsWith(".dll")) continue;

            var name = AssemblyName.GetAssemblyName(file);
            ctx.AddAssemblyName(name.Name, file);
        }

        if (entryPointFromManifest != null)
        {
            string[] split = entryPointFromManifest.Split(',');
            Debug.Assert(split.Length == 2);
            ctx.EntryPointTypeName = split[0];
            ctx.PreLoad(split[1].TrimStart());
        }

        return ctx;
    }

Then, whenever the loader probes for an assembly, we use the “requesting assembly” to bring the existing context back, and use it to load the right assembly.

private Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args)
{
    if (args.RequestingAssembly == null)
        return null;

    BindingContext ctx = GetBindingContext(args.RequestingAssembly);
    if (ctx == null) return null;

    Assembly assembly;
    if (ctx.TryGetAssembly(new AssemblyName(args.Name), out assembly))
    {
        return assembly;
    }

    return null;
}

As I mentioned before, this is just a proof of concept that shows what is possible. The sky is the limit for modular/composable frameworks out there. Enjoy!

Download sample

Leave a Reply