如何使用所有引用递归将程序集加载到AppDomain?


113

我想加载到AppDomain具有复杂引用树的新程序集(MyDll.dll-> Microsoft.Office.Interop.Excel.dll-> Microsoft.Vbe.Interop.dll-> Office.dll-> stdole.dll)

据我了解,将程序集加载到时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,版本= 1.0.0.0,文化=中性,PublicKeyToken =空'或其依赖项之一。该系统找不到指定的文件。

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

好吧,我之前要做 domain.Load(AssemblyName.GetAssemblyName(path));

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

但是FileNotFoundException再次出现在另一个(引用的)程序集上。

如何递归加载所有引用?

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


1
之前我已经多次加载过这样的程序集,而我从不需要手动加载所有引用。我不确定这个问题的前提是否正确。
米克

Answers:


68

您需要先调用,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而是使用加载任意程序集文件-但请注意,如果执行此操作,则需要自己加载所有依赖项。


20
查看我编写的用于解决此问题的代码:github.com/jduv/AppDomainToolkit。具体来说,看LoadAssemblyWithReferences方法在这个类:github.com/jduv/AppDomainToolkit/blob/master/AppDomainToolkit/...
Jduv

3
我发现,虽然这部作品大部分的时间,在某些情况下,你居然还需要一个处理程序连接到AppDomain.CurrentDomain.AssemblyResolve如描述的事件这个MSDN答案。就我而言,我试图钩到MSTest的下运行的SpecRun部署,但我认为它适用于你的代码可能不会从“主”的AppDomain运行许多情况下- VS扩展,MSTest的,等等
Aaronaught

有趣。我将对此进行研究,看看是否可以通过ADT使它更容易使用。很抱歉,这段代码已经有一段时间没有用了,我们所有人都有工作:)。
Jduv 2014年

如果可以的话,@ Jduv将对您的评论进行100次投票。您的库帮助我解决了在MSBuild下进行动态程序集加载时遇到的看似无法解决的问题。您应该将其推广为答案!
菲利普·丹尼尔斯

2
@Jduv您确定assembly变量将引用“ MyDomain”中的程序集吗?我认为var assembly = value.GetAssembly(args[0]);您将同时加载args[0]到两个域中,并且 assembly变量将引用主应用程序域中的副本
Igor Bendrup 2016年

14

http://support.microsoft.com/kb/837908/zh-CN

C#版本:

创建一个主持人类并继承自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可以传递周围的AppDomain。因此,我想Assembly.LoadFrom尝试将程序集加载到新的appdomain中,只有在调用对象可以在这些appdomain之间传递的情况下才有可能。这也被称为远程处理如下所述:msdn.microsoft.com/en-us/library/...
克里斯托夫迈斯纳

32
这行不通。如果执行代码并检查AppDomain.CurrentDomain.GetAssemblies(),则会看到您尝试加载的目标程序集已加载到当前应用程序域中,而不是代理域中。
Jduv 2012年

41
这是完全废话。从继承MarshalByRefObject不会神奇地使其相互加载AppDomain,它只是告诉.NET框架创建透明的远程代理,而不是在将引用AppDomain彼此解包时使用序列化AppDomain(通常是CreateInstanceAndUnwrap方法)。不能相信这个答案有30多个投票;这里的代码只是一种毫无意义的回旋方式Assembly.LoadFrom
Aaronaught 2014年

1
是的,看起来完全是胡说八道,但它获得28票赞成票,并被标记为答案。提供的链接甚至没有提到MarshalByRefObject。很奇怪。如果这样做确实有帮助,我希望有人可以解释如何做
Mick

12

将程序集实例传递回调用方域后,调用方域将尝试加载它!这就是为什么您会得到例外。这发生在您的最后一行代码中:

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的角度来看,我对技术细节感兴趣。如果您能提供答案,我将不胜感激。
丹尼斯·卡塞尔

您使用代理类以避免将新程序集加载到您的调用者域中。如果使用Assembly.LoadFrom(string),则调用者域将尝试加载新的程序集引用,但不会找到它们,因为它不会在“ [AsmPath]”中搜索程序集。(msdn.microsoft.com/en-us/library/yx7xezcf%28v=vs.110%29.aspx
尼尔

11

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


没有。实际上,在新AppDomain上注册此事件的行上会出现异常。您必须在当前AppDomain上注册此事件。
user1004959 2013年

如果类是从MarshalByRefObject继承的,则可以。如果仅用[Serializable]属性标记了类,则不会。
user2126375 '18


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

4

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

0

我不得不这样做几次,并且研究了许多不同的解决方案。

我发现最优雅,最容易实现的解决方案可以照此实现。

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。可以指定程序集引用的基本位置。探测将检查GAC以及当前目录和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返回一个具有type Foo, FooAssembly属性的类型的类Bar, BarAssembly(即总共3个程序集)来测试您的代码。它会继续工作吗?
abatishchev

是的,需要在组装探测阶段枚举正确的目录。AppDomain具有一个ApplicationBase,但是,我没有对其进行测试。您还可以在配置文件中指定程序集探测目录,例如dll可以使用的app.config,也可以设置为复制到属性中。另外,如果您希望控制程序集的构建并希望在单独的应用程序域中加载,则引用可以获取一个HintPath,该HintPath指定要查找的对象。如果所有失败,我将订阅新的AppDomains AssemblyResolve事件并手动加载程序集。
SimperT
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.