插件式的 WebApi 開發,首要面對的問題就是程序集的發現。因為開發的過程中,都是在各自的解決方案下進行開發,部署后是分模塊放在一個整體的的運行時網站下。
約定
這里我根據上一節的設定,把插件打包完成后的文件夾,放入網站 bin
目錄下。重復一下這樣做的好處:在插件的配置或者程序集發生變動后,網站會直接重新啟動。
這是 IIS 的機制,和 WebApi 無關。
- 約定插件的文件夾名稱使用
00_Name
的形式,可以更方便的按照我們的要求排列插件。 - 約定插件的配置文件為插件根目錄
PluginConfig.xml
文件 - 約定插件的配置文件如下(后續隨着功能的添加會適當添加內容)
<?xml version="1.0" encoding="UTF-8"?>
<configuration enabled="true">
<description>授權支持插件</description>
<assemblies>
<add type="relative">bin/Intime.AuthorizationService.dll</add>
<add type="relative">bin/Intime.AuthorizationService.Services.dll</add>
<add type="relative">bin/Intime.AuthorizationService.Data.dll</add>
<add type="relative">bin/Intime.AuthorizationService.Data.Repository.dll</add>
</assembiles>
</configuration>
解釋一下 XML
配置文檔的含義:
- enabled="true" 表示當前插件的可用性,只有值為
true
的模塊才會進行后續操作;否則不做任何操作。 - description 子元素表示當前模塊的自然語義名稱,在程序上沒有任何含義,面向自然人說明當前模塊的作用。
- assemblies 子元素表示當前模塊下需要加載的程序集列表。
- add 元素表示添加一個程序集文件:目前有且只有一個
add
元素受支持。 - type 表示內含程序集路徑的類型:
- relative 表示相對路徑,為程序集文件相對於當前配置文件的路徑:比如第一個文件就是在當前目錄的子目錄
bin
目錄下。 - absolute 表示絕對路徑,表示程序不必做額外工作就可以根據路徑信息找到文件:此處沒有栗子。
- relative 表示相對路徑,為程序集文件相對於當前配置文件的路徑:比如第一個文件就是在當前目錄的子目錄
- 內含文本表示程序集的路徑信息。
- add 元素表示添加一個程序集文件:目前有且只有一個
BuildManager 程序集加載規則
在實現程序集加載之前,我們有必要大概了解一下 BuildManager
類加載程序集的規則。
首先,是項目引用,被網站項目引用的 GAC 程序集,都會列入到引用列表中;其次,就是這個類很強大,只要你將程序集放入網站,它就能知道網站運行需要加載這個程序集;最后,還有個不常用的設置,Web.config
文件的 runtime
配置節點中引進的目錄,也會被加載。所以我們要加載的程序集,必須是這三個地方所沒有的程序集。
另外,就是程序集多版本的問題。這里我們約定更新的版本完全兼容老版本,否則就需要升級代碼以適配這個功能。在網站運行出現多個版本存在的情況下,我們約定如下原則:
bin
目錄是最先加載的路徑,后續插件加載的程序集版本必須小於等於bin
目錄下程序集的版本。- 非 .Net 框架庫自帶的程序集,一律要拷貝到網站
bin
目錄下:請使用 NuGet 管理第三方程序集。 - 不要使用
Web.config
文件的runtime
配置節點加載個性目錄。
這樣,我們可以定義包含這些元數據的類:配置文件信息、加載的程序集列表,在 PreApplicationStartMethodAttribute
程序集特性設定的方法內,將我們的程序集加到 BuildManager
管理的 程序集列表中。在程序運行時,.Net 就可以找到我們的這些程序集了。
部分源代碼:自然語義
代碼是在 Mac 下用文本編輯器寫出來的,請自行腦補。
public class DynamicModule
{
public static DynamicModule Instance
{
get{ return Instance == null ? (Instance = new DynamicModule()) : Instance; }
}
public string BaseDirectory { get; set; }
public ModuleMetadata[] BaseDirectory { get; set; }
ctor()
{
BaseDirectory = Path.Combine(App.BaseDirectory, "bin");
var modules = Directory.GetFiles(BaseDirectory, "PlugConfig.xml", AllDirectory)
.Where(p => p.Configuration.Enabled);
bar baseAssemblies = AssemblyName.GetAssemblyNames(BaseDirectory, "*.dll", TopDirectory);
var data = baseAssemblies.Intersect(modules.LoadedAssemblyNames)
.Where(p => p.CodeBase != BaseDirectory);
if(data.Any())
throw new ModuleConfiguration(string.Format("程序集 {0} 裝載出現異常!", string.Join(data)));
modules.Foreach(p => p.LoadAssemblies());
}
}