ResolveEventHandler not invoked when object is created in new AppDomain - c#

I hope it wouldn't be a duplicate, I see plenty of similar questions, but any of them help can't help me.
I have such Method code in nameofdll.dll:
DllMethod()
{
Assembly assembly = Assembly.GetExecutingAssembly();
Type type = assembly.GetType("Class");
MethodInfo method = type.GetMethod("Method");
object obj = Activator.CreateInstance(type);
method.Invoke(...);
}
and Class - constructor add AssemblyResolver
class Class
{
public Class()
{
AppDomain.CurrentDomain.AssemblyResolve += new ResolveEventHandler(MyResolveEventHandler);
}
public void Method()
{...}
private Assembly MyResolveEventHandler(object sender, ResolveEventArgs args)
{...}
}
Till now AssemblyResolver is invoked when I call metod from my main app.exe which is in the same folder as nameofdll.dll, let's name the folder as mainfolder and also from powershell script in this way:
load dll(mainfolder + nameofdll.dll)
call DllMethod()
Everything works fine, but I have to unload dll which AssemblyResolver had loaded, because Method is called multiple times witch different dlls versions.
So I modified DllMethod() like that:
{
Assembly assembly = Assembly.GetExecutingAssembly();
MethodInfo method = type.GetMethod("Method");
//new part
AppDomainSetup domainSetup = new AppDomainSetup();
domainSetup.ApplicationBase = Path.GetDirectoryName(assembly.Location);
AppDomain Domain = AppDomain.CreateDomain(Class, null, domainSetup);
object obj = Domain.CreateInstanceAndUnwrap(assembly.FullName, Class);
method.Invoke(...);
AppDomain.Unload(Domain);
}
I use
domainSetup.ApplicationBase = Path.GetDirectoryName(assembly.Location);
because BaseDirecotry was powershell folder and CreateInstanceAndUnwrap fails to find nameofdll.dll.
And finally the problem is that everyting is ok when using app.exe, but when I called that dll method from powershell script, the AssemblyResolver wasn't invoked and immediately I got error that it could not load some dlls.
I also try to copy domain setting, and add
AppDomainSetup domainSetup = AppDomain.CurrentDomain.SetupInformation;
where CurrentDomain is domain in which AssemblyResolver works fine before, but it didn't solve the problem.

Related

Loading DLL dynamically into separated app domain then unload

