How to Load an Assembly to AppDomain with all references recursively?
Asked Answered
L

8

118

I want to load to a new AppDomain some assembly which has a complex references tree (MyDll.dll -> Microsoft.Office.Interop.Excel.dll -> Microsoft.Vbe.Interop.dll -> Office.dll -> stdole.dll)

As far as I understood, when an assembly is being loaded to AppDomain, its references would not be loaded automatically, and I have to load them manually. So when I do:

string dir = @"SomePath"; // different from AppDomain.CurrentDomain.BaseDirectory
string path = System.IO.Path.Combine(dir, "MyDll.dll");

AppDomainSetup setup = AppDomain.CurrentDomain.SetupInformation;
setup.ApplicationBase = dir;
AppDomain domain = AppDomain.CreateDomain("SomeAppDomain", null, setup);

domain.Load(AssemblyName.GetAssemblyName(path));

and got FileNotFoundException:

Could not load file or assembly 'MyDll, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null' or one of its dependencies. The system cannot find the file specified.

I think the key part is one of its dependencies.

Ok, I do next before domain.Load(AssemblyName.GetAssemblyName(path));

foreach (AssemblyName refAsmName in Assembly.ReflectionOnlyLoadFrom(path).GetReferencedAssemblies())
{
    domain.Load(refAsmName);
}

But got FileNotFoundException again, on another (referenced) assembly.

How to load all references recursively?

Do I have to create references tree before loading root assembly? How to get an assembly's references without loading it?

Leontineleontyne answered 18/3, 2009 at 14:30 Comment(1)
I've loaded assemblies like this many times before, I've never had to manually load all it's references. I'm not sure the premise of this question is correct.Fireproofing
G
73

You need to invoke CreateInstanceAndUnwrap before your proxy object will execute in the foreign application domain.

 class Program
{
    static void Main(string[] args)
    {
        AppDomainSetup domaininfo = new AppDomainSetup();
        domaininfo.ApplicationBase = System.Environment.CurrentDirectory;
        Evidence adevidence = AppDomain.CurrentDomain.Evidence;
        AppDomain domain = AppDomain.CreateDomain("MyDomain", adevidence, domaininfo);

        Type type = typeof(Proxy);
        var value = (Proxy)domain.CreateInstanceAndUnwrap(
            type.Assembly.FullName,
            type.FullName);

        var assembly = value.GetAssembly(args[0]);
        // AppDomain.Unload(domain);
    }
}

public class Proxy : MarshalByRefObject
{
    public Assembly GetAssembly(string assemblyPath)
    {
        try
        {
            return Assembly.LoadFile(assemblyPath);
        }
        catch (Exception)
        {
            return null;
            // throw new InvalidOperationException(ex);
        }
    }
}

Also, note that if you use LoadFrom you'll likely get a FileNotFound exception because the Assembly resolver will attempt to find the assembly you're loading in the GAC or the current application's bin folder. Use LoadFile to load an arbitrary assembly file instead--but note that if you do this you'll need to load any dependencies yourself.

Garmaise answered 13/11, 2012 at 4:43 Comment(7)
Check out the code I wrote to solve this problem: github.com/jduv/AppDomainToolkit. Specifically, look at the LoadAssemblyWithReferences method in this class: github.com/jduv/AppDomainToolkit/blob/master/AppDomainToolkit/…Garmaise
I've found that although this works most of the time, in some cases you actually still need to attach a handler to the AppDomain.CurrentDomain.AssemblyResolve event as described in this MSDN answer. In my case, I was trying to hook into the SpecRun deployment running under MSTest, but I think it applies to many situations in which your code might not run from the "primary" AppDomain - VS extensions, MSTest, etc.Mummy
Ah interesting. I'll look into that and see if I can make that slightly easier to work with via ADT. Sorry that code's been slightly dead for a while now--we all have day jobs :).Garmaise
@Garmaise Would upvote your comment about 100 times if I could. Your library helped me solve a seemingly unsolvable problem I was having with dynamic assembly loading under MSBuild. You should promote it to an answer!Dissonancy
@Garmaise are you sure that assembly variable will reference assembly from "MyDomain"? I think by var assembly = value.GetAssembly(args[0]); you will load your args[0] into both domains and assembly variable will reference copy from the main application domainChenay
@Jduv, I got error and the message I got is "Unable to cast transparent proxy to type "Proxy" when I applied the code in VSIX command handler. Could you let me know why I can not get same result as you have. Thanks in advance.Youmans
@IgorBendrup you are right assembly = value.GetAssembly(args[0]) will make sure type where the return type assembly exits in both domainBildungsroman
Y
15

