Loading of plugin from dll - c#

I've started on a simple plugin loader that monitors a directory and loads plugins if the dll(s) in it contain the IPlugin interface.
public class PluginLoader : Dictionary<string, IPlugin>
{
private FileSystemWatcher watcher;
private string pluginPath;
public PluginLoader()
: base()
{
pluginPath = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "plugins");
if (!Directory.Exists(pluginPath))
Directory.CreateDirectory(pluginPath);
watcher = new FileSystemWatcher(pluginPath, "*.dll");
watcher.IncludeSubdirectories = true;
watcher.Created += watcher_Created;
watcher.EnableRaisingEvents = true;
}
private void watcher_Created(object sender, FileSystemEventArgs e)
{
LoadPlugin(e.FullPath);
}
private void LoadPlugin(string path)
{
IPlugin plugin = null;
Assembly assembly = Assembly.LoadFrom(path);
foreach (Type type in assembly.GetExportedTypes())
{
if (type.IsClass && type.GetInterfaces().Count(iType => iType == typeof(IPlugin)) == 1)
{
ConstructorInfo constructor = type.GetConstructor(new Type[] { });
object instance = constructor.Invoke(new object[] { });
plugin = instance as IPlugin;
// plugin is now not null
}
}
if (plugin != null && !this.ContainsKey(plugin.PluginName))
{
this[plugin.PluginName] = plugin;
}
}
}
This version of LoadPlugin() works, the plugin variable ends up being != null. This one, however, does not work:
private void LoadPlugin(string path)
{
IPlugin plugin = null;
Assembly assembly = Assembly.LoadFrom(path);
foreach (Type type in assembly.GetExportedTypes())
{
if (type.IsClass && type.GetInterface(typeof(IPlugin).FullName) != null)
{
ConstructorInfo constructor = type.GetConstructor(new Type[] { });
object instance = constructor.Invoke(new object[] { });
plugin = instance as IPlugin;
// plugin is still null
}
}
if (plugin != null && !this.ContainsKey(plugin.PluginName))
{
this[plugin.PluginName] = plugin;
}
}
I just don't understand why. So my question is: Why does plugin end up being null in the second example?
Solution:
Problem was that I had two different IPlugin types because its assembly existed in two different locations. So deleting the Framework.Lib.dll in the plugin directory solved it.

The only reason I can think of is that the type implements an IPlugin with the same namespace but from a different assembly. The typeof(IPlugin).FullName would then match between your plugin loader and the plugin, but the implemented type still does not equal the expected type.
The first example does match the exact same type, in the second example you're matching by the FullName which only includes the namespace, not the assembly a type is loaded from.
To determine whether this is the case, try log the following value:
bool matches = typeof(IPlugin).IsAssignableFrom(type);
string expected = typeof(IPlugin).AssemblyQualifiedName;
string actual = type.GetInterface(typeof(IPlugin).FullName).AssemblyQualifiedName;
typeof(IPlugin).IsAssignableFrom(type) is probably what you were looking for in the first place.

Related

AssemblyLoadContext cannot find in assembly based on type

I cannot seem to get a DefinedType based on a interface (IStartupPlugin), it seems to work with AssemblyLoadContext.Default.LoadFromAssemblyPath however when using my own class it doesn't :(
var raw = AssemblyLoadContext.Default.LoadFromAssemblyPath(pluginPath + Path.DirectorySeparatorChar + fqName + ".dll");
var raw2 = assemblyContext.LoadFromAssemblyPath(pluginPath + Path.DirectorySeparatorChar + fqName + ".dll");
bool hasDefined = raw.DefinedTypes
.Where(x => typeof(IStartupPlugin).IsAssignableFrom(x) && x != null && x != typeof(IStartupPlugin))
.Any();
bool hasDefined2 = raw2.DefinedTypes
.Where(x => typeof(IStartupPlugin).IsAssignableFrom(x) && x != null && x != typeof(IStartupPlugin))
.Any();
public class PluginAssemblyContext : AssemblyLoadContext
{
private readonly AssemblyDependencyResolver _resolver;
public PluginAssemblyContext(string mainAssemblyToLoadPath) : base(isCollectible: true)
{
_resolver = new AssemblyDependencyResolver(mainAssemblyToLoadPath);
this.LoadFromAssemblyPath(mainAssemblyToLoadPath);
}
protected override Assembly Load(AssemblyName name)
{
string assemblyPath = _resolver.ResolveAssemblyToPath(name);
if (assemblyPath != null)
{
return LoadFromAssemblyPath(assemblyPath);
}
return null;
}
}
hasDefined = true
hasDefined2 = false
Both of them is expected to have true as value hasDefined and hasDefined2
It seems I have goofed with the creation of the assembly resolver! I used the path to the '.dll' file (mainAssemblyToLoadPath), however it seems like it asked for the path to the plugin directory (main directory), or so I guessed it seems to work fine now :)
It's usually a bad practice to run such code within a constructor. Define a static (extension) method in a static class then load the desired assembly from anywhere in your app using an instance of your custom PluginAssemblyContext class.
Concretely, try this:
public class PluginAssemblyContext : AssemblyLoadContext
{
private readonly AssemblyDependencyResolver _resolver;
public PluginAssemblyContext(string mainAssemblyToLoadPath) : base(isCollectible: true)
{
_resolver = new AssemblyDependencyResolver(mainAssemblyToLoadPath);
}
protected override Assembly Load(AssemblyName name)
{
string assemblyPath = _resolver.ResolveAssemblyToPath(name);
if (assemblyPath != null)
{
return LoadFromAssemblyPath(assemblyPath);
}
return null;
}
}
Define this class somewhere within a namespace with proper usings...
public static class PluginAssemblyContextExtensions
{
public static Assembly FromAssemblyPath(this AssemblyLoadContext context, string path)
{
return context.LoadFromAssemblyPath(path);
}
}
...from somewhere else (preferably within a static method/constructor), call the above code when your app has finished starting. I didn't have a chance to test this but I suspect the smell of your code comes from within the PluginAssemblyContext constructor you defined. Please let me know if I'm right (or wrong).

