使用AppDomain临时加载程序集

4

我正在构建一个WPF工具,该工具将通过反射分析目标应用程序的程序集。

到目前为止,我一直在使用Assembly.Load等方法来加载目标程序集。这样做还可以,但它有几个限制:我希望能够重建目标应用程序并“刷新”工具以重新分析新构建的程序集。这目前不起作用,因为程序集在加载时被锁定,并且直到工具退出后才被释放。这也排除了重新加载新构建的程序集。

我认为我可以创建一个临时AppDomain,将程序集加载到其中,执行我想要的反射操作,然后卸载该域。

我的问题在于我无法让它正常工作。我尝试了很多变化,得到了如下结果:

  • 加载到当前应用程序域,而不是我明确创建的域中
  • 请求的程序集加载错误
  • 加载工具的程序集时出错(?)

例如,遵循此处的建议:Create custom AppDomain and add assemblies to it

我创建了一个SimpleAssemblyLoader,如下所示:

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

...并在调用方法中使用它:

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

当工具应用程序启动时,它无法实例化SimpleAssemblyLoader,并报告与工具的程序集相关的“文件未找到”异常 - 显然尝试将工具的程序集加载到新域中。我做错了什么?如何修复?

什么是toolPath?是指工具所在的目录路径吗? - Evk
@Evk 是的。我不确定我是否需要使用PrivateBinPath,或者我是否在错误地使用它。我已经根据你的问题编辑了示例代码。 - C Robinson
1个回答

5
这里有几个问题。首先,根据文档所述,PrivateBinPath 应该相对于 ApplicationBase,否则将被忽略。这就是你的情况。由于它被忽略了,而 ApplicationBase 引用目标应用程序的目录而不是工具目录,因此应用程序域不知道从哪里加载你的 SimpleAssemblyLoader。要解决此问题,只需将 toolPath 用作 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;
}

或者只需
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;
}

第二个问题

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

当你调用时,
var assembly = assemblyLoader.LoadFrom(path);

装配件被加载在子应用程序域中,但当你把这个装配件作为结果返回(到var assembly),当前应用程序域也会加载它(好吧 - 会尝试加载并失败,因为它不知道这个装配件在哪里)。你不应该像这样返回装配件,因为它将在当前和子域中都被加载(不好),或者在当前域中无法加载。相反,将整个方法放入MarshalByRefObject中,在子域中完全执行它并返回结果:

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

然后。
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);

谢谢您提供的内容,帮助我解决了问题。只有一个纠正:Assembly.LoadFrom 仍然在父域中加载。我必须使用 AppDomain.Load 方法,这需要一些其他的工作,但我已经在另一个 SO 线程中找到了如何做到这一点;Assembly.LoadFrom 是因为我尝试了许多不同的排列组合而出现在我的问题中。 - C Robinson
@CRobinson 如果它能工作,那就行了。但请注意,AppDomain.Load 不建议用于将任何内容加载到其他应用程序域中 - 只能用于主应用程序域。不确定您如何将其用于实现目标。而且,Assembly.LoadFrom 不应该将程序集加载到父应用程序域中,除非您返回该程序集或其中的任何类型。例如,在测试期间,我可以在卸载应用程序域后自由更改目标文件 - 目标程序集未加载到父应用程序域中,因此未被锁定。当然,我指的是在子域中执行的 Assembly.LoadFrom(来自 MarshalByRefObject)。 - Evk
这应该是我认为可接受的答案。谢谢你,你解决了我的问题! - ManselD

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接