Rick Strahl's Web Log

Wind, waves, code and everything in between...
ASP.NET • C# • HTML5 • JavaScript • AngularJs
Contact   •   Articles   •   Products   •   Support   •   Search
Ad-free experience sponsored by:
ASPOSE - the market leader of .NET and Java APIs for file formats – natively work with DOCX, XLSX, PPT, PDF, images and more

Loading .NET Assemblies out of Seperate Folders


.NET's loading of binaries is great in most standard .NET applications. Assemblies are loaded out of the application folder or a special private bin folder (like ASP.NET applications) and it all works as you would expect.

But, once you need to dynamically load assemblies and load them out of different folders things start getting pretty ugly fast. I've been to this rodeo quite a few times, and I've used different approaches and pretty much all of them are ugly. Recently when working on Markdown Monster I ran into this again and had some really odd issues and - another solution that I hadn't used before that I want to share.

Wait... Why?

Loading assemblies out of separate folders is not something you do very frequently in typical business applications. Most applications add references to their projects and the compiler and tooling handles spitting out the final required DLLs in the right folders. You take those folders and you're done.

The most common case for dynamic assembly loading involves some sort of addin mechanism where users can extend the application by creating custom components and plugging them into the current application, thereby extending the functionality.

The slippery Slope of Addins

I came to addins via my Markdown Monster Markdown Editor, which has support for addins where you can create custom extensions that extend the editor and UI. Basically it's quite easy to create a custom form that performs some task with the full Markdown document or manipulates specific text in the document - or publishes the markdown to some custom location. It seems like a natural fit for an editor - you write you want to push your document out to somewhere.

Adding addins was a very early decision when I decided to create this tool, because first of all I wanted to be able to publish my Markdown directly to my blog. And I wanted to capture images from screen shots and embed them. These two features happen to be implemented as separate addins that plug into the core Markdown Monster editor. Since then I've created a couple more (an Image to Azure Blob Storage uploader, a Gist Code embedding Addin). A few other people have also created addins.

Addin 0.1

Although I was very clear on wanting to create addins I didn't deal with all the issues of how to store the addins and get them downloaded and installed initially. My first take (Take 0.1) was to just dump all addins into an .\Addins folder and call it a day.

The application now has some startup code that checks the .DLLs in the Addins folder, checks for the addin interface and if so loads the addin.

If you're dealing with a single external folder things are easy because you can easily set the PrivateBin path which can be set in the application's app.config file:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <runtime>
    <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
      <probing privatePath="Addins" />
    </assemblyBinding>
  </runtime>
</configuration>

Behind the scenes this sets the AppDomain's PrivateBin path. This also happens to be the only way to properly add the PrivateBin path for the main executable. While there is AppDomain.AppendPrivatePath() this method is obsolete and can cause some potential load order problems. It also has to be called very early on in the application likely as the first line of code before additional assemblies beyond mscorlib are loaded.

Loading Addins

In my application I actually didn't need this. Rather than dealing with Private Bin paths I've always used Appdomain.AssemblyResolve which is fired whenever an assembly cannot be resolved. If you have a single folder it's very easy to find assemblies because you know where to look for any missing references that the application can't resolve on its own.

To put this in perspective here is how addins are loaded. The original addin loader runs through all assemblies in the Addins folder and checks for the Addin interface and if found loads the assembly with Assembly.LoadFrom().

The assembly typically loads without a problem, but the problem usually comes from any dependent assemblies that get loaded when scanning the assembly for types that return or pass dependent assembly types.

private void LoadAddinClasses(string assemblyFile)
{
    Assembly asm = null;
    Type[] types = null;

    try
    {
        asm = Assembly.LoadFrom(assemblyFile);
        types = asm.GetTypes();
    }
    catch(Exception ex)
    {
        var msg = $"Unable to load add-in assembly: {Path.GetFileNameWithoutExtension(assemblyFile)}";                
        mmApp.Log(msg, ex);
        return;
    }

    foreach (var type in types)
    {
        var typeList = type.FindInterfaces(AddinInterfaceFilter, typeof(IMarkdownMonsterAddin));
        if (typeList.Length > 0)
        {
            var ai = Activator.CreateInstance(type) as MarkdownMonsterAddin;
            AddIns.Add(ai);
        }
    }
}

