AppDomain Assembly not found when loaded from byte array
Asked Answered
M

3

6

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?

Misprint answered 2/5, 2018 at 5:58 Comment(4)
Have you tried to hook the AppDomain's AssemblyResolve event msdn.microsoft.com/fr-fr/library/… and give back what it asks for "manually"?Lombardy
@SimonMourier Yes, i've tried that like described here: https://mcmap.net/q/833406/-need-to-hookup-assemblyresolve-event-when-disallowapplicationbaseprobing-true. Still FileNotFound exception is thrown.Misprint
Do you answer a non null assembly to every call made to AssemblyResolve? Sometimes, you need to hook both the starting appdomain (the one that creates the new AppDomain), and the new AppDomainLombardy
@SimonMourier Yes, I returned Assembly.Load(_myBytes) in both Appdomains when I tried that approach.Misprint
I
9

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.

Infrangible answered 4/5, 2018 at 19:35 Comment(1)
Interesting post, though I would rephrase the first paragraphs a bit (ref my answer).Genagenappe
G
3

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.

Genagenappe answered 5/5, 2018 at 18:29 Comment(2)
Interesting answer, and thanks for correcting me on what I got wrong. It's important to note here that all the code above is running in the primary app domain, and this only works as described because you've actually created an ObjectHandle via your call to CreateInstance. You can't actually run any methods on the instantiated type because it doesn't inherit from MarshalByRefObject. (ie, if you try to unwrap it you'll get an error saying your plugin is not serializable)Infrangible
@Infrangible That's right, and besides that, Assembly.Load() should really be called from the concerned AppDomain (as with the proxy).Genagenappe
S
0

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");

Suffrage answered 2/5, 2018 at 18:15 Comment(2)
Just tried it. Still FileNotFoundException gets thrown.Misprint
This should have been a commentMarvelous

© 2022 - 2024 — McMap. All rights reserved.