I'm trying to load a DLL file into a separated app domain and invoke a method in the DLL file and get some response from it. The DLL file did not exist in the project bin folder when the application starts, the DLL file is loaded from another folder. After I have done with the DLL file I want to unload the app domain that I have just created.
The steps:
Created a new app domain
Load my DLL I want to the app domain
Invoke the method and get response
Unload the app domain
Here is what I've tried so far
This is the code in MyAssembly.dll
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
namespace MyAssembly
{
public class MyClass
{
public static string MyMethod()
{
return "Hello there, this is message from MyAssembly";
}
}
}
Here is how I load the DLL file
using System.Diagnostic;
using System.IO;
private class ProxyClass : MarshalByRefObject
{
public void LoadAssembly()
{
AppDomain dom;
string domainName = "new:" + Guid.NewGuid();
try
{
//Create the app domain
dom = AppDomain.CreateDomain(domainName, null, new AppDomainSetup
{
PrivateBinPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "bin"),
ApplicationBase = AppDomain.CurrentDomain.BaseDirectory,
ConfigurationFile = AppDomain.CurrentDomain.SetupInformation.ConfigurationFile,
ApplicationName = AppDomain.CurrentDomain.SetupInformation.ApplicationName,
ShadowCopyFiles = "true",
ShadowCopyDirectories = "true",
LoaderOptimization = LoaderOptimization.SingleDomain,
});
string dllPath = #"C:\MyProject\MyAssembly.dll";//the path to my assembly file I want to load
//load the assembly to the new app domain
Assembly asm = dom.Load(File.ReadAllBytes(dllPath));//Error occurred at here
Type baseClass = asm.GetType("MyAssembly.MyClass");
MethodInfo targetMethod = baseClass.GetMethod("MyMethod");
string result = targetMethod.Invoke(null, new object[]{});
/*Do something to the result*/
}
catch(Exception ex)
{
Debug.WriteLine(ex.Message);
Debug.WriteLine(ex.ToString());
}
finally
{
//Finally unload the app domain
if (dom != null) AppDomain.Unload(dom);
}
}
}
public void BeginLoadDll()
{
ProxyClass proxy = new ProxyClass();
proxy.LoadAssembly();
//OR like this, which gave me same error message as well
//var dom = AppDomain.CreateDomain("new:" + Guid.NewGuid(), null, new AppDomainSetup
// {
// PrivateBinPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "bin"),
// ApplicationBase = AppDomain.CurrentDomain.BaseDirectory,
// ConfigurationFile = AppDomain.CurrentDomain.SetupInformation.ConfigurationFile,
// ApplicationName = AppDomain.CurrentDomain.SetupInformation.ApplicationName,
// ShadowCopyFiles = "true",
// ShadowCopyDirectories = "true",
// LoaderOptimization = LoaderOptimization.SingleDomain,
// });
//ProxyClass proxy = (ProxyClass)dom.CreateInstanceAndUnwrap(
// typeof(ProxyClass).Assembly.FullName, typeof(ProxyClass).FullName);
//pr.LoadAssembly(watcherData, filePath);
}
Here is something I've observed so far, I'm not sure if that is just me or I'm missing something
-If the "MyAssembly.dll" exists in the project bin folder before the application starts, I can load the dll file
-If the "MyAssembly.dll" did not exist in the project bin folder before application starts, instead it was loaded somewhere else other than the project bin folder, I cannot load the dll file. For example, the project bin folder is "C:\Main\MyMainProject\MyMainProject\bin", and DLL is loaded from C:\MyProject\MyAssembly.dll"
-If I move the "MyAssembly.dll" file into the bin folder (using File.Copy() or File.Move()), it somehow stop the rest of the code to be executed.
The error message I received
Could not load file or assembly 'MyAssembly, Version=1.0.0.0,
Culture=neutral, PublicKeyToken=2c20c56a5e1f4bd4' or one of its dependencies.
The system cannot find the file specified.
EDIT
I know I can use Assembly.LoadFrom(#"PATH\TO\MY\DLL"), but the problem with this one is I cannot unload the DLL
After days of research, I finally got it working. Below is my final working code.
Useful reference links that helped me achieved this
https://learn.microsoft.com/en-us/dotnet/api/system.appdomain.createinstanceandunwrap?view=netframework-4.8#System_AppDomain_CreateInstanceAndUnwrap_System_String_System_String_
C# reflection - load assembly and invoke a method if it exists
Using AppDomain in C# to dynamically load and unload dll
The code in MyAssembly.dll is same as in the question. I also realized that I can return an object type as well.
How I load the DLL file into separated app domain and unload the app domain
public void MethodThatLoadDll()
{
AppDomain dom = null;
//declare this outside the try-catch block, so we can unload it in finally block
try
{
string domName = "new:" + Guid.NewGuid();
//assume that the domName is "new:50536e71-51ad-4bad-9bf8-67c54382bb46"
//create the new domain here instead of in the proxy class
dom = AppDomain.CreateDomain(, null, new AppDomainSetup
{
PrivateBinPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "bin"),
ApplicationBase = AppDomain.CurrentDomain.BaseDirectory,
ConfigurationFile = AppDomain.CurrentDomain.SetupInformation.ConfigurationFile,
ApplicationName = AppDomain.CurrentDomain.SetupInformation.ApplicationName,
ShadowCopyFiles = "true",
ShadowCopyDirectories = "true",/*yes they are string value*/
LoaderOptimization = LoaderOptimization.SingleDomain,
DisallowBindingRedirects = false,
DisallowCodeDownload = true,
});
ProxyClass proxy = (ProxyClass)dom.CreateInstanceAndUnwrap(
typeof(ProxyClass).Assembly.FullName, typeof(ProxyClass).FullName);
string result = proxy.ExecuteAssembly("MyParam");
/*Do whatever to the result*/
}
catch(Exception ex)
{
//handle the error here
}
finally
{
//finally unload the app domain
if(dom != null) AppDomain.Unload(dom);
}
}
My class that inherits MarshalByRefObject
private class ProxyClass : MarshalByRefObject
{
//you may specified any parameter you want, if you get `xxx is not marked as serializable` error, see explanation below
public string ExecuteAssembly(string param1)
{
/*
* All the code executed here is under the new app domain that we just created above
* We also have different session state here, so if you want data from main domain's session, you should pass it as a parameter
*/
//load your DLL file here
Debug.WriteLine(AppDomain.CurrentDomain.FriendlyName);
//will print "new:50536e71-51ad-4bad-9bf8-67c54382bb46" which is the name that we just gave to the new created app domain
Assembly asm = Assembly.LoadFrom(#"PATH/TO/THE/DLL");
Type baseClass = asm.GetType("MyAssembly.MyClass");
MethodInfo targetMethod = baseClass.GetMethod("MyMethod");
string result = targetMethod.Invoke(null, new object[]{});
return result;
}
}
A common error that you may run into
'xxx' is not marked as serializable
This could happen if you try to pass a custom class as parameter, like this
public void ExecuteAssembly(MyClass param1)
In this case, put a [Serializable] to MyClass, like this
[Serializable]
public class MyClass { }

.net reload assembly at runtime

I searched a lot about reloading an assembly at runtime in .NET. The only method I can find is using another AppDomain. But this makes things really complicated. And it is almost impossible in my case because the classes in the assembly which is going to be loaded at runtime do not inherit from MarshalByRefObject. I've looked at Unity game engine. The editor builds the components at runtime and just uses the compiled assembly. How is it possible?
I have done this using MEF. I am not sure if it is an option for you, but it works well. However even with MEF it is somewhat complicated.
On my case I am loading all the dll from a particular folder.
These are the setup classes.
public static class SandBox
{
public static AppDomain CreateSandboxDomain(string name, string path, SecurityZone zone)
{
string fullDirectory = Path.GetFullPath(path);
string cachePath = Path.Combine(fullDirectory, "ShadowCopyCache");
string pluginPath = Path.Combine(fullDirectory, "Plugins");
if (!Directory.Exists(cachePath))
Directory.CreateDirectory(cachePath);
if (!Directory.Exists(pluginPath))
Directory.CreateDirectory(pluginPath);
AppDomainSetup setup = new AppDomainSetup
{
ApplicationBase = fullDirectory,
CachePath = cachePath,
ShadowCopyDirectories = pluginPath,
ShadowCopyFiles = "true"
};
Evidence evidence = new Evidence();
evidence.AddHostEvidence(new Zone(zone));
PermissionSet permissions = SecurityManager.GetStandardSandbox(evidence);
return AppDomain.CreateDomain(name, evidence, setup, permissions);
}
}
public class Runner : MarshalByRefObject
{
private CompositionContainer _container;
private DirectoryCatalog _directoryCatalog;
private readonly AggregateCatalog _catalog = new AggregateCatalog();
public bool CanExport<T>()
{
T result = _container.GetExportedValueOrDefault<T>();
return result != null;
}
public void Recompose()
{
_directoryCatalog.Refresh();
_container.ComposeParts(_directoryCatalog.Parts);
}
public void RunAction(Action codeToExecute)
{
MefBase.Container = _container;
codeToExecute.Invoke();
}
public void CreateMefContainer()
{
RegistrationBuilder regBuilder = new RegistrationBuilder();
string pluginPath = AppDomain.CurrentDomain.SetupInformation.ApplicationBase;
_directoryCatalog = new DirectoryCatalog(pluginPath, regBuilder);
_catalog.Catalogs.Add(_directoryCatalog);
_container = new CompositionContainer(_catalog, true);
_container.ComposeExportedValue(_container);
Console.WriteLine("exports in AppDomain {0}", AppDomain.CurrentDomain.FriendlyName);
}
}
Here is the actual code.
AppDomain domain = SandBox.CreateSandboxDomain($"Sandbox Domain_{currentCount}", directoryName, SecurityZone.MyComputer);
foreach (FileInfo dll in currentDlls)
{
string path = Path.GetFullPath(Path.Combine(directoryName, dll.Name));
if (!File.Exists(path))
File.Copy(dll.FullName, Path.Combine(directoryName, dll.Name), true);
domain.Load(typeof(Runner).Assembly.FullName);
}
You can get the domain back by doing this.
Runner runner = (Runner) domain.CreateInstanceAndUnwrap(typeof(Runner).Assembly.FullName, typeof(Runner).FullName);
runner.CreateMefContainer(); // or runner.Recompose();
You will need to call your code like this.
runner.RunAction(() =>
{
IRepository export = MefBase.Resolve<IRepository>();
export?.Get("123");
Console.WriteLine("Executing {0}", export);
});
Generally speaking, you cannot reload assembly within the same AppDomain. You can create one dynamically and load it (and it will sit in your AppDomain forever), you can load another almost-but-not-quite-the-same copy of your assembly, but once the assembly is in AppDomain, it's stuck.
Imagine that a library assembly defines SomeType and your client code just created an instance. If you unload the library, what is supposed to happen with this instance? If the library is in another AppDomain, the client will use a proxy with a well-defined (in MarshalByRefObject) behaviour (to go zomby with the domain unload and throw exceptions foreverafter). Supporting unloading of arbitrary types would have made the runtime incredibly complicated, unpredictable or both.
As for Unity, see this discussion. Quote:
An "assembly reaload" sounds like some kind of quick update check but in fact the whole scripting environment reloads. This will destroy everything in the managed land. Unity can recover from this by using it's serialization system. Unity serializes the whole scene before the reload, then recreates everything and deserializing the whold scene. Of course only things which can be serialized will "survive" this process.

put together all assemblies and use it as embedded resource in exe

I have one assembly(MyAssembly.dll)(something like wrapper) which has a few references to other assemblies(3rdAssembly1.dll, 3rdAssembly2.dll). These referenced assemblies are embedded into my assembly as embedded resources. In my assembly I have main class(MyClass) which calls some functions from these third-party assemblies. This class has one default constructor where I try to resolve required dependencies as below:
public MyClass{
AppDomain.CurrentDomain.AssemblyResolve += new ResolveEventHandler(Resolver);
}
public static System.Reflection.Assembly Resolver(object sender, ResolveEventArgs args)
{
string source = "MyAssembly.Resources."+args.Name.Remove(args.Name.IndexOf(','))+".dll";
//source = "MyAssembly.Resources.3rdAssembly1.dll"
//source = "MyAssembly.Resources.3rdAssembly2.dll"
Assembly a1 = Assembly.GetExecutingAssembly();
Stream s = a1.GetManifestResourceStream(source);
byte[] block = new byte[s.Length];
s.Read(block, 0, block.Length);
Assembly a2 = Assembly.Load(block);
return a2;
}
Then I try to use my assembly(MyAssembly.dll) as embedded resource in my host app(MyApp.exe) where I also use Resolver function.
static void main(string[] args){
AppDomain.CurrentDomain.AssemblyResolve += new ResolveEventHandler(MainResolver);
MyClass my = new MyClass();
my.DoWork();
}
static System.Reflection.Assembly MainResolver(object sender, ResolveEventArgs args)
{
string source = "MyApp.Resources."+args.Name.Remove(args.Name.IndexOf(','))+".dll";
//source = "MyApp.Resources.MyAssembly.dll"
Assembly a1 = Assembly.GetExecutingAssembly();
Stream s = a1.GetManifestResourceStream(args.Name);
byte[] block = new byte[s.Length];
s.Read(block, 0, block.Length);
Assembly a2 = Assembly.Load(block);
return a2;
}
The problem is my host app doesn't resolve internal assemblies(3rdAssembly1.dll, 3rdAssembly2.dll), because Resolver function in MyClass is never called. The host app tries to resolve all of them and as a result fails. I played around with it and found out that if I exclude MyAssembly.dll from embedded resource and locate it in the same folder with host app, and replace += new ResolveEventHandler(MainResolver) on += new ResolveEventHandler(MyClass.Resolver) in main function then it works!
It is necessary to have one assembly, because I'm going to use it in several apps and I don't want to include all referenced assemblies in all apps every time.
So, my question is how to resolve all dependencies(MyAssembly.dll and all internal ones which are contained in MyAssembly.dll as embedded resources) in host app?
Thanks in advance!
I solved this issue. When the host app runs and doesn't find required assembly, it calls AssemblyResolve event. So, I have to use event handler called MainResolver to load MyAssembly.dll. Then before using methods of MyAssembly.dll, it's necessary to remove it from AssemblyResolve, because the app tries to resolve dependencies using ResolveEventHandler in order which they were added (It calls MainResolver and then Resolver). As a result the host app fails, because it can't find required assemblies in MainResolver. The solution is to reorder ResolveEventHandler or remove MainResolver from AssemblyResolve after it was called. I think that exclude useless handler is easier.
So, I don't need to change anything in MyClass. Everything I need is to add following code in host app before I create instance of MyClass.
static MyApp(){
AppDomain.CurrentDomain.AssemblyResolve += new ResolveEventHandler(MainResolver);
}
static void main(string[] args){
//remove MainResolver
AppDomain.CurrentDomain.AssemblyResolve -= MainResolver;
MyClass my = new MyClass();
my.DoWork();
}
Now it works like a charm!

PrivateBinPath outside ApplicationBase

My directory structure looks like this:
-- Host Program Base
|- HostProgram.exe
|- SharedLIB.dll
|-- LoadedLibs
|- HostedLib.dll
HostProgram.exe is attempting to load HostedLib.dll, which depends on SharedLib.dll.
Thus, SharedLib.dll's ApplicationBase for the AppDomain I am creating to load it is /Host Program Base/HostedLibs/, but it needs to be able to find SharedLib.dll.
I have tried to add .. to the PrivateBinPath for the AppDomain but according to MSDN,
Private assemblies are deployed in the same directory structure as the application. If the directories specified for PrivateBinPath are not under ApplicationBase, they are ignored.
As the PrivateBinPath is not inside the ApplicationBase, but rather is one directory up, it is not inside ApplicationBase and is being ignored. Therefore I get a AssemblyResolveException when attempting to load the DLL into the new AppDomain.
I have also attempted to set the ApplicationBase to the Host Program Base folder and add HostedLibs as a PrivateBinPath, but this causes the domain to be unable to resolve HostedLib.dll at all.
So -> how do I resolve libraries outside ApplicationBase using an AppDomainSetup?
Short of reorganizing your application structure, you could use the AppDomain.AssemblyResolve event.
Basically works like this.
Subscribe to the AssemblyResolve event on the AppDomain.
When the event fires, you can specifically look for your SharedLib.dll or simply attempt to create a full path to the desired assembly in your root folder given the assembly name specified in the ResolveEventArgs.Name and use Assembly.LoadFrom(path).
If the assembly successfully loaded from the path, return it in the AssemblyResolve handler, otherwise return null.
Implemented solution based on Jim's answer:
internal static class Program
{
static Program()
{
AppDomain.CurrentDomain.AssemblyResolve += CurrentDomainOnAssemblyResolve;
}
private static void Main()
{
//Do your stuff
}
private static Assembly CurrentDomainOnAssemblyResolve(object sender, ResolveEventArgs args)
{
try
{
AssemblyName name = new AssemblyName(args.Name);
string expectedFileName = name.Name + ".dll";
string rootDir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
return LoadAssembly(rootDir, expectedFileName, "", "Dlls", "../Dlls");
}
catch
{
return null;
}
}
private static Assembly LoadAssembly(string rootDir, string fileName, params string[] directories)
{
foreach (string directory in directories)
{
string path = Path.Combine(rootDir, directory, fileName);
if (File.Exists(path))
return Assembly.LoadFrom(path);
}
return null;
}
}

How to dynamically load references in parent assemblies in C#

How do I get a list of references in the parent assembly in C#. I'm thinking of a DLL that is loaded into another program, and the driver needs to use some of the parent assembly references in reflection and serialization. So far, I haven't tried anything as I'm not sure where to start.
It's pretty classic reflection issue, when you need to load an assembly and the assembly contains references, which are not referenced to the calling assembly.
Basically, you should load the assembly inside separate application domain.
e.g., you have a project ProxyDomain with a class ProxyType:
public class ProxyType : MarshalByRefObject
{
public void DoSomeStuff(string assemblyPath)
{
var someStuffAssembly = Assembly.LoadFrom(assemblyPath);
//Do whatever you need with the loaded assembly, e.g.:
var someStuffType = assembly.GetExportedTypes()
.First(t => t.Name == "SomeStuffType");
var someStuffObject = Activator.CreateInstance(someStuffType);
someStuffType.GetMethod("SomeStuffMethod").Invoke(someStuffObject, null);
}
}
And in your calling project, which contains a reference to ProxyDomain, you need to load the assembly, execute DoSomeStuff and unload the assembly resources:
public class SomeStuffCaller
{
public void CallDoSomeStuff(string assemblyPath)
{
AppDomainSetup setup = AppDomain.CurrentDomain.SetupInformation;
//Path to the directory, containing the assembly
setup.ApplicationBase = "...";
//List of directories where your private references are located
setup.PrivateBinPath = "...";
setup.ShadowCopyFiles = "true";
var reflectionDomain = AppDomain.CreateDomain("ProxyDomain", null, setup);
//You should specify the ProxyDomain assembly full name
//You can also utilize CreateInstanceFromAndUnwrap here:
var proxyType = (ProxyType)reflectionDomain.CreateInstanceAndUnwrap(
"ProxyDomain",
"ProxyType");
proxyType.DoSomeStuff(assemblyPath);
AppDomain.Unload(reflectionDomain);
}
}

Categories