Natasha 4.0 探索之路系列(二) "域"與外掛

AzulX發表於2022-01-21

域與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 的想法,

這個想法的優先順序不高:

  1. 是.Net Core 是在 3.0 時出現比較明顯的分水嶺, 包括依賴解析,上下文域識別等重要特性的支援;
  2. 是 Roslyn 對 FW 的支援不能低於(4.6.1);
  3. 是 UT測試需要區分版本來做,很麻煩, 外掛部分的測試不簡單;
  4. 是 個人精力原因, 還要工作, 還要維護其他專案;

這裡也希望公司們都能平穩度過升級期, 早點迎接更好更實用的"未來技術";

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 :

  1. 引入包 DotNetCore.Natasha.Domain 包.

  2. 載入外掛

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 將外掛載入到同一個域, 會出現很多問題:
問題案例

建議:

  1. 寫外掛時,本身解決好引用管理問題.
  2. 如果外掛過於龐大,請將外掛功能解耦,並載入到不同域中反射給主域執行.
  3. 主域要對依賴使用版本檢查, 請在外掛載入程式碼之前執行一些功能. 比如 _ = typeof(Dapper.CommandDefinition); 儘管這句沒有用, 但它將迫使執行時將 dapper 的程式集載入到預設上下文的快取中, 這樣在你載入外掛時, 如果遇到 dapper 依賴, 將觸發版本檢查詳見程式碼.

結尾

您可以自行檢視案例程式碼. NatashaDomain 是 Natasha 動態編譯的父級, Natasha 動態編譯中的 NatashaReferenceDomain 繼承了此類, 因此如果您想使用 Natasha 進行動態構建請使用 NatashaReferenceDomain. 下一篇將講解 Natasha 的基本編譯知識.

相關文章