Proper dependency loading for plug-ins using Assembly.Load, Assembly.LoadFile - c#

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:
In the same AppDomain as the Windows Service hosting the plug-ins
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:
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.
I can't use the GAC
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.

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.

Related

AssemblyLoadContext.Default in netcore 3.1 resolve the plugin type but can't excute its instance when the plugin use transitive dependency

I have a console application in netcoreapp3.1 that use a netstandard2.0 plugin.
The plugin reference a class library and implement an interface
All dll dependencies are in the the plugin folder and the plugin.dep.json include all referenced library.
When I run:
AssemblyLoadContext.Default.LoadFromAssemblyPath("path/to/main_myplugin.dll");//load plugin
it resolve the type of interface
When i try to run an instance as given below it fail:
if (type != null) //type is resolved and not null
{
var instance = (IContract)Activator.CreateInstance(type); //instance is created
Console.WriteLine($"Create instance : {instance.GetType()}"); // ok instance is created
var ret = instance.Execute(); //!!!fire exception here
Console.WriteLine(ret);
}
and fire error message:
System.IO.FileNotFoundException: 'Could not load file or assembly 'MyLibObjectsLib, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null'. The system cannot find the file specified.'
If I loaded all dependencies, it works fine.
Should I load all dependencies when using AssemblyLoadContext.Default or it's a bug?
I asked this question in the dotnet project
All credit go to #vitek-karas.
The detailed answer is:
Currently this is by design. LoadFromAssemblyPath as well as any other LoadAssembly-like methods only ever load that assembly, they don't try to load a "plugin". For this case to work you would need the AssemblyDependencyResolver and hook it up to the Default ALC (probably via the Resolving event), and hope that there are no collisions between the host app and the plugin (since they will share all dependencies) and so on. Generally this is why it's better to load plugins into their own ALCs as that creates the necessary isolation and also makes it easy to hook up AssemblyDependencyResolver.
Higher-level answer - in 3.0 we didn't try to provide a "plugin load" API as there were too many open questions in how it should behave (the exact isolation behavior is very tricky to get right so that it would work for most use cases). Instead we provided the necessary building blocks (ALC, ADR, diag improvements, ...) and rely on users to write the cca 20 lines of code to implement the custom ALC. In 5.0 we are improving diagnostics by adding detailed tracing around the entire assembly binding process, that should help debugging issues when loading assemblies in general, but specifically when implementing custom ALCs.

MEF not detecting plugin dependencies

