2

I am working on a plug-in based framework that allows plug-ins to exchange data between one another based on some contract (interface).

Currently, a Windows Service can load the plug-ins in two ways:

  1. In the same AppDomain as the Windows Service hosting the plug-ins
  2. In another process that the Windows service communicates with via named pipes (WCF).

This works nicely most of the time. However, there are certain scenarios where one plug-in might reference an assembly, and another plug-in references a newer version of the assembly. In this scenario, I always want the newer version of the dependency to be loaded regardless of which plug-in is loaded first.

Here is the folder structure:

  • Windows Service Directory (AppDomainBase)
    • Plugins
      • Plugin1
        • Plugin1.dll
        • SharedDependency.dll (1.0.0.0)
      • Plugin2
        • Plugin2.dll
        • SharedDependency.dll (1.0.1.0)

I have done a lot of research already, and tried many different things. That said:

  1. I cannot redirect assembly bindings via an app.config file. Although this works, it is not practical because I do not know all of the dependencies ahead of time and cannot add every single one to the app.config.
  2. I can't use the GAC
  3. I don't want multiple versions of the same assembly loaded, just the newest one.

I have read about Assembly.Load, LoadFrom, and LoadFile and have tried using all of them. I am still not 100% clear on the difference between Load and LoadFrom. They both seem to automatically load each plug-in's dependencies through fusion and probing from the directory where they are loaded.

My current solution is to search all sub-directories of the AppDomainBase to find and cache all of the DLLs in each plug-in's folder. If I encounter the same assembly more than once, I always keep track of the newest version and its location.

Then, I load each plug-in by calling Assembly.LoadFile so that no dependencies are loaded by fusion. I am subscribing to the AppDomain.CurrentDomain.AssemblyResolve event. When that event is raised, I inspect the Name of the assembly to determine which assembly should be loaded that was pre-cached, and then I load it by calling Assembly.Load.

 private Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args)
    {
        try
        {
            Log(string.Format("Resolving: {0}", args.Name));

            // Determine if the assembly is already loaded in the AppDomain. Only the name of the assembly is compared here.
            var asm = AppDomain.CurrentDomain.GetAssemblies().FirstOrDefault(a => a.GetName().FullName.Split(',')[0] == args.Name.Split(',')[0]);

            if (asm == null)
            {
                // The requsted assembly is not loaded in the current AppDomain
                Log(string.Format("Assembly is not loaded in AppDomain: [{0}]", args.Name));

                // Determine if the assembly is one that has already been found and cached
                var asmName = _RefCandidates.Find(a => a.Name.FullName.Split(',')[0] == args.Name.Split(',')[0]);

                if (asmName != null)
                {
                    // The assembly exists in the cache, but has not been loaded. Load it.
                    Log(string.Format("Pre-loaded assembly found in cache. Loading: [{0}], [{1}]", asmName.Name, asmName.Name.CodeBase));
                    return Assembly.LoadFile(asmName.File.FullName);
                }
            }
            else
                Log(string.Format("Assembly is already loaded in AppDomain: [{0}], [{1}]", asm.GetName(), asm.GetName().CodeBase));

            return asm;
        }
        catch (Exception ex)
        {
            Logger.Write(ex, LogEntryType.Error);
            return null;
        }
    }

First of all, what is the best way to accomplish what I need to do, and what am I doing wrong?

Second, what happens if a dependency in the chain is expecting to reference something that is in the GAC? I assume it will no longer be found since I am using LoadFile and skipping fusion all together. Also, I have read some things about serialization not working with LoadFile. What specifically? How about resource assemblies?

This model is assuming that all newer versions of dependent assemblies are backward compatible since I'll be loading only the newest version.

Any input is greatly appreciated.

remarkpk
  • 21
  • 3
  • Never, *never*, use LoadFile(), only LoadFrom. [Read this](https://msdn.microsoft.com/en-us/library/yx7xezcf%28v=vs.110%29.aspx) to narrow the scope of this question. – Hans Passant Jun 06 '15 at 08:54
  • Thank you for the link, good info there. However, can you provide some insight as to why LoadFile should never be used? I understand that it only loads the requested assembly and no additional probing is done for dependencies. – remarkpk Jun 08 '15 at 00:24

1 Answers1

0

You can't load assembly manually, if the resolution of this assembly not fails(only in this case AssemblyResolve is raised). So, your plugin architecture can not be implemented with Assembly.Load* methods.

There are two paths now.
First(not recommended): somehow remove the restriction(for example use Assembly.Load at start, and in any point you need an object, search for CurrentDomain assemblies, pick right assembly and by reflection construct objects and use them).
Second(recommended): review your initial problem and search for solution, that can be simple to implement and maintain.

Take a look, what Managed Extensibility Framework offers for plugin architecture.
SharpDevelop folks wrote a book, telling us about their plugins tree.
Find your way.

Also see this link about assembly loading context to understand why Assembly.Load without AssemblyResolve event will not work.

FireAlkazar
  • 1,795
  • 1
  • 14
  • 27
  • I'm don't understand. If I loaded my plug-ins using Assembly.LoadFile, I *am* able to handle the AssemblyResolve event because fusion does not seem to be used for loading dependencies. This seems to work in my example above. I actually changed the code above from using Assembly.Load to Assembly.LoadFile. What are the other restrictions that LoadFile has on serialization and resource assemblies? Also, it seems I am able use Assembly.Load method on DLLs that are in subfolders of the AppDomain base, and their dependencies are found and loaded too. I thought that was what LoadFrom was for...? – remarkpk Jun 06 '15 at 01:20
  • 1
    That works, because standart procedure of finding assembly fails. You don't have SharedDependency.dll directly in AppDomainBase folder, do you? Put you SharedDependency.dll into GAC and you will not be able to resolve it manually. Actually, it seems like Dll hell already. Sorry for not answering your questios, but I guess we deal with XY problem here http://meta.stackexchange.com/a/66378 – FireAlkazar Jun 06 '15 at 10:24