Once you pass the assembly instance back to the caller domain, the caller domain will try to load it! This is why you get the exception. This happens in your last line of code:

domain.Load(AssemblyName.GetAssemblyName(path));

Thus, whatever you want to do with the assembly, should be done in a proxy class - a class which inherit MarshalByRefObject.

Take in count that the caller domain and the new created domain should both have access to the proxy class assembly. If your issue is not too complicated, consider leaving the ApplicationBase folder unchanged, so it will be same as the caller domain folder (the new domain will only load Assemblies it needs).

In simple code:

public void DoStuffInOtherDomain()
{
    const string assemblyPath = @"[AsmPath]";
    var newDomain = AppDomain.CreateDomain("newDomain");
    var asmLoaderProxy = (ProxyDomain)newDomain.CreateInstanceAndUnwrap(Assembly.GetExecutingAssembly().FullName, typeof(ProxyDomain).FullName);

    asmLoaderProxy.GetAssembly(assemblyPath);
}

class ProxyDomain : MarshalByRefObject
{
    public void GetAssembly(string AssemblyPath)
    {
        try
        {
            Assembly.LoadFrom(AssemblyPath);
            //If you want to do anything further to that assembly, you need to do it here.
        }
        catch (Exception ex)
        {
            throw new InvalidOperationException(ex.Message, ex);
        }
    }
}

If you do need to load the assemblies from a folder which is different than you current app domain folder, create the new app domain with specific dlls search path folder.

For example, the app domain creation line from the above code should be replaced with:

var dllsSearchPath = @"[dlls search path for new app domain]";
AppDomain newDomain = AppDomain.CreateDomain("newDomain", new Evidence(), dllsSearchPath, "", true);

This way, all the dlls will automaically be resolved from dllsSearchPath.

Yearround answered 5/12, 2013 at 13:30 Comment(2)
Why do I have to load the assembly by using a proxy class? What is the difference compared to loading it using Assembly.LoadFrom(string). I am interested in the technical details, from the CLR's perspective. I would be very grateful if you could provide an answer.Lockyer
You use the proxy class in order to avoid the new assembly from being loaded in to your caller domain. If you'll use Assembly.LoadFrom(string), the caller domain will try to load the new assembly references and won't find them because it doesn't search for assemblies in the "[AsmPath]". (msdn.microsoft.com/en-us/library/yx7xezcf%28v=vs.110%29.aspx)Yearround
H
12

http://support.microsoft.com/kb/837908/en-us

C# version:

Create a moderator class and inherit it from MarshalByRefObject:

class ProxyDomain : MarshalByRefObject
{
    public Assembly GetAssembly(string assemblyPath)
    {
        try
        {
            return Assembly.LoadFrom(assemblyPath);
        }
        catch (Exception ex)
        {
            throw new InvalidOperationException(ex.Message);
        }
    }
}

call from client site

ProxyDomain pd = new ProxyDomain();
Assembly assembly = pd.GetAssembly(assemblyFilePath);
Haematosis answered 17/6, 2010 at 6:10 Comment(7)
How is this solution put into context of creating a new AppDomain, can someone explain?Stanton
A MarshalByRefObject can be passed around appdomains. So I would guess that Assembly.LoadFrom tries to load the assembly in a new appdomain, what is only possible, if the calling object could be passed between those appdomains. This is also called remoting as described here: msdn.microsoft.com/en-us/library/…Frisian
Nice solution I am going to put link to it here. #12427823Recidivate
This doesn't work. If you execute the code and check the AppDomain.CurrentDomain.GetAssemblies() you'll see that the target assembly you're attempting to load is loaded into the current application domain and not the proxy one.Garmaise
This is complete nonsense. Inheriting from MarshalByRefObject doesn't magically make it load in every other AppDomain, it just tells the .NET framework to create a transparent remoting proxy instead of using serialization when you unwrap the reference from one AppDomain in another AppDomain (the typical way being the CreateInstanceAndUnwrap method). Can't believe this answer has over 30 upvotes; the code here just a pointlessly roundabout way of calling Assembly.LoadFrom.Mummy
Yes it looks like complete nonsense, yet it has 28 up votes and is marked as the answer. The link supplied doesn't even mention MarshalByRefObject. Quite bizarre. If this actually does anything I'd love someone to explain howFireproofing
It works for me, I could load assembly from different folder and it works, before I use Load method it failed. if someone can explain how it exactly work and when we should not use this approach will be great.Brickwork
U
11

