這是比較進階一點的課題,手上有個專案剛好有用到,順手整理分享。

大家如果有寫過 ASP.NET MVC,一定都知道我們只需將類別命名成 FooController 或 BarController,不管放在 Controllers 或其他資料夾,甚至是放在獨立類別程式庫裡(這在將 MVC 功能打包成可攜式元件有示範過),ASP.NET MVC 都會自動把它們找出來並註冊好路由,將 Foo/ActionName、Bar/ActionName 導向所屬方法。新增類別時不需要手工註冊,系統會依特定條件自動發現,這個做法既酷炫又方便,讓我想模仿。

來模擬一個應用情境,假設我的網站會處理到多種格式檔案來源,且預估格式種類會持續增加。由於特定格式可能需要執行客製邏輯,我計劃用 Plugin (擴充套件,或稱外掛)方式解決。我的目標是,若新格式由第三方單位提出,系統加入支援時請他們一併提供依據事先定義好的介面寫好 Plugin DLL,讓我當成黑箱載入,以便在處理該文件時呼叫 Plugin 執行專屬邏輯。而要加入新的 Plugin,我希望只要參照新 Plugin DLL 就好,不需修改任何程式碼或設定檔,企圖做到跟 ASP.NET MVC 新增 Controller 一樣簡便。

我寫了一個簡單的 PoC 概念驗證(如下圖),在 PluginModule 專案定義 IDocPlugin 介面(若有其他開發 Plugin 需要的參數或結果型別、共用函式庫,也放在這個專),DemoJsonPlugin、DemoXmlPlugin 是兩個示範性質的 Plugin 實作,我的目標是讓 DemoWeb 自動找到這些 Plugin:

完整程式碼已放在 Github,這裡只挑重點講。

首先隨便定義一個 IDocPlugin,CreateFulltextIndex 方法是亂想的(勿砲),示範程式也不打算實作,光用 Version 傳回字串驗證成功與否:

public interface IDocPlugin
{
    /// <summary>
    /// 取得版本資訊
    /// </summary>
    string Version { get;  }
    /// <summary>
    /// 建立全文檢索用的索引
    /// </summary>
    /// <param name="content">文件檔內容</param>
    /// <returns>純文字索引</returns>
    string CreateFulltextIndex(byte[] content);
}

DemoJsonPlugin 有個 MyJsonFormat.cs 實作 IDocPlugin:

using PluginModule;
using System;

namespace DemoJsonPlugin
{
    public class MyJsonFormat : IDocPlugin
    {
        public string Version => "JSON 檔擴充套件 Ver 1.0.9527";

        public string CreateFulltextIndex(byte[] content)
        {
            throw new NotImplementedException();
        }
    }
}

DemoXmlPlugin 有個 MyXmlFormat.cs 也實作 IDocPlugin:

using PluginModule;
using System;

namespace DemoXmlPlugin
{
    public class MyXmlFormat : IDocPlugin
    {
        public string Version => "XML 檔擴充套件 Ver 2.1.5487";

        public string CreateFulltextIndex(byte[] content)
        {
            throw new NotImplementedException();
        }
    }
}

接著來看要怎麼「自動找到有實作 IDocPlugin 的類別」?構想是讓 DemoWeb 專案參照第三方提供的 DemoJsonPlugin.dll 及 DemoXmlPlugin.dll (在我的範例中是直接參照 .csproj,意義相同),如此 DemoWeb AppPool 程序在啟動時會載入 DemoJsonPlugin.dll 及 DemoXmlPlugin.dll 組件,我們用 AppDomain.CurrentDomain.GetAssemblies() 會包含,再用 GetTypes() 列出所有組件裡的所有類別,再用 Type.IsClass 及 Type.IsAssignableFrom(someType) 篩選出「有實作 IDocPlugin」的「類別」,用 LINQ 來寫易如反掌。我把這段邏輯寫在 PluginModule 的 PluginManager.cs:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace PluginModule
{
    public class PluginManager
    {
        public Dictionary<string, IDocPlugin> Plugins = null;

        public PluginManager(AppDomain app)
        {
            Type pluginInterface = typeof(IDocPlugin);
            Plugins =
                    //找出目前程序已載入的所有組件
                    app.GetAssemblies()
                    //列出所有組件裡的所有型別
                    .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)
                    );
        }
    }
}

材料都備齊了,打開瓦斯爐,大火快炒一番。

在 DemoWeb/Models 下寫一個 DocPluginManager 管理 Plugin,我透過靜態 Field 建立 PluginModule.PluginManager,自動找出系統已載入組件中實作 IDocPlugin 的類別型別,並加入列舉所有 Plugin 資訊、取得 IDocPlugin 物件的功能:

using PluginModule;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;

namespace DemoWeb.Models
{
    public static class DocPluginManager
    {
        static Dictionary<string, IDocPlugin> plugins = 
            new PluginManager(AppDomain.CurrentDomain).Plugins;

        public static Dictionary<string, string> PluginInfos =
            plugins.ToDictionary(o => o.Key, o => o.Value.Version);
        
        public static IDocPlugin GetPlugin(string docFormatName)
        {
            if (!plugins.ContainsKey(docFormatName)) 
                throw new ApplicationException("No plugin for format - " + docFormatName);
            return plugins[docFormatName];
        }
    }
}

在 /Views/Home/Index.cshtml 寫一段 Razor 把列出發現的 IDocPlugin:

@{
    Layout = null;
    var docPluginInfos = DemoWeb.Models.DocPluginManager.PluginInfos;
}

<!DOCTYPE html>

<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>Index</title>
</head>
<body>
    DocPlugin 清單:
    <ul> 
        @foreach (var keyVal in docPluginInfos)
        {
        <li>@keyVal.Key (@keyVal.Value)</li>
        }
    </ul>
</body>
</html>

實地驗測。沒參照任何 Demo*Plugin.dll 之前:

參照 DemoJsonPlugin.dll:

參照 DemoJsonPlugin.dll 與 DemoXmlPlugin.dll:

測試成功!

手上這個專案每次支援新格式時,網站的伺服器端程式必須修改,修改時一併加入 Plugin 參照還不算麻煩,這個設計已經很夠用。但有如果要做到更彈性 - 加新格式不用重新編譯網站(例如:只加入 cshtml + js + Plugin.dll 即可),便需要做到不加參照也能找到 Plugin DLL,這對 .NET 來說不算難事,但賣個關子下篇再來示範。

Example of how to implement type discovery in ASP.NET.


Comments

# by 毛豆

讓我猜猜,用 Assembly 讀取指定路徑的 dll ,就可以不加參考直接使用,甚至不需要重開網站 驗證碼 34+33 = ?

Post a comment