如何将程序集及其所有依赖递归地加载到AppDomain中?

118
我想加载一个具有复杂引用树的程序集(MyDll.dll->Microsoft.Office.Interop.Excel.dll->Microsoft.Vbe.Interop.dll->Office.dll->stdole.dll)到一个新的AppDomain中。
据我所知,当一个程序集被加载到AppDomain时,它的引用不会自动加载,我必须手动加载它们。 所以当我执行以下操作时:
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));

并且收到了 FileNotFoundException 异常:

无法加载文件或程序集 'MyDll, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null' 或其一个依赖项。系统找不到指定的文件。

我认为关键部分是其一个依赖项

好的,在 domain.Load(AssemblyName.GetAssemblyName(path));之前,我会执行以下步骤。

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

但是我再次得到了FileNotFoundException,这次是在另一个(被引用的)程序集上。

如何递归加载所有引用?

我是否需要在加载根程序集之前创建引用树?如何在不加载程序集的情况下获取程序集的引用?


1
我之前很多次都加载了这样的程序集,从来没有手动加载过它的所有引用。我不确定这个问题的前提是否正确。 - Mick
8个回答

73

在您的代理对象能够在外部应用程序域中执行之前,您需要调用CreateInstanceAndUnwrap

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

还要注意,如果使用 LoadFrom,可能会得到一个 FileNotFound 异常,因为程序集解析器将尝试在 GAC 或当前应用程序的 bin 文件夹中查找正在加载的程序集。相反,使用 LoadFile 来加载任意程序集文件--但要注意,如果这样做,则需要自己加载所有依赖项。


21
请查看我编写的解决此问题的代码: https://github.com/jduv/AppDomainToolkit。具体来说,请查看该类中的LoadAssemblyWithReferences方法:https://github.com/jduv/AppDomainToolkit/blob/master/AppDomainToolkit/AppDomainContext.cs - Jduv
3
我发现虽然大多数情况下这样做是有效的,但在某些情况下,您实际上仍然需要像此MSDN答案中所述那样将处理程序附加到 AppDomain.CurrentDomain.AssemblyResolve 事件。在我的情况下,我试图钩入在MSTest下运行的SpecRun部署,但我认为它适用于许多情况,其中您的代码可能不会从“主” AppDomain运行 - VS扩展,MSTest等。 - Aaronaught
啊,有趣。我会研究一下,看看能否通过ADT使其更易于使用。很抱歉这段代码已经有一段时间没有更新了——我们都有白天的工作 :)。 - Jduv
@Jduv 如果可以的话,我会给你的评论点赞100次。你的库帮助我解决了在MSBuild下动态装配加载似乎无法解决的问题。你应该将它推广为一个答案! - Philip Daniels
2
@Jduv,你确定assembly变量将引用"MyDomain"中的程序集吗?我认为通过var assembly = value.GetAssembly(args[0]);,你会将args[0]加载到两个域中,并且assembly变量将引用主应用程序域中的副本。 - Igor Bendrup
显示剩余2条评论

15

一旦你将汇编实例返回给调用方域,调用方域就会尝试加载它!这就是你获取异常的原因。这发生在你的最后一行代码中:

domain.Load(AssemblyName.GetAssemblyName(path));
因此,无论您想对汇编做什么,都应该在代理类中完成 - 代理类继承自 MarshalByRefObject
请注意,调用者域和新创建的域都应该可以访问代理类程序集。如果您的问题不是太复杂,请考虑保持ApplicationBase文件夹不变,这样它将与调用者域文件夹相同(新域只会加载所需的程序集)。
简单代码示例:
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);
        }
    }
}

如果您需要从与当前应用程序域文件夹不同的文件夹加载程序集,请创建具有特定dll搜索路径文件夹的新应用程序域。

例如,上面代码中的应用程序域创建行应替换为:

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

这样,所有的dll文件都会自动从dllsSearchPath中解析。


