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

我想加载到一个新的 AppDomain一些程序集,它有一个复杂的引用树(MyDll.dll-> Microsoft)。办公室。互联系统。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:

Could not load file or assembly 'MyDll, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null' or one of its dependencies. The system cannot find the file specified.

I think the key part is 它的一个附属物.

好的,我在 domain.Load(AssemblyName.GetAssemblyName(path));之前做下一个

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

但在另一个(参考)程序集上又得到了 FileNotFoundException

如何递归加载所有引用?

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

147249 次浏览

您需要处理 AppDomain。程序集解析或应用程序域。如果引用的程序集不在 GAC 或 CLR 的探测路径中,则反射 OnlyAssemblyResolve 事件(取决于您正在进行的加载)。

组装解决方案

只反射程序集解决方案

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

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

C # 版本:

Create a moderator class and inherit it from MarshalByRefObject:

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

call from client site

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

在外部应用程序域中执行代理对象之前,需要调用 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异常,因为 Assembly 解析器将尝试在 GAC 或当前应用程序的 bin 文件夹中找到您正在加载的程序集。使用 LoadFile加载任意的汇编文件——但是请注意,如果这样做,您将需要自己加载任何依赖项。

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

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

domain.Load(AssemblyName.GetAssemblyName(path));

Thus, whatever you want to do with the assembly, should be done in a proxy class - a class which inherit MarshalByRefObject.

考虑到调用方域和新创建的域都应该可以访问代理类程序集。如果您的问题并不太复杂,可以考虑让 ApplicationBase 文件夹保持不变,这样它将与调用方域文件夹相同(新域将只加载它所需的 Assembly)。

在简单的代码中:

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

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

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

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

这样,所有 dll 都将自动从 dllsSearchPath 解析。

我花了一段时间才理解@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"});
}

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

我发现最优雅和最容易实现的解决方案可以这样实现。

1. 创建一个可以创建简单界面的项目

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

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

保持这个项目的清洁和简洁是很重要的。这是一个项目,这两个 AppDomain的可以参考,并将允许我们不引用的 Assembly,我们希望在独立的域从我们的客户端装配加载。

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

与客户端 proj 一样,这个项目将引用代理 proj,您将实现接口

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

接下来,在客户端项目中,在另一个 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 Language Runtime,并将返回的对象分配给一个 dynamic类型的变量,然后直接对它调用成员。

就是这样。

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

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

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

Marshall ByRefObject 将允许您配置其所在域的生存期。例如,假设您希望在20分钟内没有调用代理的情况下销毁域。

希望这个能帮上忙。