Resolve Assembly References from Another Folder

Resolve assembly references from another folder

You should first find the folder where theses dlls are installed then use AppDomain.AssemblyResolve to hook assembly resolution and try to load the requested assemblies from this folder.

It will look something like this (not tested, and you need to check what args.Name contain exactly, could contain the version and the strong name along with type name) :

var otherCompanyDlls = new DirectoryInfo(companyFolder).GetFiles("*.dll");

AppDomain.CurrentDomain.AssemblyResolve += (sender, args) =>
{
var dll = otherCompanyDlls.FirstOrDefault(fi => fi.Name == args.Name);
if (dll == null)
{
return null;
}

return Assembly.Load(dll.FullName);
};

Load assemblies with references from subfolders at runtime

TL;DR;

You are looking for AssemblyResolve event of the AppDomain. If you are loading all the plugin assemblies in the current app domain, then you need to handle the event for AppDomain.CurrentDomain and load the requested assembly in the event handler.

No matter what folder structure you have for references, what you should do is:

  • Get all assembly files form plugins folder
  • Get all assembly files from references folder (entire hierarchy)
  • Handle AssemblyResolve of AppDomain.CurrentDomain and check if the requested assembly name is available files of reference folder, then load and return assembly.
  • For each assembly file in plugins folder, get all types and if the type implements your plugin interface, instantiate it and call its entry point for example.


Example

In this PoC I load all implementations of IPlugin dynamically at run-time from assemblies in Plugins folder and after loading them and resolving all dependencies at run-time, I call SayHello method of plugins.

The application which loads plugins, doesn't have any dependency to plugins and just loads them at run-time from the following folder structure:

Sample Image

This is what I did for loading, resolving and calling the plugins:

var plugins = new List<IPlugin>();
var pluginsPath = Path.Combine(Application.StartupPath, "Plugins");
var referencesPath = Path.Combine(Application.StartupPath, "References");

var pluginFiles = Directory.GetFiles(pluginsPath, "*.dll",
SearchOption.AllDirectories);
var referenceFiles = Directory.GetFiles(referencesPath, "*.dll",
SearchOption.AllDirectories);

AppDomain.CurrentDomain.AssemblyResolve += (obj, arg) =>
{
var name = $"{new AssemblyName(arg.Name).Name}.dll";
var assemblyFile = referenceFiles.Where(x => x.EndsWith(name))
.FirstOrDefault();
if (assemblyFile != null)
return Assembly.LoadFrom(assemblyFile);
throw new Exception($"'{name}' Not found");
};

foreach (var pluginFile in pluginFiles)
{
var pluginAssembly = Assembly.LoadFrom(pluginFile);
var pluginTypes = pluginAssembly.GetTypes()
.Where(x => typeof(IPlugin).IsAssignableFrom(x));
foreach (var pluginType in pluginTypes)
{
var plugin = (IPlugin)Activator.CreateInstance(pluginType);
var button = new Button() { Text = plugin.GetType().Name };
button.Click += (obj, arg) => MessageBox.Show(plugin.SayHello());
flowLayoutPanel1.Controls.Add(button);
}
}

And this is the result:

Sample Image

You can download or clone the code:

  • Clone r-aghaei/loadpluginassembly
  • Download zip file

load a DLL reference from a different folder?

If the DLL is in a sub folder you can add this folder to the AppDomain private path.

The private bin path of an AppDomain cannot be changed once the AppDomain has been created (AppDomain.AppendPrivatePath is obsolete), if you don't want to create a new AppDomain, you can modify the probing element in the App.config.

If it's not in a sub folder, things get more complicated, you can load the assembly using its full path, but if it references other assembly, the CLR won't be able to resolve the dependencies.

If you want to resolve dependencies, you can add an handler to AppDomain.AssemblyResolve and fetch the needed assembly in your specific folder.

Another possibility is to place this DLL (and dependencies) in the GAC.

Proper way to resolving assemblies from subfolders

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.

Clarification needed: How does .NET runtime resolve assembly references from parent folder?

AFAIK the only solution is to have at least "stub" exes in MyAppBase and then define subdirectories as sources for DLL's in app.config for each application.

NTFS does support "mounting" other directories (ie. hard links) as subdirectories, but they're very unautomatic and since I've not tried going that way except theoretizing, I can't tell if that would work as a hack.

.NET Reference dll from other location

From C# 3.0 in a Nutshell, 3rd edition, by Joseph and Ben Albahari, p. 557-558:

Deploying Assemblies Outside the Base Folder

Sometimes you might choose to deploy assemblies to locations other than the application base directory [...] To make this work, you must assist the CLR in finding the assemblies outside the base folder. The easiest solution is to handle the AssemblyResolve event.