为什么我必须使用代理类来加载程序集?与使用Assembly.LoadFrom(string)加载它相比,有什么区别?我对从CLR的角度看到的技术细节很感兴趣。如果您能提供答案,我将非常感激。 - Dennis Kassel
1
您可以使用代理类来避免新程序集被加载到调用方域中。如果您使用Assembly.LoadFrom(string),调用方域将尝试加载新的程序集引用,但不会在"[AsmPath]"中搜索程序集,因此找不到它们。(https://msdn.microsoft.com/zh-cn/library/yx7xezcf(v=vs.110).aspx) - Nir

12

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

C#版本:

创建一个Moderator类并继承自MarshalByRefObject

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

来自客户端的调用

ProxyDomain pd = new ProxyDomain();
Assembly assembly = pd.GetAssembly(assemblyFilePath);

6
这个解决方案在创建新的AppDomain的情境下是如何实施的?可以有人解释一下吗? - Tri Q Tran
2
一个MarshalByRefObject可以在应用程序域之间传递。因此,我猜测Assembly.LoadFrom尝试在新的应用程序域中加载程序集,这只有在调用对象可以在这些应用程序域之间传递时才可能。这也被称为远程处理,如此处所述:http://msdn.microsoft.com/en-us/library/system.marshalbyrefobject%28v=vs.80%29.aspx - Christoph Meißner
33
不起作用。如果您执行代码并检查AppDomain.CurrentDomain.GetAssemblies(),您会发现您试图加载的目标程序集已加载到当前应用程序域而不是代理域。 - Jduv
43
这完全是胡说八道。从“MarshalByRefObject”继承并不能让它神奇地在每个其他“AppDomain”中加载,只是告诉.NET框架在另一个“AppDomain”中解包引用时创建透明远程代理而不是使用序列化(通常的方式是使用“CreateInstanceAndUnwrap”方法)。难以相信这个答案有超过30个赞;这里的代码只是无意义地绕了一个弯去调用“Assembly.LoadFrom”。 - Aaronaught
1
是的,它看起来完全是胡言乱语,但它有28个赞和被标记为答案。提供的链接甚至没有提到MarshalByRefObject。相当奇怪。如果这确实有用,请有人解释一下如何操作。 - Mick
显示剩余2条评论

11
在您的新AppDomain上,尝试设置AssemblyResolve事件处理程序。当依赖项缺失时,该事件将被调用。

实际上不是这样的。在你注册这个事件的新AppDomain上,会出现一个异常。你必须在当前的AppDomain上注册这个事件。 - user1004959
只有当类继承自MarshalByRefObject时,它才会执行此操作。如果类仅标记有[Serializable]属性,则不会执行此操作。 - user2126375

5

我花了一些时间才理解@ user1996230的答案,所以我决定提供一个更明确的例子。在下面的示例中,我为在另一个AppDomain中加载的对象创建了一个代理,并从另一个域调用该对象的方法。

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

代码中有一些小错别字,我必须承认我并不相信它会起作用,但这对我来说真是救命稻草。非常感谢。 - Owen Ivory

5

那么我必须手动指定所请求的程序集吗?即使它在新的AppDomain的AppBase中?有没有不这样做的方法? - abatishchev

5
关键在于由AppDomain引发的AssemblyResolve事件。
[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"}));
}

2

我曾多次需要这样做,并研究了许多不同的解决方案。

我发现最优雅且易于实现的解决方案如下:

1. 创建一个项目,您可以在其中创建一个简单的接口

该接口将包含您希望调用的任何成员的签名。

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

保持项目的简洁和轻量化非常重要。这是一个可以被两个AppDomain引用的项目,它将允许我们不从客户端程序集中引用我们希望在单独域中加载的Assembly

2. 现在创建一个包含你想要在单独的AppDomain中加载的代码的项目。

这个项目和客户端项目一样,将引用代理项目并实现接口。

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

3. 接下来,在客户端项目中,在另一个AppDomain中加载代码。

现在,我们创建了一个新的AppDomain。可以指定程序集引用的基本位置。探测将会检查全局程序集缓存和当前目录中的相关程序集,以及AppDomain基本位置中的相关程序集。

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

如果需要的话,有很多不同的方法可以加载程序集。您可以使用此解决方案的其他方式。如果您有程序集限定名称,那么我喜欢使用CreateInstanceAndUnwrap,因为它会加载程序集字节,然后为您实例化类型并返回一个object,您可以将其简单地转换为代理类型,或者如果您不太熟悉强类型代码,您可以使用动态语言运行时,并将返回的对象分配给dynamic类型变量,然后直接调用其中的成员。

就是这样。

这允许在客户端项目中没有引用的程序集中加载一个单独的AppDomain,并从客户端调用其中的成员。

要进行测试,我喜欢使用Visual Studio中的模块窗口。它将显示您的客户端程序集域以及该域中加载的所有模块,以及您的新应用程序域以及在该域中加载的程序集或模块。

关键是要确保您的代码要么派生自MarshalByRefObject,要么可序列化。

`MarshalByRefObject` 可以让您配置其所在域的生命周期。例如,假设您希望在 20 分钟内未调用代理时销毁该域。

希望这可以帮助到您。


嗨,如果我没记错的话,核心问题是如何递归加载所有依赖项,因此才会问这个问题。请通过将HelloWorld更改为返回类型为“Foo,FooAssembly”的类(其中包含类型为“Bar,BarAssembly”的属性)来测试您的代码,即总共3个程序集。它是否会继续工作? - abatishchev
是的,在程序集探测阶段需要正确的目录枚举。AppDomain有一个ApplicationBase,但我没有测试过它。此外,您可以在配置文件中指定程序集探测目录,例如app.config,dll也可以使用它,只需在属性中设置为复制即可。另外,如果您控制要加载到单独应用程序域中的程序集的构建,引用可以获得指定查找位置的HintPath。如果所有这些都失败了,我会订阅新的AppDomains AssemblyResolve事件并手动加载程序集。有很多例子可以参考。 - SimperT

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