Why is PrivateBinPath when creating a new AppDomain not being considered at all?

So when I create a new app domain, specifying PrivateBinPath doesn't seem to be working correctly. I know that it has to be a subfolder of application base.
Here's the example:
public class PluginHandler : MarshalByRefObject
{
public void Launch()
{
var domainSetup = new AppDomainSetup()
{
ApplicationBase = Util.AssemblyDirectory,
PrivateBinPath = #"Plugins"
};
appDomain = AppDomain.CreateDomain(Guid.NewGuid().ToString(), null, domainSetup);
assemblyLoader = appDomain.CreateInstanceAndUnwrap
(Assembly.GetExecutingAssembly().FullName, typeof(AssemblyLoader).FullName) as AssemblyLoader;
thread = new Thread(() =>
assemblyLoader.LoadAssembly(PluginPath, ProxyBase, this));
thread.Start();
}
}
public class AssemblyLoader : MarshalByRefObject
{
private object pluginObj;
private MethodInfo pluginStop;
[HandleProcessCorruptedStateExceptions]
public bool LoadAssembly(string path, CoreBase proxyBase, PluginHandler pluginHandler)
{
var assembly = Assembly.Load(File.ReadAllBytes(path));
var types = assembly.GetTypes().Where(x => x.IsSubclassOf(typeof(Core))).ToArray();
for (int i = 0; i < types.Length; i++)
{
FieldInfo coreBase = types[i].GetField
("proxyBase", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
MethodInfo pluginRun = types[i].GetMethod("PluginRun");
pluginStop = types[i].GetMethod("PluginStop");
if (pluginRun != null)
{
pluginObj = assembly.CreateInstance(types[i].FullName);
coreBase.SetValue(pluginObj, proxyBase);
pluginHandler.IsLoaded = true;
pluginHandler.CorePlugin = pluginObj;
if (!proxyBase.LaunchedPlugins.Contains(pluginHandler))
{
proxyBase.LaunchedPlugins.Add(pluginHandler);
}
pluginRun.Invoke(pluginObj, null);
return true;
}
}
return false;
}
}
So as you can see, I'm loading assembly in Proxy on new app domain, and the loaded assembly "Plugin". Now, whenever PluginRun is invoked, I get an exception FileNotFound, because it's searching for Plugin assembly in base path, yet it's in Plugins folder.
So even if specifying PrivateBinPath to PLugins folder, it doesn't seem to be able to find it.
I used Binding Log Viewer and private path is NULL, just a note.
Any solution to this issue? Thanks

After Assembly.LoadFile setup Callback

I'm trying to figure out how to setup a call back after I load a DLL dynamically using Assembly.LoadFile. In the below example, "MyCallBackMethod" is a non delegate, so it will not work. I've created a new DLL, and Reference that DLL in both projects and I can pass that object around, but is that really the correct way to do it or am I overlooking something simple?
string fullDLLPath = #"C:\Code\MyTest\MyDLL.dll";
Assembly assembly = Assembly.LoadFile(fullDLLPath);
Type type = assembly.GetType("MyNameSpace.MyClass");
if (type != null)
{
//get both the start and stop method
MethodInfo myMethod = type.GetMethod("MyMethod");
if (myMethod != null)
{
object result = null;
//create instance
object classInstance = Activator.CreateInstance(type, null);
//get all parameters
ParameterInfo[] paramInfo = myMethod.GetParameters();
object[] paramToPass = null;
foreach (ParameterInfo pi in paramInfo)
{
paramToPass = new object[] { MyCallBackMethod };
break;
}
result = myMethod.Invoke(classInstance, paramToPass);
if (result != null)
{
Console.WriteLine(result);
}
}
}
I'll just go with a 3rd DLL which is referenced in both projects. Then I can pass that Class through the object[] with no issues. The DLL then puts that as a static within it's own Class and can reference that to send messages back to the EXE.
EXE:
static public Logger.Textlog Logging { get; } = new Logger.Textlog();
...
foreach (ParameterInfo pi in paramInfo)
{
paramToPass = new object[] { Logging };
}
DLL:
static public Logger.Textlog Logging { get; } = new Logger.Textlog();
public bool MyMethod(Logger.Textlog passedTextLog = null)
{
if (passedTextLog != null) {
Logging = passedTextLog;
Logging.WriteLine(LOG_TRANSACTION, "Here");
}
...
}

Reflection and Attributes in .Net Core refuse to cooperate

I have class:
public class Domain
{
public static Assembly[] GetAssemblies()
{
var assemblies = new List<Assembly>();
foreach (ProcessModule module in Process.GetCurrentProcess().Modules)
{
try
{
var assemblyName = AssemblyLoadContext.GetAssemblyName(module.FileName);
var assembly = Assembly.Load(assemblyName);
assemblies.Add(assembly);
}
catch (BadImageFormatException)
{
// ignore native modules
}
}
return assemblies.ToArray();
}
}
My main class looks like:
class Program
{
public static Dictionary<String, Type> animals;
static void Main(string[] args)
{
var assTab = Domain.GetAssemblies();
foreach (var assembly in assTab)
{
var m = assembly.GetCustomAttribute<Method>();
if (m != null)
{
animals.Add(m.Name, assembly.GetType());
}
}
Where Method is a MethodAttribute class. In Animal.dll I have class like Dog, Cat etc. with Attribute [Method("cat")] and so on. To my dictionary I want to add this attribute name as string and type as Type (dog, Dog) and so on. My problem is that my program do not do that. After running program in variable animal I have 0 score. What should I change to achieve what I want?
The problem is in this line:
var m = assembly.GetCustomAttribute<Method>();
This line is getting attributes on the assembly. So if the attribute is not applied to the assembly then you will get null back.
What you need to do is get the types in the assembly first and then check each type for whether it has the attribute on it. Something more like this:
foreach(Type type in assembly.GetTypes())
{
var attr = assembly.GetCustomAttribute<Method>();
if (attr!=null)
{
animals.Add(attr.Name, type);
}
}

How to find uses of a method in a second assembly?

I am trying to map out the dependency matrix for a collection of assemblies including what dependency methods are used where. The basic DLL dependency matrix was easy but I am finding it difficult to get the method mapping. The tool I have been using is jbevain MethodBaseRocks.cs.
The dependencies of the assembly I want to parse are being loaded according to AppDomain.CurrentDomain.GetAssemblies() but I am getting FileNotFoundException and ReflectionTypeLoadExceptions.
Is there a correct way to load referenced assemblies?
I have tried LoadFile, LoadFrom and ReflectionOnlyLoadFrom all with the same result.
How do I get the types for methods that use a references Type?
I can step around the ReflectionTypeLoadExceptions error with the answer from here but these methods are the exact ones I want to map
TestLibraryA.dll
namespace TestLibraryA
{
public class TestClassA
{
public int DoStuff(int a, int b)
{
return a + b;
}
}
}
TestLibaryB.dll
using TestLibraryA;
namespace TestLibraryB
{
public class TestClassB
{
public int DoStuffAgain()
{
TestClassA obj = new TestClassA();
int ans = obj.DoStuff(3, 5);
return ans;
}
public TestClassA DoOtherStuff()
{
TestClassA result = new TestClassA();
return result;
}
}
}
Parser Code Application
public List<string> GetMethods()
{
List<string> result = new List<string> { };
Assembly dependencyAssembly = Assembly.LoadFile("TestLibraryA.dll");
Assembly targetAssembly = Assembly.LoadFile("TestLibaryB.dll");
Type[] types = targetAssembly.GetTypes();
// ReflectionTypeLoadExceptions thrown if a dependency type is used
// NB Not demo'ed in this example
foreach(var type in types)
{
foreach(var method in type.GetMethods())
{
// With the above DoStuffAgain() method is returned but DoOtherStuff() is not
var instructions = MethodBodyReader.GetInstructions(method);
// FileNotFoundException thrown saying TestLibraryA.dll not loaded
// the line throwing the error is
// MethodBodyReader(method)
// this.body = method.GetMethodBody();
foreach (var instruction in instructions)
{
MethodInfo methodInfo = instruction.Operand as MethodInfo;
if (methodInfo != null)
{
result.Add(methodInfo.DeclaringType.FullName + "." + methodInfo.Name);
}
}
}
}
return result;
}
To avoid the exception you need to add the code below before you load TestLibaryB
AppDomain.CurrentDomain.AssemblyResolve += new ResolveEventHandler(CurrentDomain_AssemblyResolve);
private static Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args)
{
if (args.Name == "TestLibaryA...")
{
return Assembly.LoadFrom("TestLibaryA's Path");
}
return null;
}

Categories