I have a problem with MEF and using a plugins folder.
I have a main app that supports plugins via MEF. The main app does not reference the assemblies containing the .NET Task type for multithreading but one or more of the plugins do.
The plugins are located in a Plugins folder and I'm using a DirectoryCatalog.
I keep getting ReflectionTypeLoadException being thrown by MEF on
Unable to load one or more of the requested types. Retrieve the
LoaderExceptions property for more information.
The LoaderExceptions property contains a FileNotFoundException
"Could not load file or assembly 'System.Threading.Tasks,
Version=1.5.11.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a' or
one of its dependencies. The system cannot find the file
specified.":"System.Threading.Tasks, Version=1.5.11.0,
Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"
The plugin is referencing System.Threading.Tasks via a Microsoft NuGet package reference.
This is my helper method:
public static void Compose(IEnumerable<string> searchFolders, params object[] parts)
{
// setup composition container
var catalog = new AggregateCatalog();
// check if folders were specified
if (searchFolders != null)
{
// add search folders
foreach (var folder in searchFolders.Where(System.IO.Directory.Exists))
{
catalog.Catalogs.Add(new DirectoryCatalog(folder, "*.dll"));
}
}
catalog.Catalogs.Add(new AssemblyCatalog(typeof(Program).Assembly));
// compose and create plug ins
var composer = new CompositionContainer(catalog);
composer.ComposeParts(parts);
}
public class MEFComposer
{
[ImportMany(typeof(IRepository))]
public List<IRepository> Repositories;
[ImportMany(typeof(ILogging))]
public List<ILogging> LoggingRepositories;
[ImportMany(typeof(IPlugin))]
public List<IPlugin> Plugins;
}
This is the code I'm using to invoke MEF and load the plugins.
public void Compose()
{
// try to connect with MEF types
try
{
var parts = new MEFComposer();
MEFHelpers.Compose(new[] { Path.Combine(Application.StartupPath, "Plugins") }, parts);
RepositoryFactory.Instance.Repository = parts.Repositories.FirstOrDefault();
Logging.Repositories.AddRange(parts.LoggingRepositories);
foreach (var plugin in parts.Plugins)
{
this.applicationApi.Plugins.Add(plugin);
plugin.Connect(this.applicationApi);
}
}
catch
{
// ERR: handle error
}
}
Why is MEF not able to load the plugins even though the Microsoft.Threading.Tasks.dll and related assembly files are present in the Plugins folder, but not the main application bin folder? And is there any way of telling MEF to search the Plugins folder for assembly dependencies?
Having a plugin model means I can't anticipate what assemblies a plugin may be referencing so I cannot include them in the main bin folder for the app, which is why I want all related plugins and plugins dependencies to be in the plugins folder.
You have run into an essential problem when supporting 3rd party plugins. Your problem is that when you are loading the plugin, the runtime will search for its references when needed only in the specified folders your AppDomain knowns of. That would be the WorkingDirectory of that process, then path etc.
Essentially, you are loading an Plugin that requires System.Threading.Tasks. That DLL lies within your /Plugin folder. As .net loads your plugin, it will search for that assembly but has no way in finding it as it is located within the /Plugin folder and fails.
There are a couple of solutions to that.
Do not use a Plugin folder
That would be the most simple solution, when all assemblies (including references of the 3rd party lib) are located in your WorkingDirectory, .net will have not trouble finding all references of that plugin.
Add the Plugin folder to your Probing path
This will extend the paths .net will search the 3rd party references:
https://learn.microsoft.com/en-us/dotnet/framework/deployment/how-the-runtime-locates-assemblies#locating-the-assembly-through-probing
Use AppDomains for each plugin.
An AppDomain is my choice of go here as it allows you to load the assembly not only in its "own" container, but also can simulate the Working directory only for the plugin. That can come handy if on of the Plugins uses the same framework as your application but in a different version for example.
Load the dependencies by yourself
That would be the "straight forward" method of solving this. You can load every assembly as ReflectionOnly, Determinate all dependencies and then load these. That will almost Garantie to work.
4.1. AssemblyResolve event
This is just another way how to "redirect" .net to load assemblies from the PluginFolder
https://learn.microsoft.com/en-us/dotnet/api/system.appdomain.assemblyresolve?view=netframework-4.8
Edit:
There is also a certain problem with the AssemblyCatalog, it uses Assembly.Load instead of Assembly.LoadFrom to load the given assembly. That is an essential part of your problem, as LoadFrom would probe the path where the assembly originates from for its dependencies where Load does not.
https://github.com/JPVenson/MSEF/blob/master/JPB.Shell/JPB.Shell.MEF/Model/StrongNameCatalog.cs
You could use an Catalog like this one that is using LoadFrom instead.
Disclaimer: I am the creator of that project.

Dynamic Assembly Loading in .Net 4.0

My problem begins with moving a .Net 2.0 application to .Net 4.0. The reason I had to do this was that Windows 8 does not enable the earlier .Net versions by default and my application cannot ask the user to enable it.
The application is a NPAPI plugin which uses .Net components via UnmanagedExports. I designed it as a low integrity application and therefore it has to reside in the users 'LocalLow' directory.
In my application I used a dynamic assembly loading mechanism to load several assemblies at runtime. I used the following method to load an assembly,
MyInterface Instance;
Assembly assembly = Assembly.LoadFrom(AssemblyFile);
Type type = assembly.GetType(Identifier); // Identifier is implementing the MyInterface
Instance = Activator.CreateInstance(type) as MyInterface;
// Do something with the Instance
After modifying the project to .Net 4.0, I noticed that the plugin crashes when the binaries are placed inside the LocalLow directory (It works in other places). My next step was to create a minimalistic plugin with least possible code to figure out the issue. I noticed that the dynamic assembly loading failed with the following exception,
System.IO.FileLoadException: Could not load file or assembly '<assemblyPath>' or one of its dependencies. Operation is not supported. (Exception from HRESULT: 0x80131515 (COR_E_NOTSUPPORTED)) --->
System.NotSupportedException: An attempt was made to load an assembly from a network location which would have caused the assembly to be sandboxed in previous versions of the .NET Framework. This release of the .NET Framework does not enable CAS policy by default, so this load may be dangerous. If this load is not intended to sandbox the assembly, please enable the loadFromRemoteSources switch. See http://go.microsoft.com/fwlink/?LinkId=131738 for more information.
I tried the following approaches to create a separate domain and load the assemblies but with no luck,
http://blogs.msdn.com/b/shawnfa/archive/2009/06/08/more-implicit-uses-of-cas-policy-loadfromremotesources.aspx
http://www.west-wind.com/weblog/posts/2009/Jan/19/Assembly-Loading-across-AppDomains
Adding the configuration 'loadFromRemoteSources' did not work either. It seems that the .Net component does not load .dll.config files. (Could be because of UnmanagedExporting)
My questions are,
Is it possible to dynamically load an assembly from LocalLow?
Does the new CAS policy in CLR 4.0 apply to LocalLow as well? From what I understood so far it should affect only assemblies loaded over the network
Is there any other way to overcome this issue?
While it doesn't address your LocalLow issue specifically, if you are able to "read a file" from the directory, you might be able to use the "work around" detailed here:
How can I get LabView to stop locking my .NET DLL?

