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
ofAppDomain.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:
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:
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
No Console Output When Using Allocconsole and Target Architecture X86
How to Disable a System Device Programmatically
Inheritance with Base Class Constructor with Parameters
Lock (Monitor) Internal Implementation in .Net
How to Include Quotes in a String
How to Convert an Iso8601 Timespan to a C# Timespan
Capturing Binary Output from Process.Standardoutput
Call Non-Static Method in Server-Side from Client-Side Using Javsscript
How to Find the State of Numlock, Capslock and Scrolllock in .Net
Getting Specified Node Values from Xml Document
How to Refresh Datasource of a Listbox
How to Get Little Endian Data from Big Endian in C# Using Bitconverter.Toint32 Method