Dynamically loaded Assembly not loading in new AppDomain
Asked Answered
E

1

0

This is not a duplicate - I have reviewed this related StackOverflow question with no luck: How to Load an Assembly to AppDomain with all references recursively?

I have two console applications. AssemblyLoaderTest.exe and testapp.exe

  1. I am trying to use AssemblyLoaderTest.exe to dynamically load testapp.exe and call a method from a class within testapp.exe
  2. So far the code works - the method "TestWrite()" in testapp.exe is executed correctly (and outputsuccess.txt is written), however, testapp.exe is loaded in the same AppDomain, which is proven because "CallMethodFromDllInNewAppDomain" always returns false. I am trying to load testapp.exe in a new AppDomain.

My question: how can I modify the below code so that testapp.exe is loaded in a new AppDomain, and as a result, "CallMethodFromDllInNewAppDomain" returns true? Thank you!

Code below. Both can be simply copied into new Console applications in VS and executed/compiled.

Console application #1:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Security.Policy;

namespace AssemblyLoaderTest
{
    class Program
    {
        static void Main(string[] args)
        {
            List<object> parameters = new List<object>();
            parameters.Add("Test from console app");
            bool loadedInNewAppDomain = DynamicAssemblyLoader.CallMethodFromDllInNewAppDomain(@"c:\temp\testapp.exe", "testapp.TestClass", "TestWrite", parameters);
        }
    }
    public static class DynamicAssemblyLoader
    {
        public static string ExeLoc = "";
        public static bool CallMethodFromDllInNewAppDomain(string exePath, string fullyQualifiedClassName, string methodName, List<object> parameters)
        {
            ExeLoc = exePath;
            List<Assembly> assembliesLoadedBefore = AppDomain.CurrentDomain.GetAssemblies().ToList<Assembly>();
            int assemblyCountBefore = assembliesLoadedBefore.Count;
            AppDomainSetup domaininfo = new AppDomainSetup();
            Evidence adevidence = AppDomain.CurrentDomain.Evidence;
            AppDomain domain = AppDomain.CreateDomain("testDomain", adevidence, domaininfo);
            AppDomain.CurrentDomain.AssemblyResolve += new ResolveEventHandler(CurrentDomain_AssemblyResolve);
            domain.CreateInstanceFromAndUnwrap(exePath, fullyQualifiedClassName);
            List<Assembly> assemblies = domain.GetAssemblies().ToList<Assembly>();
            string mainExeName = System.IO.Path.GetFileNameWithoutExtension(exePath);
            Assembly assembly = assemblies.FirstOrDefault(c => c.FullName.StartsWith(mainExeName));
            Type type2 = assembly.GetType(fullyQualifiedClassName);
            List<Type> parameterTypes = new List<Type>();
            foreach (var parameter in parameters)
            {
                parameterTypes.Add(parameter.GetType());
            }
            var methodInfo = type2.GetMethod(methodName, parameterTypes.ToArray());
            var testClass = Activator.CreateInstance(type2);
            object returnValue = methodInfo.Invoke(testClass, parameters.ToArray());
            List<Assembly> assembliesLoadedAfter = AppDomain.CurrentDomain.GetAssemblies().ToList<Assembly>();
            int assemblyCountAfter = assembliesLoadedAfter.Count;
            if (assemblyCountAfter > assemblyCountBefore)
            {
                //  Code always comes here
                return false;
            }
            else
            {
                // This would prove the assembly was loaded in a NEW domain.  Never gets here.
                return true;
            }
        }
        public static Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args)
        {
            // This is required I've found
            return System.Reflection.Assembly.LoadFrom(ExeLoc);
        }
    }
}

Console application #2:

using System;
namespace testapp
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Hello from console");
        }
    }
    [Serializable]
    public class TestClass : MarshalByRefObject
    {
        public void TestWrite(string message)
        {
            System.IO.File.WriteAllText(@"outputsuccess.txt", message);
        }
    }

}
Encephalo answered 25/3, 2014 at 16:48 Comment(0)
D
4