Late binding in child AppDomain (.Net)

I am trying to develop a plugin architecture in .Net. The application will be a .Net application. There will be directories which holds the plug-ins. Each directory will represent a plugin. Each plugin directory will also contain all the dependency dlls as well. The plugins need to be stored in separate AppDomain as the plugins may use the same assemblies, but different versions.
As it iterates through the foreach loop in Init(), I get a
System.IO.FileNotFoundException : Could not load file or assembly '[Assembly Name], Version=1.0.0.0, Culture=neutral, PublicKeyToken=null' or one of its dependencies. The system cannot find the file specified.
in _apDomain.Load() for assemblies that are not in the main project.
Code:
readonly private AppDomain _appDomain;
internal ItemModuleAppDomain()
{
AppDomainSetup info = AppDomain.CurrentDomain.SetupInformation;
_appDomain = AppDomain.CreateDomain("ChildDomain");
}
public void Init(string dllDir)
{
string[] dlls = Directory.GetFiles(dllDir, "*.dll", SearchOption.TopDirectoryOnly);
foreach(string dll in dlls)
{
_appDomain.Load(AssemblyName.GetAssemblyName(dll));
}
Any idea why? I tried several methods such as reading the assembly as an array of bytes to load.
I believe AppDomain.Load may be a wrong method for your purpose - it loads the assembly into both current app domain as well as target domain. And you get an error because your assembly is located in a sub folder and .NET does not know about it. You have to give private paths in configuration (http://msdn.microsoft.com/en-us/library/15hyw9x3.aspx). You can also use AssemblyResolve or TypeResolve event of AppDomain for resolution.
You should also look at AppDomain.CreateInstanceFrom method to load your main plugin type (and containing assembly).

Assembly Load and loading the "sub-modules" dependencies - "cannot find the file specified"

There are several questions out there that ask the same question. However the answers they received I cannot understand. Similar questions:
Dynamically Load Assembly and manually force path to get referenced assemblies ;
Loading assemblies and its dependencies
The question in short:
I need to figure out how dependencies, ie References in my modules can be loaded dynamically. Right now I am getting "The system cannot find the file specified" on Assemblies referenced in my so called modules.
I cannot really understand how to use the AssemblyResolve event.
The longer version
I have one application, MODULECONTROLLER, that loads separate modules.
These "separate modules" are located in well-known subdirectories, like
appBinDir\Modules\Module1
appBinDir\Modules\Module2
Each directory contains all the DLLs that exists in the bin-directory of those projects after a build.
So the MODULECONTROLLER loads all the DLLs contained in those folders using this code:
byte[] bytes = File.ReadAllBytes(dllFileFullPath);
Assembly assembly = null;
assembly = Assembly.Load(bytes);
I am, as you can see, loading the byte[]-array (so I don't lock the DLL-files).
Now, in for example MODULE1, I have a static reference called MyGreatXmlProtocol. The MyGreatXmlProtocol.dll then also exists in the directory appBinDir\Modules\Module1 and is loaded using the above code
When code in the MODULE1 tries to use this MyGreatXmlProtocol, I get:
Could not load file or assembly 'MyGreatXmlProtocol, Version=1.0.3797.26527, Culture=neutral, PublicKeyToken=null' or one of its dependencies. The system cannot find the file specified.
So, in a post (like this one) they say that
To my understanding reflection will
load the main assembly and then search
the GAC for the referenced assemblies,
if it cannot find it there, you can
then incorparate an assemblyResolve
event:
First; is it really needed to use the AssemblyResolve-event to make this work? Shouldn't my different MODULEs themself load their DLLs, as they are statically referenced?
Second; if AssemblyResolve is the way to go - how do I use it? I have attached a handler to the Event but I never get anything on MyGreatXmlProctol.
Edit
Code regarding the AssemblyResolve-event handler:
public GUI()
{
InitializeComponent();
AppDomain.CurrentDomain.AssemblyResolve += new ResolveEventHandler(CurrentDomain_AssemblyResolve);
...
}
//
Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args)
{
Console.WriteLine(args.Name);
return null;
}
try to do assembly = Assembly.Load(bytes); to MyGreatXmlProtocol assembly. I read somewhere that if you load assembly by byte array you have to resolve dependencies manually.

Categories