PrivateBinPath outside ApplicationBase - c#

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;
}
}

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 { }

ResolveEventHandler not invoked when object is created in new AppDomain

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.

.NET add directory for dependencies at runtime

From my main C# application i instantiate another application via reflection.
assembly.CreateInstance( ... )
however this other assembly relies on DLLs which are in another directory than the executing assembly. how can i add this directory to the lookup path?
Here is how we implement this need in NDepend.PowerTools. These are a set of tools based on NDepend.API. The DLL NDepend.API.dll is in the directory .\Lib while the NDepend.PowerTools.exe assembly is inn the directory .\.
The NDepend.PowerTools.exe Main() method looks like:
[STAThread]
static void Main() {
AppDomain.CurrentDomain.AssemblyResolve += AssemblyResolverHelper.AssemblyResolveHandler;
MainSub();
}
// MainSub() is here to avoids that the Main() method uses something
// from NDepend.API without having registered AssemblyResolveHandler
[MethodImpl(MethodImplOptions.NoInlining)]
static void MainSub() {
...
And the AssemblyResolverHelper class is:
using System;
using System.Diagnostics;
using System.Reflection;
namespace NDepend.PowerTools {
internal static class AssemblyResolverHelper {
internal static Assembly AssemblyResolveHandler(object sender, ResolveEventArgs args) {
var assemblyName = new AssemblyName(args.Name);
Debug.Assert(assemblyName != null);
var assemblyNameString = assemblyName.Name;
Debug.Assert(assemblyNameString != null);
// Special treatment for NDepend.API and NDepend.Core because they are defined in $NDependInstallDir$\Lib
if (assemblyNameString != "NDepend.API" &&
assemblyNameString != "NDepend.Core") {
return null;
}
string binPath =
System.IO.Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location) +
System.IO.Path.DirectorySeparatorChar +
"Lib" +
System.IO.Path.DirectorySeparatorChar;
const string extension = ".dll";
var assembly = Assembly.LoadFrom(binPath + assemblyNameString + extension);
return assembly;
}
}
}
This works within a single AppDomain. I wouldn't use an extra AppDomain here if not needed. AppDomain is a pretty costly facility (in terms of performance) + the thread that jumps the AppDomains boundaries, has to serialize/unserialize in/out data to feed your assembly code, and this can be a headache.
The only advantage of AppDomain is that it lets unload loaded assemblies. So if you expect load/unload assemblies on a regular basis within the life of your main AppDomain, using some extra temporary AppDomain is the way to go.

Create custom AppDomain and add assemblies to it

How can I create an appdomain, add assemblies to it, then destroy that app domain? This is what I have tried:
static void Main(string[] args)
{
string pathToExe = #"A:\Users\Tono\Desktop\ConsoleApplication1.exe";
AppDomain myDomain = AppDomain.CreateDomain("MyDomain");
Assembly a = Assembly.Load(System.IO.File.ReadAllBytes(pathToExe));
myDomain.Load(a.FullName); // Crashes here!
}
I have also tried:
myDomain.Load(File.ReadAllBytes(pathToExe));
how can I add an assembly to the appdomain. Once I do that I can find the method via reflection execute it and then destroy the appdomain
The exception that I get is:
Could not load file or assembly 'ConsoleApplication1, Version=1.0.0.0,
Culture=neutral, PublicKeyToken=null' or one of its dependencies. The
system cannot find the file specified.
Two quick points:
AppDomain.Load loads an assembly in the current AppDomain and not on myDomain (weird, I know).
AppDomain.Load loads an assembly in the Load context, which resolves assemblies from the apps base-dir, the private-bin-paths and the GAC. Most probably, the assembly you try to load is not located in any of these locations which explains the exception message.
For more info have a look at this answer.
One way to load assemblies to an AppDomain is by creating a MarshalByRef-derived loader. You need something like this to avoid leaking types (and assemblies) to your main AppDomain. Here's a simple one:
public class SimpleAssemblyLoader : MarshalByRefObject
{
public void Load(string path)
{
ValidatePath(path);
Assembly.Load(path);
}
public void LoadFrom(string path)
{
ValidatePath(path);
Assembly.LoadFrom(path);
}
private void ValidatePath(string path)
{
if (path == null) throw new ArgumentNullException("path");
if (!System.IO.File.Exists(path))
throw new ArgumentException(String.Format("path \"{0}\" does not exist", path));
}
}
And use it like that:
//Create the loader (a proxy).
var assemblyLoader = (SimpleAssemblyLoader)myDomain.CreateInstanceAndUnwrap(typeof(SimpleAssemblyLoader).Assembly.FullName, typeof(SimpleAssemblyLoader).FullName);
//Load an assembly in the LoadFrom context. Note that the Load context will
//not work unless you correctly set the AppDomain base-dir and private-bin-paths.
assemblyLoader.LoadFrom(pathToExe);
//Do whatever you want to do.
//Finally unload the AppDomain.
AppDomain.Unload(myDomain);

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