域与ALC
在 Natasha 发布之后有不少小伙伴跑过来问域相关的问题, 能不能兼容 AppDomain, 如何使用 AppDomain, 为什么 CoreAPI 阉割了 AppDomain 等一系列的问题.
今天答复一下:
-
首先 AppDomain 作为程序集隔离容器的存在, 是风靡了 .Net Framework 的各大版本, 被誉为是轻量级进程, 由 AppDomain 发展的特性和操作也很多.而 Natasha 采用的是 AssemblyLoadContext 简称 "ALC"; ALC 是 .NET Core 版本后出现的操作类, 这个类在 .NET Core 及以后的版本中, 只要加载依赖项, 就会调用它.有趣的是,你在调试代码过程中如果去观察它, 可以看到它缓存程序集的数量在增加. 因为还没运行到的程序集可以先不加载, 检测代码
AssemblyLoadContext.Default.Assemblies.Count()
; -
其次它本不是域,或者不能称为域. 它和域的区别是, FW 支持多域, 而 CORE 仅支持单域, CORE 就一个默认域. ALC 的名字翻译过来是, 程序集加载上下文, 看英文名字也是和域区分开了;
-
最后一点区别是域的卸载是强制的, ALC 的卸载是"协商"的, 相比域而言, ALC 中的程序集所包含的元数据被保持引用,就不能被卸载, 比如你反射出来的类或者方法或者其他什么的放到了一个主域的字典中,那么字典不毁,这个ALC就没办法卸载,尽管 ALC 有 Unload 方法,卸载还是要看元数据是否被保持引用;
Natasha 设计初衷是使用隔离性较强的字眼, 用域的概念来减少 .NETCore 带来的新的理解成本, 另外之前有打算兼容 AppDomain 的想法,
这个想法的优先级不高:
- 是.Net Core 是在 3.0 时出现比较明显的分水岭, 包括依赖解析,上下文域识别等重要特性的支持;
- 是 Roslyn 对 FW 的支持不能低于(4.6.1);
- 是 UT测试需要区分版本来做,很麻烦, 插件部分的测试不简单;
- 是 个人精力原因, 还要工作, 还要维护其他项目;
这里也希望公司们都能平稳度过升级期, 早点迎接更好更实用的"未来技术";
Natasha 域的使用
插件的开发技巧
这里不得不回顾一下插件开发的知识, 它可不是像培训机构讲的编译一个 DLL 然后 Assembly.LoadFrom 就可以的.
首先要了解加载插件的两个侧重点, 插件依赖打包和插件依赖管理.
- 插件依赖打包: 首先插件生成时,你需要把必要的引用库一起打包, 此时需要在工程文件的 PropertyGroup 节点中添加
<EnableDynamicLoading>true</EnableDynamicLoading>
让编译程序输出依赖文件, 同时不要忘了交付 "xxx.deps.json", 这是让宿主程序解析依赖的关键; - 插件依赖管理: 如果你的接口 IPlugin 给到插件开发人员, 让他按照这个接口去写功能, 那么当他交付插件时, 你不能再将他包里的 IPlugin 再引进来. 否则如下代码将报错, (
var plugin = (IPlugin)Activtor.Create(pluginA);
) 类型转换错误, 原因是代码中的 IPlugin 在主域中使用, 而 pluginA 是加载到其他域中的, 而且在那个域里也存在一个 IPlugin, 这个接口类型不同于主域的接口类型, 因此在转换时会引发类型转换的错误. - 解决方法1: 让插件开发人员在自己的工程添加设置,自动排除这个主要依赖. (官方的推荐做法)
<ItemGroup>
<ProjectReference Include="..\IPlugin\IPlugin.csproj">
<Private>false</Private>
<ExcludeAssets>runtime</ExcludeAssets>
</ProjectReference>
</ItemGroup>
- 解决方法2: 在实现的 ALC 中添加过滤方法排除 IPlugin.
以上是基本的插件开发知识,如果你还不了解, 可以读一读微软插件开发文档.
单独使用 NatashaDomain :
-
引入包
DotNetCore.Natasha.Domain
包. -
加载插件
NatashaDomain domain = new NatashaDomain("NewPluginDomain");
//加载方法: 参数1: 插件位置; 参数2: 根据 AssemblyName 排除需要加载的插件名称.
//加载插件,如果主域存在相同名字的依赖,则使用版本较高的那版.
domain.LoadPluginWithHighDependency("c:/xxx/pluginA.dll", excludeAssembliesFunc: asn => asn.Name.Contains("IPlugin"));
//加载插件,如果主域存在相同名字的依赖,则使用版本较低的那版.
domain.LoadPluginWithLowDependency("c:/xxx/pluginA.dll", excludeAssembliesFunc: asn => asn.Name.Contains("IPlugin"));
//加载插件,如果主域存在相同名字的依赖,则使用主域中的那版.
domain.LoadPluginUseDefaultDependency("c:/xxx/pluginA.dll");
//加载插件,不判重,全部加载.
domain.LoadPluginWithAllDependency("c:/xxx/pluginA.dll", excludeAssembliesFunc: asn => asn.Name.Contains("IPlugin"));
//卸载域
domain.Dispose();
避坑指南
如果您使用以上 API 将插件加载到同一个域, 会出现很多问题:
建议:
- 写插件时,本身解决好引用管理问题.
- 如果插件过于庞大,请将插件功能解耦,并加载到不同域中反射给主域执行.
- 主域要对依赖使用版本检查, 请在插件加载代码之前执行一些功能. 比如
_ = typeof(Dapper.CommandDefinition);
尽管这句没有用, 但它将迫使运行时将 dapper 的程序集加载到默认上下文的缓存中, 这样在你加载插件时, 如果遇到 dapper 依赖, 将触发版本检查详见代码.
结尾
您可以自行查看案例代码. NatashaDomain 是 Natasha 动态编译的父级, Natasha 动态编译中的 NatashaReferenceDomain 继承了此类, 因此如果您想使用 Natasha 进行动态构建请使用 NatashaReferenceDomain. 下一篇将讲解 Natasha 的基本编译知识.