Use this class. Here are some notes:

  1. This class explicitly sets the current directory of the process and the app base path of the isolated app domain. This isn't entirely necessary, but it will make your life a whole lot easier.

    1. If you don't set the app base path to the directory containing the secondary assembly, then the runtime will attempt to resolve any dependencies of the secondary assembly against the same app base path as the primary assembly, which probably doesn't have the secondary assembly's dependencies. You could use the AssemblyResolve event to manually resolve the dependencies correctly, but settings the app base path is a much simpler and less error-prone way to do this.

    2. If you don't set Environment.CurrentDirectory, then file operations such as File.WriteAllText("myfile.txt", "blah") will resolve paths against the current directory, which is probably not what the secondary assembly's author intended. (ASIDE: Always resolve paths manually for this reason.)

  2. I believe simple reflection operations like GetMethod won't work on a MarshalByRefObject proxy such as returned by CreateInstanceFromAndUnwrap. So you need to do a little more to invoke.

    1. If you are the owner of both the primary and secondary assemblies, you could create an interface for the invocation -- put the interface in a shared assembly, define the cross-domain call in the interface, implement the interface in the target class, do a domain.CreateInstanceFromAndUnwrap on the target type and cast the result as the interface, which you can then use to call across the domain boundary.

    2. The solution below provides an alternative means that is less invasive -- you don't have to own the secondary assembly for this technique to work. The idea is that the primary domain creates a well-known intermediary object (InvokerHelper) in the secondary domain, and that intermediary performs the necessary reflection from inside the secondary domain.

Here's a complete implementation:

// Provides a means of invoking an assembly in an isolated appdomain
public static class IsolatedInvoker
{
    // main Invoke method
    public static void Invoke(string assemblyFile, string typeName, string methodName, object[] parameters)
    {
        // resolve path
        assemblyFile = Path.Combine(Environment.CurrentDirectory, assemblyFile);
        Debug.Assert(assemblyFile != null);

        // get base path
        var appBasePath = Path.GetDirectoryName(assemblyFile);
        Debug.Assert(appBasePath != null);

        // change current directory
        var oldDirectory = Environment.CurrentDirectory;
        Environment.CurrentDirectory = appBasePath;
        try
        {
            // create new app domain
            var domain = AppDomain.CreateDomain(Guid.NewGuid().ToString(), null, appBasePath, null, false);
            try
            {
                // create instance
                var invoker = (InvokerHelper) domain.CreateInstanceFromAndUnwrap(Assembly.GetExecutingAssembly().Location, typeof(InvokerHelper).FullName);

                // invoke method
                var result = invoker.InvokeHelper(assemblyFile, typeName, methodName, parameters);

                // process result
                Debug.WriteLine(result);
            }
            finally
            {
                // unload app domain
                AppDomain.Unload(domain);
            }
        }
        finally
        {
            // revert current directory
            Environment.CurrentDirectory = oldDirectory;
        }
    }

    // This helper class is instantiated in an isolated app domain
    private class InvokerHelper : MarshalByRefObject
    {
        // This helper function is executed in an isolated app domain
        public object InvokeHelper(string assemblyFile, string typeName, string methodName, object[] parameters)
        {
            // create an instance of the target object
            var handle = Activator.CreateInstanceFrom(assemblyFile, typeName);

            // get the instance of the target object
            var instance = handle.Unwrap();

            // get the type of the target object
            var type = instance.GetType();

            // invoke the method
            var result = type.InvokeMember(methodName, BindingFlags.InvokeMethod | BindingFlags.Public | BindingFlags.Instance, null, instance, parameters);

            // success
            return result;
        }
    }
}
Dissension answered 25/3, 2014 at 17:24 Comment(5)
Michael, thanks for your comment. (2) will not be possible for my use case (since this doesn't allow me to call a class/method by name). (3) - I added domaininfo.ApplicationBase = System.IO.Path.GetDirectoryName(exePath);, but this actually doesn't do anything at all (outputsuccess.txt is still written to the parent AppDomain's current directory).Encephalo
Would be very grateful if you can edit my existing code and make it work. There is a lot of discussion about theory on this topic, but still have not found a working code example. Thank you!Encephalo
I just wiped out and recreated my post entirely. This should answer your question.Dissension
Michael - reviewing this again today, I'm concerned that this is not thread-safe, since it changes the "Environment.CurrentDirectory" (which could in turn affect other threads calling this method). Is there a way to re-write this to ensure it's thread-safe?Encephalo
Just remove the Environment.CurrentDirectory stuff. It's not necessary, as described in the post. File operations like File.ReadAllText("blah.txt") always resolve against Environment.Currentdirectory. No way to change that. If the secondary assembly author expects those files to resolve relative to the assembly's directory, the second assembly will fail.Dissension

© 2022 - 2024 — McMap. All rights reserved.