I'm a bit confused here and haven't gotten much help from google. Here's what I'm trying to do:
public Boolean LoadModule(String moduleHandle)//name of module MUST match its .dll name. Name of AppDomain is the same as the Handle.
{
try
{
AppDomain moduleDomain = AppDomain.CreateDomain(moduleHandle);
String pathToDll = #"C:\IModules.dll"; //Full path to dll you want to load
Type moduleType = typeof(IModule);
IModule loadedModule = (IModule)moduleDomain.CreateInstanceFromAndUnwrap(pathToDll, moduleType.FullName);
ModuleList.Add(loadedModule, moduleDomain);
Broadcast("Module loaded: " + moduleHandle, ModuleManagerHandle);
return true;
}
catch (Exception e)
{
//console writeline the error? probably cant
OutputBox.AppendText(e.ToString() + Environment.NewLine);
return false;
}
}
I thought I finally had this figured out but when I try to instantiate the IModule (ConsoleModule, in this case), I get the following error:
System.MissingMethodException: Constructor on type 'IModules.IModule' not found.
I take this to mean that I need to have a constructor, as if this were a class object instantiating itself on this function call, but I cannot make an interface have a constructor.
I have seen other threads suggesting ways to solve this problem, but they use assembly instead of appdomain, which will mess up the ability to unload modules. I'm concerned that without the ability to unload modules the application will suffer memory bloating over time.
The end goal is to be able to write a module, leave the program running and load/unload the modules during runtime without any changes to the core program, and add functionality on the go.
Anyone know of a workaround or maybe a better way to deal with dynamic module loading and unloading?
This is fixed with .NET 5.0 AssemblyLoadContext:
var basePath = Path.GetDirectoryName(Assembly.GetEntryAssembly().Location);
AssemblyLoadContext moduleAssemblyLoadContext = new AssemblyLoadContext(moduleHandle, true);
Assembly moduleAssembly = moduleAssemblyLoadContext.LoadFromAssemblyPath($"{basePath}\\{moduleHandle}.dll");
Type[] types = moduleAssembly.GetTypes();
foreach (Type type in types)
{
// Does this class support the transport interface?
Type typeModule = type.GetInterface("IModule");
if (typeModule == null)
{
// Not supported.
continue;
}
// This class supports the interface. Instantiate it.
IModule loadedModule = moduleAssembly.CreateInstance(type.FullName) as IModule;
if (loadedModule != null)
{
loadedModule.LoadedModule(this);
ModuleList.Add(loadedModule, moduleAssemblyLoadContext);
Broadcast("Module loaded: " + moduleHandle, ModuleManagerHandle);
OutputTextBox.AppendText(moduleHandle + " was loaded." + Environment.NewLine);
// Successfully created the interface. We are done.
return true;
}
}
return false;
Can't find the source anymore but found it looking for a related problem (you can find it on MSDN anyways). This successfully loads and unloads assemblies into their context. User must set the isCollectible value to TRUE to enable full unloading.
Only issue I had is that .NET 5.0 is not compatible -with itself- yet and libraries loaded as .NET 5.0 into .NET 5.0 programs will give a BadImageFormatException when trying to load the assembly. To fix, set the LIBRARY to the next most recent target framework (in my case, .NET Core 3.1) and move the newly compiled dll to wherever it goes and the application should run using the new dll.
The error tells you that there is no default (empty) constructor found for the type IModule. Since IModule is an interface, the message seems to make some sense.
Resulotion: Instantiate a class that implements IModule. An interface can never be intantiated on its own.
To instantiate the class, just one line neds to be changed:
Type moduleType = typeof(ClassThatImplementIModule);
You can still cast the instance to IModule
Related
I have a plugin architecutre which is using a Shared DLL to implement a plugin interface and a base plugin class (eg. IPlugin & BasePlugin). The shared DLL is used by both the Plugin and the main application.
For some reason, when I try to cast an object that was instantiated by the main application to the shared plugin interface (IPlugin), I get an InvalidCastException saying I cannot cast this interface.
This is despite:
The class definitely implementing the plugin interface.
The Visual Studio debugger saying that "(objInstance is IPlugin)" is true when I mouse-over the statement, despite the same 'if' condition evaluating as false when I step-through the code or run the .exe.
The Visual Studio Immediate Window also confirms that the above condition is true and that is it possible to cast the object successfully.
I have cleaned/deleted all bin folders and also tried both VS2019 and VS2022 with exactly the same outcome.
I am going a little crazy here because I assume it is something to do with perhaps with multiple references of the same DLL somehow causing the issue (like this issue). The fact that the debugger tells me everything is okay makes it hard to trouble-shoot. I'm not sure if it's relevant but I have provided example code and the file structure below:
Shared.dll code
public interface IPlugin
{
}
public class BasePlugin : IPlugin
{
}
Plugin.dll code
class MyPlugin : BasePlugin
{
void Init()
{
// The plugin contains references to the main app dlls as it requires
// to call various functions inside the main app such as adding menu items etc
}
}
Main.exe
(Note: This is pseudo-code only and does not show the plugin framework used to load the plugin DLLs via Assembly.Load())
var obj = Activator.CreateInstance(pluginType); // Plugin type will be 'MyPlugin'
if (obj is IPlugin) // <== This executes as false when executed but evaluates to true in the Visual Studio debugger
{
}
var castObj = (IPlugin)obj // <== This will cause an invalid cast exception
Folder structure
|--- MainApp.exe
|--- Shared.dll
|--- Addins
|------- Plugin.dll
|------- Shared.dll
Does anyone know the reasons how I can trouble-shoot this issue and what might be causing it?
My suggestion is that (using the Properties\Signing tab) you sign your shared dll assembly with a Strong Name Key. If your plugin classes reference a signed copy of the dll, there should be no possibility of confusion.
The application has no prior knowledge of what the plugins are going to be, but it does know that it's going to support IPlugin. Therefore, I would reference the PlugInSDK project directly in the app project.
Testbench
Here's what works for me and maybe by comparing notes we can get to the bottom of it.
static void Main(string[] args)
{
Confirm that the shared dll (known here as PlugInSDK.dll) has already been loaded and display the source location. In the Console output, note the that PublicKeyToken is not null for the SDK assembly (this is due to the snk signing).
var sdk =
AppDomain.CurrentDomain.GetAssemblies()
.Single(asm=>(Path.GetFileName(asm.Location) == "PlugInSDK.dll"));
Console.WriteLine(
$"'{sdk.FullName}'\nAlready loaded from:\n{sdk.Location}\n");
The AssemblyLoad event provides the means to examine on-demand loads as they occur:
AppDomain.CurrentDomain.AssemblyLoad += (sender, e) =>
{
var name = e.LoadedAssembly.FullName;
if (name.Split(",").First().Contains("PlugIn"))
{
Console.WriteLine(
$"{name}\nLoaded on-demand from:\n{e.LoadedAssembly.Location}\n");
}
};
It doesn't matter where the plugins are located, but when you go to discover them I suggest SearchOption.AllDirectories because they often end up in subs like netcoreapp3.1.
var pluginPath =
Path.Combine(
Path.GetDirectoryName(Assembly.GetEntryAssembly().Location),
"..",
"..",
"..",
"PlugIns"
);
Chances are good that a copy of PlugInSDK.dll is going to sneak into this directory. Make sure that the discovery process doesn't accidentally pick this up. Any other Type that implements IPlugin gets instantiated and put in the list.
List<IPlugin> plugins = new List<IPlugin>();
foreach (
var plugin in
Directory.GetFiles(pluginPath, "*.dll", SearchOption.AllDirectories))
{
// Exclude copy of the SDK that gets put here when plugins are built.
if(Path.GetFileName(plugin) == "PlugInSDK.dll") continue;
// Be sure to use 'LoadFrom' not 'Load'
var asm = Assembly.LoadFrom(plugin);
// Check to make sure that any given *.dll
// implements IPlugin before adding to list.
Type plugInType =
asm.ExportedTypes
.Where(type =>
type.GetTypeInfo().ImplementedInterfaces
.Any(intfc => intfc.Name == "IPlugin")
).SingleOrDefault();
if ((plugInType != null) && plugInType.IsClass)
{
plugins.Add((IPlugin)Activator.CreateInstance(plugInType));
}
}
Now just display the result of the plugin discovery.
Console.WriteLine("LIST OF PLUGINS");
Console.WriteLine(
string.Join(
Environment.NewLine,
plugins.Select(plugin => plugin.Name)));
Console.ReadKey();
}
Is this something you're able to repro on your side?
Please bear with me, I spent 30+ hours trying to get this work - but without success.
At the start of my program I load an Assembly (dll) in bytearray and delete it afterwards.
_myBytes = File.ReadAllBytes(#"D:\Projects\AppDomainTest\plugin.dll");
Later on in the program I create a new Appdomain, load the byte array and enumerate the types.
var domain = AppDomain.CreateDomain("plugintest", null, null, null, false);
domain.Load(_myBytes);
foreach (var ass in domain.GetAssemblies())
{
Console.WriteLine($"ass.FullName: {ass.FullName}");
Console.WriteLine(string.Join(Environment.NewLine, ass.GetTypes().ToList()));
}
The types get correctly listed:
ass.FullName: plugin, Version=1.0.0.0, Culture=neutral,PublicKeyToken=null
...
Plugins.Test
...
Now I want to create an instance of that type in the new AppDomain
domain.CreateInstance("plugin", "Plugins.Test");
This call results in System.IO.FileNotFoundException and I don't know why.
When I look in ProcessExplorer under .NET Assemblies -> Appdomain: plugintest I see that the assembly is loaded correctly in the new appdomain.
I suspect the exception to occur because the assembly is searched again on disk. But why does the program want to load it again?
How can I create an instance in a new appdomain with an assembly loaded from byte array?
The main problem here is thinking that you can instantiate a plugin while executing code in your primary appdomain.
What you need to do instead, is create a proxy type which is defined in an already loaded assembly, but instantiated in the new appdomain. You can not pass types across app domain boundaries without the type's assembly being loaded in both appdomains. For instance, if you want to enumerate the types and print to console as you do above, you should do so from code which is executing in the new app domain, not from code that is executing in the current app domain.
So, lets create our plugin proxy, this will exist in your primary assembly and will be responsible for executing all plugin related code:
// Mark as MarshalByRefObject allows method calls to be proxied across app-domain boundaries
public class PluginRunner : MarshalByRefObject
{
// make sure that we're loading the assembly into the correct app domain.
public void LoadAssembly(byte[] byteArr)
{
Assembly.Load(byteArr);
}
// be careful here, only types from currently loaded assemblies can be passed as parameters / return value.
// also, all parameters / return values from this object must be marked [Serializable]
public string CreateAndExecutePluginResult(string assemblyQualifiedTypeName)
{
var domain = AppDomain.CurrentDomain;
// we use this overload of GetType which allows us to pass in a custom AssemblyResolve function
// this allows us to get a Type reference without searching the disk for an assembly.
var pluginType = Type.GetType(
assemblyQualifiedTypeName,
(name) => domain.GetAssemblies().Where(a => a.FullName == name.FullName).FirstOrDefault(),
null,
true);
dynamic plugin = Activator.CreateInstance(pluginType);
// do whatever you want here with the instantiated plugin
string result = plugin.RunTest();
// remember, you can only return types which are already loaded in the primary app domain and can be serialized.
return result;
}
}
A few key points in the comments above I will reiterate here:
You must inherit from MarshalByRefObject, this means that the calls to this object can be proxied across app-domain boundaries using remoting.
When passing data to or from the proxy class, the data must be marked [Serializable] and also must be in a type which is in the currently loaded assembly. If you need your plugin to return some specific object to you, say PluginResultModel then you should define this class in a shared assembly which is loaded by both assemblies/appdomains.
Must pass an assembly qualified type name to CreateAndExecutePluginResult in its current state, but it would be possible to remove this requirement by iterating the assemblies and types yourself and removing the call to Type.GetType.
Next, you need to create the domain and run the proxy:
static void Main(string[] args)
{
var bytes = File.ReadAllBytes(#"...filepath...");
var domain = AppDomain.CreateDomain("plugintest", null, null, null, false);
var proxy = (PluginRunner)domain.CreateInstanceAndUnwrap(typeof(PluginRunner).Assembly.FullName, typeof(PluginRunner).FullName);
proxy.LoadAssembly(bytes);
proxy.CreateAndExecutePluginResult("TestPlugin.Class1, TestPlugin, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null");
}
Going to say this again because it's super important and I didn't understand this for a long time: when you're executing a method on this proxy class, such as proxy.LoadAssembly this is actually being serialized into a string and being passed to the new app domain to be executed. This is not a normal function call and you need to be very careful what you pass to/from these methods.
This call results in System.IO.FileNotFoundException and I don't know why. I suspect the exception to occur because the assembly is searched again on disk. But why does the program want to load it again?
The key here is understanding loader contexts, there's an excellent article on MSDN:
Think of loader contexts as logical buckets within an application domain that hold assemblies. Depending on how the assemblies were being loaded, they fall into one of three loader contexts.
Load context
LoadFrom context
Neither context
Loading from byte[] places the assembly in the Neither context.
As for the Neither context, assemblies in this context cannot be bound to, unless the application subscribes to the AssemblyResolve event. This context should generally be avoided.
In the code below we use the AssemblyResolve event to load the assembly in the Load context, enabling us to bind to it.
How can I create an instance in a new appdomain with an assembly loaded from byte array?
Note this is merely a proof of concept, exploring the nuts and bolts of loader contexts. The advised approach is to use a proxy as described by #caesay and further commented upon by Suzanne Cook in this article.
Here's an implementation that doesn't keep a reference to the instance (analogous to fire-and-forget).
First, our plugin:
Test.cs
namespace Plugins
{
public class Test
{
public Test()
{
Console.WriteLine($"Hello from {AppDomain.CurrentDomain.FriendlyName}.");
}
}
}
Next, in a new ConsoleApp, our plugin loader:
PluginLoader.cs
[Serializable]
class PluginLoader
{
private readonly byte[] _myBytes;
private readonly AppDomain _newDomain;
public PluginLoader(byte[] rawAssembly)
{
_myBytes = rawAssembly;
_newDomain = AppDomain.CreateDomain("New Domain");
_newDomain.AssemblyResolve += new ResolveEventHandler(MyResolver);
}
public void Test()
{
_newDomain.CreateInstance("plugin", "Plugins.Test");
}
private Assembly MyResolver(object sender, ResolveEventArgs args)
{
AppDomain domain = (AppDomain)sender;
Assembly asm = domain.Load(_myBytes);
return asm;
}
}
Program.cs
class Program
{
static void Main(string[] args)
{
byte[] rawAssembly = File.ReadAllBytes(#"D:\Projects\AppDomainTest\plugin.dll");
PluginLoader plugin = new PluginLoader(rawAssembly);
// Output:
// Hello from New Domain
plugin.Test();
// Output:
// Assembly: mscorlib
// Assembly: ConsoleApp
foreach (var asm in AppDomain.CurrentDomain.GetAssemblies())
{
Console.WriteLine($"Assembly: {asm.GetName().Name}");
}
Console.ReadKey();
}
}
The output shows CreateInstance("plugin", "Plugins.Test") is successfully called from the default app domain, although it has no knowledge of the plugin assembly.
Have you tried providing the Assemblies full name, In your case
domain.CreateInstance("plugin, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null", "Plugins.Test");
I'm currently at a point where I really need some advice. In our company we mix many languages that are unmanaged like PowerBuilder or pure C++. For now we need a lot of code out of .NET. So my first purpose was why not just make a plugin system via COM.
This is what I'm trying to achieve at the moment. Everything works fine, the plugin system can load plugins. But as soon as I expose my plugin system to COM and try to load a plugin via, as an example VBS, I always get the the following error:
Unable to load one or more of the requested types. Retrieve the LoaderExceptions property for more information
From my research I found that this problem occurs when, during Assembly.GetTypes(), the type couldn't be loaded. Which is really strange as I have put my interface into it's own assembly and reference it by my plugin system and the plugin, so that the interface is always the same.
But as I debugged a bit more I found that actually the problem seems to not be my interface. The load problem actually happens when he tries to load the type of my class of the plugin which inherits of the interface. Maybe it's the interface or something else.
But to point out for now, as long as I use the plugin system via managed code directly there is no problem. As soon as I do it via COM I receive this error. So I assume that I'm missing or messing something up with COM.
At the moment I really need a solution but not just the solution an explanation of the solution would also be really nice because I actually want to understand what I messed up.
Here is the download link to the solution. Maybe you find someting.
Solution
[ComVisible(true)]
public bool Initialize(string dllPlugin)
{
try
{
string dllPluginPath = Assembly.GetExecutingAssembly().Location.Replace("IncoPluginSystem.dll", "") + "plugins\\";
string completePluginPath = dllPluginPath + dllPlugin + ".dll";
if (!File.Exists(completePluginPath))
{
pluginError = "The plugin could not be found in the plugins directory.";
return false;
}
plugin = Assembly.LoadFile(completePluginPath);
if (plugin == null)
{
pluginError = "No plugin loaded. Pls initialize first";
return false;
}
foreach (Type t in plugin.GetTypes())
{
if (t.GetInterface("IPlugin") != null)
{
pluginInstance = Activator.CreateInstance(t) as IPlugin;
}
}
return true;
}
catch (Exception e)
{
pluginError = e.Message;
return false;
}
}
The Problem happens in the foreach loop where u access the Types of the assembly.
I added a more detailed Exception handling. This is the Error when i expose it to COM
Could not load file or assembly 'IncoPluginSystemInterface, Version-1.0.0.0, Culture=neutral, PuplicKeyToken=497bca4abf979e3e' or one of its dependencies. The system cannot finde the file specified.
Fusion Log:
WRN: Assembly binding logging is turned OFF.
Note: There is some performance penalty associated with assembly bind failure logging.
The references of the DLL are just the standard ones of creating a .net library. Nothing added.
The structure of the plugin system is the following
IncoPluginSystem.dll
IncoPluginSystemInterface.dll
plugins
->IncoPluginSystemInterface.dll
->TestPlugin.dll
So the missing dll is actually at the 2 spots where needed. Maybe it's because the IncoPluginSystem.dll is loaded as a COM object so maybe the path is incorrect but I'm not sure. I tested and changed all I can imagine. So it definitly has to be something I messed up with COM.
I was able to locate the problem. It's actually really that my DLL is in the wrong place. As soon as i put the DLL into the GAC it works. So for now my question is - how can i determine where to put the dll where it was missing. In my exception i only see what dll is missing but not the path. Does anyone know how to determine this?
Ok I was able to answer it myself. To Explain what was wrong. What I did to may help\improve others work in future.
I turned on the FusionLog in the Registry and on the LoaderException I checked for the FusionLog
catch (ReflectionTypeLoadException ex)
{
StringBuilder sb = new StringBuilder();
foreach (Exception exSub in ex.LoaderExceptions)
{
sb.AppendLine(exSub.Message);
FileNotFoundException exFileNotFound = exSub as FileNotFoundException;
if (exFileNotFound != null)
{
if (!string.IsNullOrEmpty(exFileNotFound.FusionLog))
{
sb.AppendLine("Fusion Log:");
sb.AppendLine(exFileNotFound.FusionLog);
}
}
sb.AppendLine();
}
pluginError = sb.ToString();
}
After enabling the FisonLog on the Registry (HKLM\SOFTWARE\Microsoft\Fusion!EnableLog (DWORD 1) I was able to see that the missing DLL is actually needed at the location of the executing program that call's the OLEObject to create an instance of the system via COM.
I wasn't aware of this, but yeah in some way it's rather logic. Luckily i didn't messed up anything on the code it was just in the wrong place.
Ok, here is the deal:
I want to load a user defined Assembly into my AppDomain, but I only want to do so if the specified Assembly matches some requirements. In my case, it must have, among other requirements, an Assembly level Attribute we could call MandatoryAssemblyAttribute.
There are two paths I can go as far as I see:
Load the Assembly into my current AppDomain and check if the Attribute is present. Easy but inconvenient as I'm stuck with the loaded Assembly even if it doesnt have the MandatoryAssemblyAttribute. Not good.
I can create a new AppDomain and load the Assembly from there and check if my good old MandatoryAddemblyAttribute is present. If it is, dump the created AppDomain and go ahead and load the Assembly into my CurrentAppDomain, if not, just dump the new AppDomain, tell the user, and have him try again.
Easier said than done. Searching the web, I've found a couple of examples on how to go about this, including this previous question posted in SO:Loading DLLs into a separate AppDomain
The problem I see with this solution is that you actually have to know a type (full name) in the Assembly you want to load to begin with. That is not a solution I like. The main point is trying to plug in an arbitrary Assembly that matches some requirements and through attributes discover what types to use. There is no knowledge beforehand of what types the Assembly will have. Of course I could make it a requirement that any Assembly meant to be used this way should implement some dummy class in order to offer an "entry point" for CreateInstanceFromAndUnwrap. I'd rather not.
Also, if I go ahead and do something along this line:
using (var frm = new OpenFileDialog())
{
frm.DefaultExt = "dll";
frm.Title = "Select dll...";
frm.Filter = "Model files (*.dll)|*.dll";
answer = frm.ShowDialog(this);
if (answer == DialogResult.OK)
{
domain = AppDomain.CreateDomain("Model", new Evidence(AppDomain.CurrentDomain.Evidence));
try
{
domain.CreateInstanceFrom(frm.FileName, "DummyNamespace.DummyObject");
modelIsValid = true;
}
catch (TypeLoadException)
{
...
}
finally
{
if (domain != null)
AppDomain.Unload(domain);
}
}
}
This will work fine, but if then I go ahead and do the following:
foreach (var ass in domain.GetAssemblies()) //Do not fret, I would run this before unloading the AppDomain
Console.WriteLine(ass.FullName);
I get a FileNotFoundException. Why?
Another path I could take is this one: How load DLL in separate AppDomain But I'm not getting any luck either. I'm getting a FileNotFoundException whenever I choose some random .NET Assembly and besides it defeats the purpose as I need to know the Assembly's name (not file name) in order to load it up which doesn't match my requirements.
Is there another way to do this barring MEF (I am not targeting .NET 3.5)? Or am I stuck with creating some dummy object in order to load the Assembly through CreateInstanceFromAndUnwrap? And if so, why can't I iterate through the loaded assemblies without getting a FileNotFoundException? What am I doing wrong?
Many thanks for any advice.
The problem I see with this solution is that you actually have to know
a type (full name) in the Assembly
That is not quite accurate. What you do need to know is a type name is some assembly, not necessarily the assembly you are trying to examine. You should create a MarshalByRef class in your assembly and then use CreateInstanceAndUnwrap to create an instance of it from your own assembly (not the one you are trying to examine). That class would then do the load (since it lives in the new appdomain) and examine and return a boolean result to the original appdomain.
Here is some code to get you going. These classes go in your own assembly (not the one you are trying to examine):
This first class is used to create the examination AppDomain and to create an instance of your MarshalByRefObject class (see bottom):
using System;
using System.Security.Policy;
internal static class AttributeValidationUtility
{
internal static bool ValidateAssembly(string pathToAssembly)
{
AppDomain appDomain = null;
try
{
appDomain = AppDomain.CreateDomain("ExaminationAppDomain", new Evidence(AppDomain.CurrentDomain.Evidence));
AttributeValidationMbro attributeValidationMbro = appDomain.CreateInstanceAndUnwrap(
typeof(AttributeValidationMbro).Assembly.FullName,
typeof(AttributeValidationMbro).FullName) as AttributeValidationMbro;
return attributeValidationMbro.ValidateAssembly(pathToAssembly);
}
finally
{
if (appDomain != null)
{
AppDomain.Unload(appDomain);
}
}
}
}
This is the MarshalByRefObject that will actually live in the new AppDomain and will do the actual examination of the assembly:
using System;
using System.Reflection;
public class AttributeValidationMbro : MarshalByRefObject
{
public override object InitializeLifetimeService()
{
// infinite lifetime
return null;
}
public bool ValidateAssembly(string pathToAssembly)
{
Assembly assemblyToExamine = Assembly.LoadFrom(pathToAssembly);
bool hasAttribute = false;
// TODO: examine the assemblyToExamine to see if it has the attribute
return hasAttribute;
}
}
This can be easily done by using a managed assembly reader, such as Mono.Cecil. You'd check whether the assembly is decorated with an attribute, without loading the assembly in the AppDomain, and actually, without messing with AppDomains at all. For instance, with Cecil:
bool HasMandatoryAttribute (string fileName)
{
return AssemblyDefinition.ReadAssembly (fileName)
.CustomAttributes
.Any (attribute => attribute.AttributType.Name == "MandatoryAttribute");
}
That's basically what are doing most of he plugins systems, as creating an AppDomain and tearing it down is quite expensive for the runtime.
I have been trying to get the following code to work(everything is defined in the same assembly) :
namespace SomeApp{
public class A : MarshalByRefObject
{
public byte[] GetSomeData() { // }
}
public class B : MarshalByRefObject
{
private A remoteObj;
public void SetA(A remoteObj)
{
this.remoteObj = remoteObj;
}
}
public class C
{
A someA = new A();
public void Init()
{
AppDomain domain = AppDomain.CreateDomain("ChildDomain");
string currentAssemblyPath = Assembly.GetExecutingAssembly().Location;
B remoteB = domain.domain.CreateInstanceFromAndUnwrap(currentAssemblyPath,"SomeApp.B") as B;
remoteB.SetA(someA); // this throws an ArgumentException "Object type cannot be converted to target type."
}
}
}
What I'm trying to do is pass a reference of an 'A' instance created in the first AppDomain to the child domain and have the child domain execute a method on the first domain. In some point on 'B' code I'm going to call 'remoteObj.GetSomeData()'. This has to be done because the 'byte[]' from 'GetSomeData' method must be 'calculated' on the first appdomain.
What should I do to avoid the exception, or what can I do to achieve the same result?
The actual root cause was your dll was getting loaded from different locations in the two different app domains. This causes .NET to think they are different assemblies which of course means the types are different (even though they have the same class name, namespace etc).
The reason Jeff's test failed when run through a unit test framework is because unit test frameworks generally create AppDomains with ShadowCopy set to "true". But your manually created AppDomain would default to ShadowCopy="false". This would cause the dlls to be loaded from different locations which leads to the nice "Object type cannot be converted to target type." error.
UPDATE: After further testing, it does seem to come down to the ApplicationBase being different between the two AppDomains. If they match, then the above scenario works. If they are different it doesn't (even though I've confirmed that the dll is loaded into both AppDomains from the same directory using windbg) Also, if I turn on ShadowCopy="true" in both of my AppDomains, then it fails with a different message: "System.InvalidCastException: Object must implement IConvertible".
UPDATE2: Further reading leads me to believe it is related to Load Contexts. When you use one of the "From" methods (Assembly.LoadFrom, or appDomain.CreateInstanceFromAndUnwrap), if the assembly is found in one of the normal load paths (the ApplicationBase or one of the probing paths) then is it loaded into the Default Load Context. If the assembly isn't found there, then it is loaded into the Load-From Context. So when both AppDomains have matching ApplicationBase's, then even though we use a "From" method, they are both loaded into their respective AppDomain's Default Load Context. But when the ApplicationBase's are different, then one AppDomain will have the assembly in its Default Load Context while the other has the assembly in it's Load-From Context.
I can duplicate the issue, and it seems to be related to TestDriven.net and/or xUnit.net. If I run C.Init() as a test method, I get the same error message. However, if I run C.Init() from a console application, I do not get the exception.
Are you seeing the same thing, running C.Init() from a unit test?
Edit: I'm also able to duplicate the issue using NUnit and TestDriven.net. I'm also able to duplicate the error using the NUnit runner instead of TestDriven.net. So the problem seems to be related to running this code through a testing framework, though I'm not sure why.
This is a comment to #RussellMcClure but as it is to complex for a comment I post this as an answer:
I am inside an ASP.NET application and turning off shadow-copy (which would also solve the problem) is not really an option, but I found the following solution:
AppDomainSetup adSetup = new AppDomainSetup();
if (AppDomain.CurrentDomain.SetupInformation.ShadowCopyFiles == "true")
{
var shadowCopyDir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
if (shadowCopyDir.Contains("assembly"))
shadowCopyDir = shadowCopyDir.Substring(0, shadowCopyDir.LastIndexOf("assembly"));
var privatePaths = new List<string>();
foreach (var dll in Directory.GetFiles(AppDomain.CurrentDomain.SetupInformation.PrivateBinPath, "*.dll"))
{
var shadowPath = Directory.GetFiles(shadowCopyDir, Path.GetFileName(dll), SearchOption.AllDirectories).FirstOrDefault();
if (!String.IsNullOrWhiteSpace(shadowPath))
privatePaths.Add(Path.GetDirectoryName(shadowPath));
}
adSetup.ApplicationBase = shadowCopyDir;
adSetup.PrivateBinPath = String.Join(";", privatePaths);
}
else
{
adSetup.ApplicationBase = AppDomain.CurrentDomain.SetupInformation.ApplicationBase;
adSetup.PrivateBinPath = AppDomain.CurrentDomain.SetupInformation.PrivateBinPath;
}
This will use the shadow-copy directory of the main app-domain as the application-base and add all shadow-copied assemblies to the private path if shadow-copy is enabled.
If someone has a better way of doing this please tell me.