4
votes

I am building a WPF tool which will analyze via reflection the assemblies of a target application.

I have so far been using Assembly.Load etc. to load the target assemblies. This is OK except that it has a couple of limitations: I want to be able to rebuild the target application and "refresh" the tool to re-analyze the newly-built assemblies. This won't currently work because the assemblies are locked upon load and not released until the tool exits. This also precludes re-loading newly built assemblies.

I believe I can create a temporary AppDomain, load the assemblies into it, do the reflection I want to do, and then unload the domain.

The problem I am having is that I can't get it to work. I've tried numerous variations and get results such as:

  • Loading into the current application domain, not the one I explicitly created
  • Error loading the requested assembly
  • Error loading the tool's assembly(?)

For example, following the suggestion here: Create custom AppDomain and add assemblies to it

I created a SimpleAssemblyLoader thus:

public class SimpleAssemblyLoader : MarshalByRefObject
{
    public Assembly Load(string path)
    {
        ValidatePath(path);

        return Assembly.Load(path);
    }

    public Assembly LoadFrom(string path)
    {
        ValidatePath(path);

        return Assembly.LoadFrom(path);
    }

    public Assembly UnsafeLoadFrom(string path)
    {
        ValidatePath(path);

        return Assembly.UnsafeLoadFrom(path);
    }

    private void ValidatePath(string path)
    {
        if (path == null) throw new ArgumentNullException(nameof(path));
        if (!System.IO.File.Exists(path))
            throw new ArgumentException($"path \"{path}\" does not exist");
    }
}

... and use it in a calling method thus:

    private static AppDomain MakeDomain(string name, string targetPath, string toolPath)
    {
        var appDomain =
            AppDomain.CreateDomain(name, AppDomain.CurrentDomain.Evidence, new AppDomainSetup
                {
                    ApplicationBase = targetPath,
                    PrivateBinPath = toolPath,
                    LoaderOptimization = LoaderOptimization.MultiDomainHost
                },
                new PermissionSet(PermissionState.Unrestricted));
        return appDomain;
    }

    /// <summary>
    /// 
    /// </summary>
    /// <param name="targetPath">Location of assemblies to analyze</param>
    /// <param name="toolPath">Location of this tool</param>
    /// <param name="file">Filename of assembly to analyze</param>
    /// <returns></returns>
    public string[] Test(string targetPath, string toolPath, string file)
    {
        var dom = MakeDomain("TestDomain", targetPath, toolPath);
        var assemblyLoader = (SimpleAssemblyLoader)dom.CreateInstanceAndUnwrap(typeof(SimpleAssemblyLoader).Assembly.FullName, typeof(SimpleAssemblyLoader).FullName);

        var path = Path.Combine(targetPath, file);

        var assembly = assemblyLoader.LoadFrom(path);

        var types = assembly.GetTypes();

        List<string> methods = new List<string>();

        foreach (var type in types)
        {
            foreach (var method in type.GetMethods(BindingFlags.Instance|BindingFlags.Public))
            {
                methods.Add(method.Name);
            }
        }

        AppDomain.Unload(dom);

        return methods.ToArray();
    }

... the tool application launches, but it fails to instantiate the SimpleAssemblyLoader, reporting a "File not found" exception associated with the tool's assembly - apparently trying to load the tool's assembly into the new domain(?).

What am I doing wrong, and how do I fix it?

1
What is toolPath? Path to directory with tool?Evk
@Evk Yes. Unclear to me if I need to use PrivateBinPath or if I am misusing it. I've edited sample code in response to your question.C Robinson

1 Answers

5
votes

There are several problems here. First, as stated in documentation, PrivateBinPath should be relative to ApplicationBase - otherwise it's ignored. This is what happens in your case. Since it is ignored and ApplicationBase refers to directory with target application and not tool directory - app domain has no idea from where to load your SimpleAssemblyLoader. To fix this, just use toolPath as ApplicationBase:

private static AppDomain MakeDomain(string name, string toolPath)
{
    var appDomain =
        AppDomain.CreateDomain(name, AppDomain.CurrentDomain.Evidence, new AppDomainSetup
            {
                ApplicationBase = toolPath,                        
                LoaderOptimization = LoaderOptimization.MultiDomainHost
            },
            new PermissionSet(PermissionState.Unrestricted));
    return appDomain;
}

Or just

private static AppDomain MakeDomain(string name) {
    var appDomain =
        AppDomain.CreateDomain(name, AppDomain.CurrentDomain.Evidence, new AppDomainSetup {
                ApplicationBase = AppDomain.CurrentDomain.BaseDirectory,
                LoaderOptimization = LoaderOptimization.MultiDomainHost
            },
            new PermissionSet(PermissionState.Unrestricted));
    return appDomain;
}

Second problem

public Assembly LoadFrom(string path) {
    ValidatePath(path);
    return Assembly.LoadFrom(path);
}

When you call

var assembly = assemblyLoader.LoadFrom(path);

Assembly is loaded in child app domain BUT when you return this assembly as a result (to var assembly) - current app domain will load it too (well - will try to load and fail, because it has no idea where this assembly is located). You should not return assembly like this, because it will either get loaded in both current and child domains (bad) or just fail to load in current domain. Instead - put your whole method into MarshalByRefObject to execute it completely in child domain and return results:

public class WpfInspector : MarshalByRefObject {
    public string[] Inspect(string path) {
        ValidatePath(path);
        var assembly = Assembly.LoadFrom(path);

        var types = assembly.GetTypes();

        List<string> methods = new List<string>();

        foreach (var type in types) {
            foreach (var method in type.GetMethods(BindingFlags.Instance | BindingFlags.Public)) {
                methods.Add(method.Name);
            }
        }
        return methods.ToArray();
    }

    private void ValidatePath(string path) {
        if (path == null) throw new ArgumentNullException(nameof(path));
        if (!System.IO.File.Exists(path))
            throw new ArgumentException($"path \"{path}\" does not exist");
    }
}

Then

var dom = MakeDomain("TestDomain", toolPath);            
var inspector = (WpfInspector)dom.CreateInstanceAndUnwrap(typeof(WpfInspector).Assembly.FullName, typeof(WpfInspector).FullName);

var path = Path.Combine(targetPath, file);
var methods = inspector.Inspect(path);
AppDomain.Unload(dom);