Reputation: 584
I have a problem where I have a program that should load a plugin (DLL) from a specific directory where the DLL implements a specific base class. The problem is that my program that loads the DLL has a reference to an other DLL which the DLL being loaded also references. I will show an example of how the problem arises. This simple tests consists of 3 different solutions and 3 separate projects. NOTE: If I have all projects in the same solution, the problem does not arise.
Solution 1 - Project that defines the Base class and an interface AdapterBase.cs
namespace AdapterLib
{
public interface IAdapter
{
void PrintHello();
}
public abstract class AdapterBase
{
protected abstract IAdapter Adapter { get; }
public void PrintHello()
{
Adapter.PrintHello();
}
}
}
Solution 2 - Project that defines an implementation of the base class MyAdapter.cs
namespace MyAdapter
{
public class MyAdapter : AdapterBase
{
private IAdapter adapter;
protected override IAdapter Adapter
{
get { return adapter ?? (adapter = new ImplementedTestClass()); }
}
}
public class ImplementedTestClass : IAdapter
{
public void PrintHello()
{
Console.WriteLine("Hello beautiful worlds!");
}
}
}
Solution 3 - Main program which loads the DLL implementing AdapterBase* **Program.cs
namespace MyProgram {
internal class Program {
private static void Main(string[] args) {
AdapterBase adapter = LoadAdapterFromPath("C:\\test\\Adapter");
adapter.PrintHello();
}
public static AdapterBase LoadAdapterFromPath(string dir) {
string[] files = Directory.GetFiles(dir, "*.dll");
AdapterBase moduleToBeLoaded = null;
foreach (var file in files) {
Assembly assembly = Assembly.LoadFrom(file);
foreach (Type type in assembly.GetTypes()) {
if (type.IsSubclassOf(typeof(AdapterBase))) {
try {
moduleToBeLoaded =
assembly.CreateInstance(type.FullName, false, BindingFlags.CreateInstance, null, null,
null, null) as AdapterBase;
} catch (Exception ex) {
}
if (moduleToBeLoaded != null) {
return moduleToBeLoaded;
}
}
}
}
return moduleToBeLoaded;
}
}
}
So now the main program MyProgram.cs will try to load the DLL from path C:\test\Adapter and this works fine if I ONLY put the file MyAdapter.dll in that folder. However Solution 2 (MyAdapter.cs), will put both MyAdapter.dll and AdapterBase.dll in the output bin/ directory. Now if copy both those files to c:\test\Adapter the instance from the DLL does not get loaded since the comparison if (type.IsSubclassOf(typeof(AdapterBase))) { fails in MyProgram.cs.
Since MyProgram.cs already has a reference to AdapterBase.dll there seems to be some conflict in an other DLL gets loaded from a different path which references the same DLL. The loaded DLL seems to first resolve its dependencies against DLLs in the same folder. I think this has to do with assembly contexts and some problem with the LoadFrom method but I do not know how to get C# to realise that it actually is the same DLL which it already has loaded.
A solution is of course only to copy the only needed DLL but my program would be much more robust if it could handle that the other shared DLL also was there. I mean they are actually the same. So any solutions how to do this more robust?
Upvotes: 3
Views: 1124
Reputation: 584
I found a solution to my problem, although the path for the DLL can not be fully arbitrary. I was able to put the DLLs into, for example, bin/MyCustomFolder and load the DLL without getting the Type conflict problem.
The solution was to use the Assembly.Load()
method which takes the full assembly name as argument. So first I find the name of the assembly by loadning all DLLs in the specified folder and the use Assembly.GetTypes()
and checking if the Type is a subclass of AdapterBase
. Then I use Assembly.Load()
to actually load the assembly, which elegantly loads the DLL without any Type conflicts.
Program.cs
namespace MyProgram {
internal class Program {
private static void Main(string[] args) {
string codeBase = Assembly.GetExecutingAssembly().CodeBase;
UriBuilder uri = new UriBuilder(codeBase);
string path = Uri.UnescapeDataString(uri.Path);
string dir = Path.GetDirectoryName(path);
string pathToLoad = Path.Combine(dir, "MyCustomFolder");
AdapterBase adapter = LoadAdapterFromPath(pathToLoad);
adapter.PrintHello();
}
/// <summary>
/// Loads the adapter from path. LoadFile will be used to find the correct type and then Assembly.Load will be used to actually load
/// and instantiate the class.
/// </summary>
/// <param name="dir"></param>
/// <returns></returns>
public static AdapterBase LoadAdapterFromPath(string dir) {
string assemblyName = FindAssembyNameForAdapterImplementation(dir);
Assembly assembly = Assembly.Load(assemblyName);
Type[] types = assembly.GetTypes();
Type adapterType = null;
foreach (var type in types)
{
if (type.IsSubclassOf(typeof(AdapterBase)))
{
adapterType = type;
break;
}
}
AdapterBase adapter;
try {
adapter = (AdapterBase)Activator.CreateInstance(adapterType);
} catch (Exception e) {
adapter = null;
}
return adapter;
}
public static string FindAssembyNameForAdapterImplementation(string dir) {
string[] files = Directory.GetFiles(dir, "*.dll");
foreach (var file in files)
{
Assembly assembly = Assembly.LoadFile(file);
foreach (Type type in assembly.GetTypes())
{
if (type.IsSubclassOf(typeof(AdapterBase)))
{
return assembly.FullName;
}
}
}
return null;
}
}
}
NOTE: It is also important to add the extra probing path for Assembly.Load()
to find the assembly in bin/MyCustomFolder. The probing path must be a subdir of the executing assembly, therefore it is not possible to have the DLL in a completely arbitrary location. Update your App.config as below:
App.config
<configuration>
<runtime>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<probing privatePath="MyCustomFolder"/>
</assemblyBinding>
</runtime>
</configuration>
Tip: Actually I was having this problem for a Web application I had created. In that case you should update Web.config instead. Also in that case the probing path does not have the executing assembly as root but is actually the root of your web application. So in that case you can put you DLL folder MyCustomFolder directly in you web apps root folder, for example: inetpub\wwwroot\mywebapp\MyCustomFolder and then update Web.config as App.config above.
Upvotes: 0
Reputation: 942207
Yes, this is a type identity problem. A .NET type's identity is not just the namespace and type name, it also includes the assembly it came from. Your plugin has a dependency on the assembly that contains IAdapter, when LoadFrom() loads the plugin it is also going to need that assembly. The CLR finds it in the LoadFrom context, in other words in the c:\test\adapter directory, normally highly desirable since that allows plugins to use their own DLL versions.
Just not in this case. This went wrong because you plugin solution dutifully copied the dependencies. Normally highly desirable, just not in this case.
You'll have to stop it from copying the IAdapter assembly:
Copy Local
property to False.Copy Local
is the essence, the rest of the bullets are there just to make sure that an old copy doesn't cause problems. Since the CLR can no longer find the IAdapter assembly the "easy way", it is forced to keep looking for it. Now finding it in the Load context, in other words, the directory where the host executable is installed. Already loaded, no need to load the one-and-only again. Problem solved.
Upvotes: 2