範例 - ASP.NET 自動發現特定類別 (DLL 檔掃瞄版)
5 |
接續ASP.NET 自動發現特定類別議題,上回提到 AppDomain.CurrentDomain.GetAssemblies() 尋找特定型別的前題是專案必須參照第三方 DLL,程序啟動時才會載入。這意味著每次要新增 DLL 都需修改專案加入參照並重新編譯,有點麻煩。故希望做到新增擴充套件不需重新編譯,複製 DLL 檔到特定目錄系統就能找到載入。
有 Reflection 在手,掃瞄 DLL 動態載入不是難事。先修改 PluginManager,除了原本傳入 AppDomain 的建構式外,再增加一個傳 bin 目錄的建構式,透過 Directory.GetFiles(binPath, "*Plugin.dll") 找出以 Plugin.dll 結尾的 DLL 檔,再用 Assembly.LoadFrom(file) 載入組件,其餘邏輯不變:
public class PluginManager
{
public Dictionary<string, IDocPlugin> Plugins = null;
public PluginManager(AppDomain app)
{
//找出目前程序已載入的所有組件
FindIDocPlugins(app.GetAssemblies());
}
public PluginManager(string binPath)
{
//找出bin目錄下*Plugin.dll
FindIDocPlugins(System.IO.Directory.GetFiles(binPath, "*Plugin.dll")
.Select(file => Assembly.LoadFrom(file)));
}
void FindIDocPlugins(IEnumerable<Assembly> assemblies)
{
Type pluginInterface = typeof(IDocPlugin);
Plugins = assemblies
//列出所有組件裡的所有型別
.SelectMany(o => o.GetTypes())
//篩選出有實作IDocPlugin的所有類別
.Where(t => t.IsClass && pluginInterface.IsAssignableFrom(t))
.ToDictionary(
o => o.Name, //名稱為Key,對映IDocPlugin Instance
//假設IDocPlugin為ThreadSafe,整個Process共用一個Instance即可
o => (IDocPlugin)Activator.CreateInstance(o)
);
}
}
ASP.NET MVC 的 DocPluginManager 做點小調整,我是直接把 *Plugin.dll 放在 bin,但另開資料夾集中管理也成:
//原本
static Dictionary<string, IDocPlugin> plugins =
new PluginManager(AppDomain.CurrentDomain).Plugins;
//改成
static Dictionary<string, IDocPlugin> plugins =
new PluginManager(HostingEnvironment.MapPath("~/bin")).Plugins;
這樣就改好了,還蠻簡單的。花掉我比較多時間的反而是研究怎樣在沒有參照關係的狀況下,要求 Visual Studio 將 DemoJsonPlugin.dll 及 DemoXmlPlugin.dll 自動複製到 DemoWeb\bin。最後我是用 Post-build event command line,在 Demo*Plugin 專案編譯後將檔案複製到解決方案下的 PluginDlls 資料夾:
DemoWeb 專案則設定編譯完成後將 PluginDlls 資料夾的 *Plugin.dll 內容複製到 bin 目錄:
搞定!
另外,我還試著將同樣做法搬到 ASP.NET Core。當初寫 PluginModule、DemoJsonPlugin、DemoXmlPlugin 有留下伏筆,我選擇開 .NET Standard Class Library 專案,如此程式庫可直接用在 ASP.NET Core。
在 ASP.NET Core 引用 PluginManager,最大的差異點不意外於全面 DI (依賴注入)化,並避免使用靜態屬性及方法。所以 Models/DocPluginManager 要做點小調整,取消靜態屬性,改從建構式接入 PluginManager:
public class DocPluginManager
{
Dictionary<string, IDocPlugin> plugins;
public Dictionary<string, string> PluginInfos { get; private set; }
public DocPluginManager(PluginManager manager)
{
plugins = manager.Plugins;
PluginInfos = plugins.ToDictionary(o => o.Key, o => o.Value.Version);
}
public IDocPlugin GetPlugin(string docFormatName)
{
if (!plugins.ContainsKey(docFormatName))
throw new ApplicationException("No plugin for format - " + docFormatName);
return plugins[docFormatName];
}
}
然後在 Startup ConfigureServices() 時,先註冊 PluginManager,再註冊 DocPluginManager,都用 Singleton,這樣就好了:
public void ConfigureServices(IServiceCollection services)
{
//env.ContentRootPath 在 VS 測試時非 bin 目錄,改由 Startup 所屬組件路徑推算
var binPath = Path.GetDirectoryName(this.GetType().Assembly.Location);
services.AddSingleton<PluginManager>(new PluginManager(binPath));
services.AddSingleton<DocPluginManager>();
services.AddRazorPages();
}
我使用 Razor Page 顯示找到的 Plugin。這裡不能像在 ASP.NET MVC 直接由 DocPluginManager.Plugins 抓靜態屬性,但只需改用 @inject Models.DocPluginManager manager 由 DI 取得,其餘做法照舊。ASP.NET Core 的全面 DI 化一開始需要點時間適應,但熟悉後並不難上手。
@page
@model IndexModel
@inject Models.DocPluginManager manager
@{
ViewData["Title"] = "Home page";
var docPluginInfos = manager.PluginInfos;
}
<div class="text-center">
<h1 class="display-4">Welcome</h1>
<p>Learn about <a href="https://docs.microsoft.com/aspnet/core">building Web apps with ASP.NET Core</a>.</p>
DocPlugin 清單:
<ul>
@foreach (var keyVal in docPluginInfos)
{
<li>@keyVal.Key (@keyVal.Value)</li>
}
</ul>
</div>
測試成功。
程式碼已更新到 Github,放在 feature/scan-dll 分支,有需要的同學可自取參考。
補充一點,前文發佈後,感謝讀者程凱大分享微軟有個 MEF (Managed Extensibility Framework) 可實現極類似的可擴充套件架構,一樣能做到自動尋找 DLL 載入應用,並使用 Attribute 建立關聯,需要花點時間了解上手,但彈性與功能都更強大,在此一併提供大家參考。
- Managed Extensibility Framework (MEF) - Microsoft Docs
- MEF開發系列 - Managed Extensibility Framework(MEF)的概念與簡介 - Level Up
- C#可扩展编程之MEF学习笔记(一):MEF简介及简单的Demo
Exmaple of scaning dll files to find and load specific types.
Comments
# by Chris Torng
上一篇我也想講 MEF。不過前面文章引用的是 .NET Framework 時代的,.NET Core 的話可參考 https://weblogs.asp.net/ricardoperes/using-mef-in-net-core ,有重要功能不能使用 (猜想應該是跨平台的問題吧?)。針對新版 MEF2 微軟沒什麼文件說明 https://github.com/dotnet/docs/issues/7613 ,還好套件還有持續更新 https://www.nuget.org/packages/System.Composition/ ,應該一時還不會被放生吧?
# by Chris Torng
可再參考 https://cloud.tencent.com/developer/article/1341027 。
# by Slash
Relection 是指 Reflection 嗎?
# by Jeffrey
to Chris Torng,謝謝補充這麼多資訊(尤其是 .NET Core 後續)。我這次的應用情境很單純,簡單寫了幾行搞定就沒想再換了,但 MEF 看起來是好物,已收入口袋名單,找時間練習。
# by Jeffrey
to Slash,you got it. 錯字狂的日常... orz 謝謝指正