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.

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.


Jorge Rodríguez Galán
March 10, 2017

# re: Loading .NET Assemblies out of Seperate Folders

My vote for simplicity...

My trajectory has been MEF, load assemblies from the same domain, from different appdomains and back to the same domain. For me this is the most easy, not overkill and maintainable solution. Currently I prefer to use a mixed approach with a host that act as the addin assembly referencing the specific module as a nuget package.

 

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