接續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 建立關聯,需要花點時間了解上手,但彈性與功能都更強大,在此一併提供大家參考。

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 謝謝指正

Post a comment