The asm = Assembly.LoadFrom(assemblyFile); never fails by itself - loading an assembly typically works. When an assembly is loaded only that assembly is loaded and not any of its dependencies.

But, when running asm.GetTypes(); additional types and dependencies are accessed and that triggers an assembly load attempt from .NET natively. If there's no additional probing or assembly resolve the code bombs.

If you are very vigilant about not bleeding external dependencies in your public interfaces you may not see dependency exceptions here, but you will then hit them later at runtime when you actually invoke code that uses them.

.NET's assembly loading is smart and delay loads assemblies only when a method is called that uses a dependency (except ASP.NET applications which explicitly pre-load all BIN folder assemblies).

However, .NET only looks for dependencies in the startup folder or any additionally declared Private Bin paths. I don't have those, so the asm.GetTypes() in many cases causes assembly load failures.

AssemblyResolve

Luckily you can capture assembly load failures and tell .NET where to look for assemblies. A simple implementation of AssemblyResolve looks like this:

private Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args)
{
    // Ignore missing resources
    if (args.Name.Contains(".resources"))
        return null;

    // check for assemblies already loaded
    Assembly assembly = AppDomain.CurrentDomain.GetAssemblies().FirstOrDefault(a => a.FullName == args.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 = args.Name.Split(',')[0] + ".dll".ToLower();

    string asmFile = Path.Combine(@".\","Addins",filename);
    
    try
    {
        return System.Reflection.Assembly.LoadFrom(asmFile);
    }
    catch (Exception ex)
    {
        return null;
    }
}

And to intialize you hook it up in the startup code of your application:

AppDomain.CurrentDomain.AssemblyResolve += 
               CurrentDomain_AssemblyResolve;

This is pretty straight forward and it works easily because I can look for assemblies in a known folder.

Checking already loaded Assemblies?

Notice that before going to check for assemblies out on disk, there's a check against already loaded assemblies. Huh? This sounds counter intuitive. Why would this code actually trigger and find an already loaded assembly? I dunno, but I've had this happen consistently with CookComputing.XmlRpcV2.dll which is already loaded, yet somehow ends up in the AssemblyResolve handler. Simply returning the already loaded assembly instance oddly works, which is just strange.

If the assembly is not already loaded, I can then try and load it from the .\Addins folder. With the single folder this all worked just fine.

Folder Loading

Fast forward a couple of months and now I'm looking at creating an addin manager with downloadable addins. Of course, I quickly realized that a single folder isn't going to work as each addin needs to provide some metadata and folders make it easy to see what's installed and easy to uninstall without having to track all the files.

In theory moving the code to use folders should work the same, but there's a catch - I no longer know where the assemblies are loading from specifically because there are no many addin folders.

MIA - args.RequestingAssembly

Note that the AssemblyResolve handler has a args.RequestingAssembly property which maddeningly is always blank. If this value actually gave me the requesting or calling assembly things would be easy since I could just try loading from the same folder. But alas the value is always empty, so no go.

I tried a number of different approaches in order to figure out how to get at the assembly. Different ways of loading the assembly, moving the files, AppendPrivateBin() (which as mentioned earlier has no effect after assemblies have started to load).

Using Brute Force: Scan folder hierarchy for DLLs

In the end I ended up using an extension of what worked initially which is simply to load the assembly from disk. This time around I don't know the exact folder, but I know what the base folder is so I can simply scan the directory hierarchy for the DLLs. Yes, this definitely has some overhead, but after all the false starts this just seems to be the most reliable way to ensure assemblies are found and matched.

So now instead of hardcoding the assembly path I use this routine and reference it in AssemblyResolve to find my assembly:

private string FindFileInPath(string filename, string path)
{
    filename = filename.ToLower();

    foreach (var fullFile in Directory.GetFiles(path))
    {
        var file = Path.GetFileName(fullFile).ToLower();
        if (file == filename)
            return fullFile;

    }
    foreach (var dir in Directory.GetDirectories(path))
    {
        var file = FindFileInPath(filename, dir);
        if (!string.IsNullOrEmpty(file))
            return file;
    }

    return null;
}

Which is then called in AssemblyResolve like this:

string asmFile = FindFileInPath(filename, ".\\Addins");
if (!string.IsNullOrEmpty(asmFile))
{
    try
    {
        return Assembly.LoadFrom(asmFile);
    }
    catch
    {
        return null;
    }
}

// FAIL - not found
return null;

This works with everything I've thrown at it thus far so this seems like a good solution. There's definitely some overhead in this - both searching for the assemblies and then also from all the assembly preloading that occurs because of the type scanning in order to find the addin interface - which effectively preloads all used dependencies.

Asynchronous Addin Loading

In order to minimize the overhead of this addin loading, I also load addins asynchronously, so they happen in the background while the rest of the application loads.

protected override void OnStartup(StartupEventArgs e)
{
    // force startup directory in case we started from command line
    var dir = Assembly.GetExecutingAssembly().Location;            
    Directory.SetCurrentDirectory(Path.GetDirectoryName(dir));            

    mmApp.SetTheme(mmApp.Configuration.ApplicationTheme,
                   App.Current.MainWindow as MetroWindow);

    new TaskFactory().StartNew(() =>
    {
        ComputerInfo.EnsureBrowserEmulationEnabled("MarkdownMonster.exe");

        try
        {
            AddinManager.Current.LoadAddins();
            AddinManager.Current.RaiseOnApplicationStart();
        }
        catch (Exception ex)
        {
            mmApp.Log("Addin loading failed", ex);
        }
    });
}

and this seems to help to mitigate the startup lag quite a bit.

AppDomains

No discussion of Add ins and assembly loading would be complete without mentioning AppDomains and loading Addins separately. A lot of the issues I've described here could be mitigated by using a custom AppDomain and explicitly setting up the private bin path before load time by pre-scanning the folders.

There are a some clear advantages to using AppDomains:

  • Ability to load and unload addins dynamically
  • Better control of Assembly resolving
  • Better Isolation from the main application

But after having struggled with AppDomain based addins in a couple of other applications and realizing that add-ins need to have access to the WPF UI, there's no easy way to deal with the cross domain serialization in such a tighly integrated addin. Although possible, the complexities that this raises are not worth the effort.
Along the same lines app Isolation is not a concern since addins have to have tight integration with the main application anyway in order to do what they need to. So addins run in-process.

It all depends on the solution used of course. More business service centric addins can be a good fit for AppDomain.

Summary

This may sound like a fairly esoteric problem, but while searching for solutions around not getting Assembly resolve errors and loading of assemblies from multiple folders, there are a lot of people running into these same problems. There are a lot of hacky workarounds and this one is just one more in a long line of hacks. But for me at least this one has been reliably working - in fact so much so I've retrofitted it to two other applications that were previously guessing at paths.

Hopefully this will prove useful to some of you, but as always it helps me to write this down so I can find this for the inevitable next I build some add in based interface and will have forgotten what worked - it's one of those things you do so seldom that it's easy to forget...

If you want to see all the pieces together in a working application you can check out the links below in the Markdown Monster source code on Github.

Resources

this post created with Markdown Monster

Posted in .NET  

The Voices of Reason


 

Paw Baltzersen
December 12, 2016

# re: Loading .NET Assemblies out of Seperate Folders

Or just throw MEF into your application and load from all the different folders you want to, and use DI to get whatever instances you want from them.


Donnie Hale
December 12, 2016

# re: Loading .NET Assemblies out of Seperate Folders

If I follow you correctly (and I may not), you're loading add-ins by folder now, not by DLL-at-a-time in the "Addins" folder. If that's correct, could you change the AssembyResolve handler before you begin loading each addin, perhaps as a closure over the addin's folder name so that you know which addin you are adding when the handler gets called?

I'm sure I'm missing something, but that's what went through my mind as I read your article.

As always, thanks for these.


Rick Strahl
December 13, 2016

# re: Loading .NET Assemblies out of Seperate Folders

@Paw - yep MEF is an option, but seems like overkill for a relatively simple task. While it wasn't exactly straight forward to arrive at this solution, it works and is easy to implement.

@Donnie - while that might work I don't think that's a good idea. Assembly resolve errors can occur at any time, and you may end up with oddball dependencies getting pulled in that might have to be loaded differently (or just passed back) - too risky. I suppose one could track the current addin folder with a static variable and based on that know the active addin folder, but again it's not generic. The solution here - brute force as it is - works and it simply runs through files to find what it needs. The folder strucuture isn't going to be large for addin assemblies so while not fast, it's not exactly a ball breaker either especially if the add-in loading is offloaded to a separate async/thread operation.


Laurent Kempé
December 13, 2016

# re: Loading .NET Assemblies out of Seperate Folders

I have used MEF for that particular problem in a little side project called Nubot; a chat bot in C# connecting to HipChat. You can find the source here https://github.com/laurentkempe/Nubot It worked really nicely and I found it a clean solution and definitely not an overkill!


Dustin
December 13, 2016

# re: Loading .NET Assemblies out of Seperate Folders

try LoadFile instead of LoadFrom

http://stackoverflow.com/a/10245012


Shannon Deminick
December 13, 2016

# re: Loading .NET Assemblies out of Seperate Folders

I had done a lot of this same kind of stuff quite some time ago when we were building the infamous Umbraco version 5. I wrote quite a bit about this, plus we had to support Med Trust which added a ton more complexity. We also tried MEF but it turns out (at least at the time) that MEF locked assemblies outside of the /bin because those assemblies aren't shadow copied to the ASP.NET temp folder where all /bin files actually end up and are used. Might be of interest so here's the post I wrote about all that: http://shazwazza.com/post/developing-a-plugin-framework-in-aspnet-with-medium-trust/ and more recently a follow up post about ASP.NET Core and how much easier it is to do this kind of thing: http://shazwazza.com/post/custom-assembly-loading-with-aspnet-core/


Can Birch
December 13, 2016

# re: Loading .NET Assemblies out of Seperate Folders

A slight performance improvement could be gained by scanning the addins directories recursively and building a static dictionary of all the assemblies. That would at least put the disk cost to very close to the single directory solution.

Since you cannot unload dlls once loaded you can either shadow copy them (app load option) or ignore the uninstall after start issue.

Then if you want a little bit of extra awesome you can add a file system watcher to rebuild your dictionary and load newly installed plugins.

I'm going to bookmark this for next time I need to update our LOB app that uses plugins. Always nice to pre-emptively prevent bugs.


Govert van Drimmelen
December 13, 2016

# re: Loading .NET Assemblies out of Seperate Folders

To answer your question of why the AssemblyResolve gets called even for assemblies already loaded, you need to dive into .NET "Load Contexts".

Assemblies loaded through Assembly.LoadFrom(...) are not always treated the same as though found through the runtime probing.

Some links to lear about Load Contexts:


Muhammad Rehan Saeed
December 14, 2016

# re: Loading .NET Assemblies out of Seperate Folders

Another vote for MEF 2.0 as it's designed with addin's in mind. MEF 1.0 is attribute heavy and has crazy defaults. MEF 2.0 is better.

 

West Wind  © Rick Strahl, West Wind Technologies, 2005 - 2017