3

Here is how my application folders looks like:

Application:
+ App.exe
+ App.exe.config
Application/Plugins:
+ Plugin1 (folder)
Application/Plugins/Plugin1:
+ Plugin1.dll
+ SomeDll.dll

So main application App.exe looking for plugins folder and load {PluginName}.dll into memory to run it. This plugin usually uses it's own dependant assemblies which must be loaded (like SomeDll.dll). It appears that it make serious troubles sometimes. I receive exception that for example dependant assembly of dependant assembly cannot be found and I don't know why.

For example, My plugin must load lots of additional dlls becouse plugin runs OwinSelfHost service.

So it must load for example:

System.Web.Http.Owin
Owin
Microsoft.Owin
Microsoft.Owin.Host.HttpListener
Microsoft.Owin.Hosting

and when load Microsoft.Owin.Hosting then throw exception that cannot load Microsoft.Owin

Exception looks like:

Could not load file or assembly 'Microsoft.Owin, Version=2.0.2.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35' or one of it's dependencies. File not found.
Puchacz
  • 1,987
  • 2
  • 24
  • 38
  • Why don't you put all the DLLs in one folder when building the app? – Ilya Kogan Nov 28 '15 at 18:48
  • Becouse it will make lot of mess. I try to keep it clean – Puchacz Nov 28 '15 at 18:50
  • 1
    @IlyaKogan having all DLL in location searched by default (aka [probing path](https://msdn.microsoft.com/en-us/library/823z9h8w.aspx?f=255&MSPPError=-2147217396)) is significantly less interesting than hitting all load conflicts at run time. I totally see why OP want to keep it interesting and clean up mess when they neck deep in it :) – Alexei Levenkov Nov 28 '15 at 18:58
  • @AlexeiLevenkov for which app.config should I define probing path, for App.exe.config or Plugin.dll.config ? – Puchacz Nov 28 '15 at 19:03
  • Why do you even need to ask? Since DLL can't have config (http://stackoverflow.com/questions/594298/c-sharp-dll-config-file) you only have one option. – Alexei Levenkov Nov 28 '15 at 19:12

2 Answers2

5

I wrote this method to resolve assemblies. It is tweaked to fit my needs.

It basically hooks a AssemblyResolve event to the current application domain to retrieve an requested assembly from a list of directories.

There is no easy way to find where the assembly file that match the namespace to resolve, except by loading an assembly file and check to which namespace it belongs to.

Plus, it discards some unwanted assemblies (like serializers, resources...) and detects dlls or exes that are not .NET assemblies.

A better approach would consist in using the Global Assembly Cache, but we want our plugins to be fully moveable. So here it is.

public static class AssemblyResolver
{
    internal static void Hook(params string[] folders)
    {
        AppDomain.CurrentDomain.AssemblyResolve += (sender, args) =>
            {
                // Check if the requested assembly is part of the loaded assemblies
                var loadedAssembly = AppDomain.CurrentDomain.GetAssemblies().FirstOrDefault(a => a.FullName == args.Name);
                if (loadedAssembly != null)
                    return loadedAssembly;

                // This resolver is called when an loaded control tries to load a generated XmlSerializer - We need to discard it.
                // http://connect.microsoft.com/VisualStudio/feedback/details/88566/bindingfailure-an-assembly-failed-to-load-while-using-xmlserialization

                var n = new AssemblyName(args.Name);

                if (n.Name.EndsWith(".xmlserializers", StringComparison.OrdinalIgnoreCase))
                    return null;

                // http://stackoverflow.com/questions/4368201/appdomain-currentdomain-assemblyresolve-asking-for-a-appname-resources-assembl

                if (n.Name.EndsWith(".resources", StringComparison.OrdinalIgnoreCase))
                    return null;

                string assy = null;

                // Find the corresponding assembly file
                foreach (var dir in folders)
                {
                    assy = new[] { "*.dll", "*.exe" }.SelectMany(g => Directory.EnumerateFiles(dir, g)).FirstOrDefault(f =>
                               {
                                   try { return n.Name.Equals(AssemblyName.GetAssemblyName(f).Name, StringComparison.OrdinalIgnoreCase); }
                                   catch (BadImageFormatException) { return false; /* Bypass assembly is not a .net exe */ }
                                   catch (Exception ex) { throw new ApplicationException("Error loading assembly " + f, ex); }
                               });

                    if (assy != null)
                        return Assembly.LoadFrom(assy);
                }

                throw new ApplicationException("Assembly " + args.Name + " not found");
            };
    }
}

Here is how it works:

AssemblyResolver.Hook("\Plugins", "\CommonReferences");

Everytime some assemblies needs to be resolved, it will get the one that is loaded in memory, otherwise it will search in any given folders.

Larry
  • 17,605
  • 9
  • 77
  • 106
  • 1
    You mention in your answer the following "it will get the one that is loaded in memory". Why would .NET need to look for an assembly when it is already loaded into memory? Why is this not automatically resolved? – Laurence Aug 10 '21 at 14:52
  • Been a long time since... If your assumption is right, it means the event will not trigger and the first three lines would not be necessary at all. It deserves a try. – Larry Aug 17 '21 at 10:11
0

You can use "AssemblyResolve Event" (Method 3): https://support.microsoft.com/en-us/kb/837908

Gregor Primar
  • 6,759
  • 2
  • 33
  • 46