7

我有一个在MyAssembly我的主程序集中使用的内存程序集(类库)MyApp.exe

byte[] assemblyData = GetAssemblyDataFromSomewhere();

(对于测试,该GetAssemblyDataFromSomewhere方法可以只File.ReadAllBytes对现有的程序集文件执行,但在我的真实应用程序中没有文件。)

MyAssembly仅具有 .NET Framework 引用,并且不依赖于任何其他用户代码。

我可以将此程序集加载到当前(默认)中AppDomain

Assembly.Load(assemblyData);

// this works
var obj = Activator.CreateInstance("MyAssembly", "MyNamespace.MyType").Unwrap();

现在,我想将此程序集加载到不同AppDomain的地方并在那里实例化该类。MyNamespace.MyType派生自MarshalByRefObject,因此我可以跨应用程序域共享实例。

var newAppDomain = AppDomain.CreateDomain("DifferentAppDomain");

// this doesn't really work...
newAppDomain.Load(assemblyData);

// ...because this throws a FileNotFoundException
var obj = newAppDomain.CreateInstanceAndUnwrap("MyAssembly", "MyNamespace.MyType");

是的,我知道AppDomain.Load文档中有一条注释:

此方法应仅用于将程序集加载到当前应用程序域中。

是的,它应该用于那个,但是......

如果当前AppDomain对象表示应用程序域A,并且从应用程序域BLoad调用该方法,则程序集将加载到两个应用程序域中。

我可以忍受这一点。如果程序集将被加载到两个应用程序域中,对我来说没有问题(因为我实际上将它加载到默认应用程序域中)。

我可以看到该程序集已加载到新的应用程序域中。有点儿。

var assemblies = newAppDomain.GetAssemblies().Select(a => a.GetName().Name);
Console.WriteLine(string.Join("\r\n", assemblies));

这给了我:

mscorlib
MyAssembly

但是尝试实例化类总是会导致 a FileNotFoundException,因为 CLR 尝试从文件加载程序集(尽管它已经加载,至少根据AppDomain.GetAssemblies)。

我可以这样做MyApp.exe

newAppDomain.AssemblyResolve += CustomResolver;

private static Assembly CustomResolver(object sender, ResolveEventArgs e)
{
    byte[] assemblyData = GetAssemblyDataFromSomewhere();
    return Assembly.Load(assemblyData);
}

这可行,但这会导致第二个应用程序域MyApp.exe从文件加载调用程序集 ()。发生这种情况是因为该应用程序域现在需要来自CustomResolver调用程序集的代码(方法)。

我可以将应用程序域创建逻辑和事件处理程序移动到不同的程序集中,例如MyAppServices.dll,因此新的应用程序域将加载该程序集而不是MyApp.exe.

但是,我想不惜一切代价避免文件系统访问我的应用程序目录:新的应用程序域不得从文件中加载任何用户程序集。

我也试过AppDomain.DefineDynamicAssembly了,但这也没有用,因为返回值的类型System.Reflection.Emit.AssemblyBuilder既没有MarshalByRefObject也没有标记[Serializable]

有没有办法将字节数组中的程序集加载到非默认值AppDomain中,而无需将调用程序集从文件加载到该应用程序域中?实际上,没有任何文件系统访问我的应用程序目录?

4

2 回答 2

1

您的第一个问题是将程序集加载到第二个AppDomain.

您需要在两者之间加载/共享某种类型AppDomainsAppDomain如果程序集尚未加载到第一个AppDomain 中,则无法将程序集从第一个加载到第二个 AppDomain 中AppDomain(如果将程序集字节加载到第一个 AppDomain uisng 中,它也将不起作用.Load(...))。
这应该是一个很好的起点:
假设我有一个名为Models的类库,其中包含单个类Person,如下所示:

namespace Models
{
    public class Person : MarshalByRefObject
    {
        public void SayHelloFromAppDomain()
        {
            Console.WriteLine($"Hello from {AppDomain.CurrentDomain.FriendlyName}");
        }
    }
}

和控制台应用程序如下(模型类库不是项目中的引用)

namespace ConsoleApp
{
    internal class Program
    {
        [LoaderOptimizationAttribute(LoaderOptimization.MultiDomain)]
        public static void Main(String[] args)
        {
            CrossAppDomain();
        }

        private static Byte[] ReadAssemblyRaw()
        {
            // read Models class library raw bytes
        }