(We can ignore the fact that in your case, someone other than you is deploying the assemblies.)

Which you tried. But a very important clue follows somewhat later. Read the two code comments:

public static void Loader
{
static void Main(string[] args)
{
AppDomain.CurrentDomain.AssemblyResolve += FindAssem;

// We must switch to another class before attempting to use
// any of the types in C:\ExtraAssemblies:
Program.Go();
}

static Assembly FindAssem(object sender, ResolveEventArgs args)
{
string simpleName = new AssemblyName(args.Name).Name;
string path = @"C:\ExtraAssemblies\" + simpleName + ".dll";

if (!File.Exists(path)) return null;
return Assembly.LoadFrom(path);
}
}

public class Program
{
public static void Go()
{
// Now we can reference types defined in C:\ExtraAssemblies
}
}

As you see, the class where you resolve the external assemblies must not refer to any type from any of the external DLLs anywhere. If it did, code execution would stop way before your AssemblyResolve ever gets a chance to run.

Load Assembly with dependent references

A code snippet to resolve assemblies at runtime:

private static Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args) {

String DllName = new AssemblyName(args.Name).Name + ".dll";

return Assembly.LoadFile(DllName);
}

Set it at the beginning of your your plugin initializer:

AppDomain.CurrentDomain.AssemblyResolve += CurrentDomain_AssemblyResolve;

No error checking is included.

The actual assembly can be loaded from any path, the example above loads it from the current working directory. You can load it from embedded resource as well.

How would I force my executable to load an assembly from a different path?

In the end, I solved this using the AssemblyResolve event and the current processes directory:

                    AppDomainSetup domainSetup = new AppDomainSetup()
{
ApplicationBase = _config.ShadowPath
};
AppDomain domain = AppDomain.CreateDomain(available.Description.ShortName, null, domainSetup);
domain.AssemblyResolve += (source, args) =>
{
int comma = args.Name.IndexOf(',');
string path = Path.Combine(Path.GetDirectoryName(Process.GetCurrentProcess().Modules[0].FileName), args.Name.Substring(0, comma) + ".dll");
return Assembly.LoadFrom(path);
};

Assembly resolving in ASP.NET project outside of bin folder

We can use PreApplicationStartMethodAttribute
and mark them some public static void method(in web-project assembly) with no arguments. This can be done at AssemblyInfo.cs class
For example:

[assembly: PreApplicationStartMethod(
typeof(Web.Initializer), "Initialize")]

That method will be called before compilation but after processing of the web.config. So we must explicitly tell to the compiler witch assembly it need to use during compilation. Also we need to subscribe here on Assembly Resolve event so we can manage assemblies resolving. Here is example:

  public static class Initializer
{
public static void Initialize()
{
AppDomain.CurrentDomain.AssemblyResolve += LoadFromCommonBinFolder;
var referAsm = Assembly.GetExecutingAssembly().GetReferencedAssemblies();
foreach (var assemblyName in referAsm)
{
try
{
var curAsm = Assembly.Load(assemblyName);
BuildManager.AddReferencedAssembly(curAsm);
LoadChildReferences(curAsm);
}
catch {}
}
}

private static void LoadChildReferences(Assembly curAsm)
{
foreach (var assemblyName in curAsm.GetReferencedAssemblies())
{
try
{
BuildManager.AddReferencedAssembly(Assembly.Load(assemblyName));
}
catch {}
}
}

private static Assembly LoadFromCommonBinFolder(object sender, ResolveEventArgs args)
{
string commonBinFolder = System.Configuration.ConfigurationManager.AppSettings["CommonBinFolderPath"];

if (String.IsNullOrEmpty(commonBinFolder))
{
throw new InvalidOperationException("​​CommonBinFolderPath in the app.config isn't seted.");
}

string assemblyName = new AssemblyName(args.Name).Name;
string assemblyPath = Path.Combine(commonBinFolder, assemblyName);

if (!File.Exists(assemblyPath + ".dll"))
{
if (!File.Exists(assemblyPath + ".exe"))
{
//searching for resources
var ci = CultureInfo.CurrentUICulture;
assemblyPath = Path.Combine(commonBinFolder, ci.Name, assemblyName + ".dll");
if (!File.Exists(assemblyPath))
{
assemblyPath = Path.Combine(commonBinFolder, ci.Parent, assemblyName + ".dll");
if (!File.Exists(assemblyPath))
{
return null;
}
}
}
}

return Assembly.LoadFrom(assemblyPath);
}
}

At this case "Web.Project.Assembly" still must be located in the bin folder. Others assemblies can shared from any folder.

Assemblies that are included under compilation Element in the web.config file must be also in the bin folder or at sub folder with probing element setted.

In same cases we must also add to this code adding references to child assemblies.



Related Topics



Leave a reply



Submit