On your new AppDomain, try setting an AssemblyResolve event handler. That event gets called when a dependency is missing.

Uranous answered 18/3, 2009 at 14:55 Comment(2)
It doesn't. Actually, you get an exception on the line you're registering this event on the new AppDomain. You've to register this event on the current AppDomain.Dialectical
It does if class is inherited from MarshalByRefObject. It does not if class is marked only with [Serializable] attribute.Justiceship
H
5

You need to handle the AppDomain.AssemblyResolve or AppDomain.ReflectionOnlyAssemblyResolve events (depending on which load you're doing) in case the referenced assembly is not in the GAC or on the CLR's probing path.

AppDomain.AssemblyResolve

AppDomain.ReflectionOnlyAssemblyResolve

Howard answered 18/3, 2009 at 14:52 Comment(1)
So I have to indicate requested assembly manually? Even it is in new AppDomain's AppBase ? Is there a way not to do that?Leontineleontyne
E
5

The Key is the AssemblyResolve event raised by the AppDomain.

[STAThread]
static void Main(string[] args)
{
    fileDialog.ShowDialog();
    string fileName = fileDialog.FileName;
    if (string.IsNullOrEmpty(fileName) == false)
    {
        AppDomain.CurrentDomain.AssemblyResolve += CurrentDomain_AssemblyResolve;
        if (Directory.Exists(@"c:\Provisioning\") == false)
            Directory.CreateDirectory(@"c:\Provisioning\");

        assemblyDirectory = Path.GetDirectoryName(fileName);
        Assembly loadedAssembly = Assembly.LoadFile(fileName);

        List<Type> assemblyTypes = loadedAssembly.GetTypes().ToList<Type>();

        foreach (var type in assemblyTypes)
        {
            if (type.IsInterface == false)
            {
                StreamWriter jsonFile = File.CreateText(string.Format(@"c:\Provisioning\{0}.json", type.Name));
                JavaScriptSerializer serializer = new JavaScriptSerializer();
                jsonFile.WriteLine(serializer.Serialize(Activator.CreateInstance(type)));
                jsonFile.Close();
            }
        }
    }
}

static Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args)
{
    string[] tokens = args.Name.Split(",".ToCharArray());
    System.Diagnostics.Debug.WriteLine("Resolving : " + args.Name);
    return Assembly.LoadFile(Path.Combine(new string[]{assemblyDirectory,tokens[0]+ ".dll"}));
}
Execration answered 5/3, 2013 at 16:52 Comment(0)
P
5

It took me a while to understand @user1996230's answer so I decided to provide a more explicit example. In the below example I make a proxy for an object loaded in another AppDomain and call a method on that object from another domain.

class ProxyObject : MarshalByRefObject
{
    private Type _type;
    private Object _object;

    public void InstantiateObject(string AssemblyPath, string typeName, object[] args)
    {
        assembly = Assembly.LoadFrom(AppDomain.CurrentDomain.BaseDirectory + AssemblyPath); //LoadFrom loads dependent DLLs (assuming they are in the app domain's base directory
        _type = assembly.GetType(typeName);
        _object = Activator.CreateInstance(_type, args); ;
    }

    public void InvokeMethod(string methodName, object[] args)
    {
        var methodinfo = _type.GetMethod(methodName);
        methodinfo.Invoke(_object, args);
    }
}

static void Main(string[] args)
{
    AppDomainSetup setup = new AppDomainSetup();
    setup.ApplicationBase = @"SomePathWithDLLs";
    AppDomain domain = AppDomain.CreateDomain("MyDomain", null, setup);
    ProxyObject proxyObject = (ProxyObject)domain.CreateInstanceFromAndUnwrap(typeof(ProxyObject).Assembly.Location,"ProxyObject");
    proxyObject.InstantiateObject("SomeDLL","SomeType", new object[] { "someArgs});
    proxyObject.InvokeMethod("foo",new object[] { "bar"});
}
Preceptive answered 20/3, 2014 at 4:24 Comment(1)
Some small typos in the code, and I have to admit I didn't believe it would work, but this was a life saver for me. Thanks a ton.Isoagglutination
C
2

I have had to do this several times and have researched many different solutions.

The solution I find in most elegant and easy to accomplish can be implemented as such.

1. Create a project that you can create a simple interface

the interface will contain signatures of any members you wish to call.

public interface IExampleProxy
{
    string HelloWorld( string name );
}

Its important to keep this project clean and lite. It is a project that both AppDomain's can reference and will allow us to not reference the Assembly we wish to load in seprate domain from our client assembly.

2. Now create project that has the code you want to load in seperate AppDomain.

This project as with the client proj will reference the proxy proj and you will implement the interface.

public interface Example : MarshalByRefObject, IExampleProxy
{
    public string HelloWorld( string name )
    {
        return $"Hello '{ name }'";
    }
}

3. Next, in the client project, load code in another AppDomain.

So, now we create a new AppDomain. Can specify the base location for assembly references. Probing will check for dependent assemblies in GAC and in current directory and the AppDomain base loc.

// set up domain and create
AppDomainSetup domaininfo = new AppDomainSetup
{
    ApplicationBase = System.Environment.CurrentDirectory
};

Evidence adevidence = AppDomain.CurrentDomain.Evidence;

AppDomain exampleDomain = AppDomain.CreateDomain("Example", adevidence, domaininfo);

// assembly ant data names
var assemblyName = "<AssemblyName>, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null|<keyIfSigned>";
var exampleTypeName = "Example";

// Optional - get a reflection only assembly type reference
var @type = Assembly.ReflectionOnlyLoad( assemblyName ).GetType( exampleTypeName ); 

// create a instance of the `Example` and assign to proxy type variable
IExampleProxy proxy= ( IExampleProxy )exampleDomain.CreateInstanceAndUnwrap( assemblyName, exampleTypeName );

// Optional - if you got a type ref
IExampleProxy proxy= ( IExampleProxy )exampleDomain.CreateInstanceAndUnwrap( @type.Assembly.Name, @type.Name );    

// call any members you wish
var stringFromOtherAd = proxy.HelloWorld( "Tommy" );

// unload the `AppDomain`
AppDomain.Unload( exampleDomain );

if you need to, there are a ton of different ways to load an assembly. You can use a different way with this solution. If you have the assembly qualified name then I like to use the CreateInstanceAndUnwrap since it loads the assembly bytes and then instantiates your type for you and returns an object that you can simple cast to your proxy type or if you not that into strongly-typed code you could use the dynamic language runtime and assign the returned object to a dynamic typed variable then just call members on that directly.

There you have it.

This allows to load an assembly that your client proj doesnt have reference to in a seperate AppDomain and call members on it from client.

To test, I like to use the Modules window in Visual Studio. It will show you your client assembly domain and what all modules are loaded in that domain as well your new app domain and what assemblies or modules are loaded in that domain.

The key is to either make sure you code either derives MarshalByRefObject or is serializable.

`MarshalByRefObject will allow you to configure the lifetime of the domain its in. Example, say you want the domain to destroy if the proxy hasnt been called in 20 minutes.

I hope this helps.

Calkins answered 9/4, 2018 at 23:47 Comment(2)
Hi, if I remember correctly, the core issue was to how to load all dependencies recursively, hence the question. Please test your code by changing HelloWorld to return a class of type Foo, FooAssembly which has a property of type Bar, BarAssembly, i.e 3 assemblies total. Would it continue to work?Leontineleontyne
Yes, need proper directory enumerated in the assembly probing stage. AppDomain has a ApplicationBase , however, I did not test it. Also config files you can specify assembly probing directories such as a app.config which a dll can use as well just set to copy in properties. Also, if you have control over the building of the assembly wishing to load in separate app domain, references can get a HintPath that specify were to look for it. If all that failed I would result to subscribing to the new AppDomains AssemblyResolve event and manually load the assemblies.Tons of example for that.Calkins

© 2022 - 2024 — McMap. All rights reserved.