        private static void CrossAppDomain()
        {
            var bytes = ReadAssemblyRaw();
            var isolationDomain = AppDomain.CreateDomain("Isolation App Domain");

            var isolationDomainLoadContext = (AppDomainBridge)isolationDomain.CreateInstanceAndUnwrap(Assembly.GetExecutingAssembly().FullName, "ConsoleApp.AppDomainBridge");
            // person is MarshalByRefObject type for the current AppDomain
            var person = isolationDomainLoadContext.ExecuteFromAssembly(bytes);
        }
    }


    public class AppDomainBridge : MarshalByRefObject
    {
        public Object ExecuteFromAssembly(Byte[] raw)
        {
            var assembly = AppDomain.CurrentDomain.Load(rawAssembly: raw);
            dynamic person = assembly.CreateInstance("Models.Person");
            person.SayHelloFromAppDomain();
            return person;
        }
    }
}

AppDomainBridge它的工作方式是通过从加载到两者中的ConsoleApp项目创建实例AppDomains。现在这个实例正在进入第二个AppDomain。然后,您可以使用该AppDomainBridge实例将程序集实际加载到第二个中AppDomain,并跳过与第一个 AppDomain 相关的任何内容。
这是我执行代码(.NET Framework 4.7.2)时控制台的输出,因此Person实例位于第二个AppDomain

在此处输入图像描述


您的第二个问题是在 AppDomains 之间共享实例。

共享相同代码之间的主要问题AppDomains是需要共享相同的 JIT 编译代码(方法表、类型信息……等)。
来自docs.microsoft

JIT 编译的代码不能用于加载到加载源上下文、使用 Assembly 类的 LoadFrom 方法或使用指定字节数组的 Load 方法的重载从图像加载的程序集。

因此,当您从字节加载 assmebly 时,您将无法完全共享类型信息,这意味着此时您的对象只是MarshalByRefObject第一个AppDomain. 这意味着您只能从MarshalByRefObject类型执行和访问方法/属性(如果您尝试使用动态/反射没关系 - 第一个AppDomain没有实例的类型信息)。

您可以做的不是从 中返回对象ExecuteFromAssembly,而是将AppDomainBridge类扩展为已创建实例的简单包装器,Person并使用它将任何方法执行从第一个委托AppDomain到第二个,如果您确实需要它来实现这些目的。

于 2020-02-02T16:14:50.620 回答
0

我不太确定您要达到什么目的,但我会尝试以下方法。

一般来说,你的方法似乎没问题。您必须确保正确设置辅助 appdomain 的探测路径(尤其是 appbase 路径)。否则,.NET Fusion 将探测这些位置的依赖关系,并且您将尝试避免那些不需要的文件系统访问尝试。(好吧,至少确保将这些路径配置为一些没有设置真正权限的临时文件夹)。

提议的解决方案

在任何情况下,您都可以尝试向您的动态(我应该这样称呼它吗?)程序集添加一个入口点(例如Main某个类中的方法),并在将程序集加载到辅助 AppDomain 之后Bootstrap尝试调用AppDomain.ExecuteAssemblyByName 。

我会将您的方法添加到BootstrapCustomResolver中,并在该Main方法中订阅AssemblyResolve.

这样,当Main调用该方法时(希望它按预期工作),对 AppDomain 的订阅AssemblyResolve不会触发融合。

我没有测试这个解决方案,它可能是一个很长的镜头,但更糟糕的是尝试。

PS: 我确实看到有关此方法的文档确实指出,运行时将首先尝试加载程序集(可能使用常规探测逻辑),但它没有说明程序集预加载到的情况调用之前的 AppDomain。

评论

ExecuteAssemblyByName 方法提供与 ExecuteAssembly 方法类似的功能,但通过显示名称或 AssemblyName 而不是文件位置指定程序集。因此,ExecuteAssemblyByName 使用Load方法而不是 LoadFile 方法加载程序集。

程序集在 .NET Framework 标头中指定的入口点开始执行。

此方法不会创建新的进程或应用程序域,也不会在新线程上执行入口点方法。

加载方法文档也没有提供明确的答案。

PPS: 调用Unwrap方法可能会触发主 AppDomain 中的融合,因为为您的类创建了代理。我认为,此时您的主 AppDomain 将尝试定位该动态加载的程序集。您确定是引发异常的辅助 AppDomain 吗?

于 2020-02-01T22:37:41.960 回答