域與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 的基本編譯知識.