C# 動態載入元件類庫,支援熱插拔元件

木狼發表於2024-03-24

新建專案IPlugin類庫,裡面新增IPlugin.cs(外掛介面),PluginManager.cs(外掛管理),FileListenerServer.cs(資料夾監控),依次如下

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

namespace IPlugin
{
    public interface IPlugin:IDisposable
    {
        Guid Guid { get; }
        string Menu { get; }
        string Name { get; }
        void Execute();
        void Load();
        string ShowName();
    }
}
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;


namespace IPlugin
{
    public class PluginManager
    {
        /// <summary>
        /// 當前擁有的外掛
        /// </summary>
        public Dictionary<string, IPlugin> Plugins { get => _plugins; }

        private Dictionary<string, IPlugin> _plugins;

        /// <summary>
        /// 檔案監聽
        /// </summary>
        private FileListenerServer _fileListener = null;
        private void Main(string[] args)
        {
            Console.WriteLine("可插拔外掛服務");
            var dic = Directory.GetCurrentDirectory();
            var path = Path.Combine(dic, "plugIn");
            Init(path);
            // 監聽檔案下外掛變化,實現熱插拔
            _fileListener = new FileListenerServer(path, ref _plugins);
            _fileListener.Start();
            Console.WriteLine("按q/Q退出");
            while (true)
            {
                string input = Console.ReadLine();
                switch (input)
                {
                    case "q":
                        _fileListener.Stop();
                        return;
                    case "Q":
                        _fileListener.Stop();
                        return;
                    default:
                        Console.WriteLine("按q/Q退出");
                        break;
                }
            }
        }
        /// <summary>
        /// 初始化外掛
        /// </summary>
        private void Init(string path)
        {
            Console.WriteLine(string.Format("==========【{0}】==========", "開始載入外掛"));
            // 1.獲取資料夾下所有dll檔案
            DirectoryInfo directoryInfo = new DirectoryInfo(path);
            var dlls = directoryInfo.GetFiles();
            // 2.啟動每個dll檔案
            for (int i = 0; i < dlls.Length; i++)
            {
                // 2.1 獲取程式集
                var fileData = File.ReadAllBytes(dlls[i].FullName);
                Assembly asm = Assembly.Load(fileData);
                var manifestModuleName = asm.ManifestModule.ScopeName;
                // 2.2 dll名稱
                var classLibrayName = manifestModuleName.Remove(manifestModuleName.LastIndexOf("."), manifestModuleName.Length - manifestModuleName.LastIndexOf("."));
                Type type = asm.GetType("Plugin_Test" + "." + classLibrayName);
                if (!typeof(IPlugin).IsAssignableFrom(type))
                {
                    Console.WriteLine("未繼承外掛介面");
                    continue;
                }
                //dll例項化
                var instance = Activator.CreateInstance(type) as IPlugin;
                instance.Execute();
                _plugins.Add(classLibrayName, instance);
                //釋放外掛資源
                instance.Dispose();
                instance = null;
            }

            Console.WriteLine(string.Format("==========【{0}】==========", "外掛載入完成"));
            Console.WriteLine(string.Format("==========【{0}】==========", "共載入外掛{0}個"), _plugins.Count);
        }
    }
}
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using System;

namespace IPlugin
{
    public class FileListenerServer
    {
        /// <summary>
        /// 檔案監聽
        /// </summary>
        private FileSystemWatcher _watcher;
        /// <summary>
        /// 外掛
        /// </summary>
        private Dictionary<string, IPlugin> _iPlugin;
        /// <summary>
        /// 外掛資訊
        /// </summary>
        public FileListenerServer(string path, ref Dictionary<string, IPlugin> keyValuePairs)
        {

            try
            {
                _iPlugin = keyValuePairs;
                this._watcher = new FileSystemWatcher();
                _watcher.Path = path;
                _watcher.NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.FileName | NotifyFilters.Size | NotifyFilters.DirectoryName;
                //_watcher.IncludeSubdirectories = true;
                _watcher.Created += new FileSystemEventHandler(FileWatcher_Created);
                _watcher.Changed += new FileSystemEventHandler(FileWatcher_Changed);
                _watcher.Deleted += new FileSystemEventHandler(FileWatcher_Deleted);
                _watcher.Renamed += new RenamedEventHandler(FileWatcher_Renamed);
            }

            catch (Exception ex)
            {
                Console.WriteLine("Error:" + ex.Message);
            }
        }


        public void Start()
        {
            // 開始監聽
            this._watcher.EnableRaisingEvents = true;
            Console.WriteLine(string.Format("==========【{0}】==========", "檔案監控已經啟動..."));

        }

        public void Stop()
        {

            this._watcher.EnableRaisingEvents = false;
            this._watcher.Dispose();
            this._watcher = null;
            Console.WriteLine(string.Format("==========【{0}】==========", "檔案監控已經關閉"));
        }
        /// <summary>
        /// 新增外掛
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        protected void FileWatcher_Created(object sender, FileSystemEventArgs e)
        {
            Console.WriteLine(string.Format("==========【{0}】==========", "新增" + e.Name));
            var dll = new FileInfo(e.FullPath);
            var fileData = File.ReadAllBytes(dll.FullName);
            Assembly asm = Assembly.Load(fileData);
            var manifestModuleName = asm.ManifestModule.ScopeName;
            var classLibrayName = manifestModuleName.Remove(manifestModuleName.LastIndexOf("."), manifestModuleName.Length - manifestModuleName.LastIndexOf("."));
            Type type = asm.GetType("Plugin_Test" + "." + classLibrayName);
            // 這裡預設不替換之前的外掛內容
            if (_iPlugin.ContainsKey(classLibrayName))
            {
                Console.WriteLine("已經載入該外掛");
                return;
            }
            if (!typeof(IPlugin).IsAssignableFrom(type))
            {
                Console.WriteLine($"{asm.ManifestModule.Name}未繼承約定介面");
                return;
            }
            //dll例項化
            var instance = Activator.CreateInstance(type) as IPlugin;
            instance.Execute();
            _iPlugin.Add(classLibrayName, instance);
            //釋放外掛資源
            instance.Dispose();
            instance = null;
            Console.WriteLine(string.Format("==========【{0}】==========", "共載入外掛{0}個"), _iPlugin.Count);
        }
        /// <summary>
        /// 修改外掛
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        protected void FileWatcher_Changed(object sender, FileSystemEventArgs e)
        {
            string pluginName = e.Name.Split('.')[0];
            var dll = new FileInfo(e.FullPath);
            // 替換外掛
            Console.WriteLine(string.Format("==========【{0}】==========", "修改" + e.Name));
            // 更新
            var fileData = File.ReadAllBytes(e.FullPath);
            Assembly asm = Assembly.Load(fileData);
            var manifestModuleName = asm.ManifestModule.ScopeName;
            var classLibrayName = manifestModuleName.Remove(manifestModuleName.LastIndexOf("."), manifestModuleName.Length - manifestModuleName.LastIndexOf("."));
            Type type = asm.GetType("Plugin_Test" + "." + classLibrayName);
            if (!typeof(IPlugin).IsAssignableFrom(type))
            {
                Console.WriteLine($"{asm.ManifestModule.Name}未繼承約定介面");
                return;
            }
            var instance = Activator.CreateInstance(type) as IPlugin;
            instance.Execute();
            _iPlugin[classLibrayName] = instance;
            instance.Dispose();
            instance = null;
            // 避免多次觸發
            this._watcher.EnableRaisingEvents = false;
            this._watcher.EnableRaisingEvents = true;
            Console.WriteLine(string.Format("==========【{0}】==========", "共載入外掛{0}個"), _iPlugin.Count);
        }
        /// <summary>
        /// 刪除外掛
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        protected void FileWatcher_Deleted(object sender, FileSystemEventArgs e)
        {
            Console.WriteLine(string.Format("==========【{0}】==========", "刪除" + e.Name));
            string pluginName = e.Name.Split('.')[0];
            if (_iPlugin.ContainsKey(pluginName))
            {
                _iPlugin.Remove(pluginName);
                Console.WriteLine($"外掛{e.Name}被移除");
            }
            Console.WriteLine(string.Format("==========【{0}】==========", "共載入外掛{0}個"), _iPlugin.Count);
        }
        /// <summary>
        /// 重新命名
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        protected void FileWatcher_Renamed(object sender, RenamedEventArgs e)
        {
            //TODO:暫時不做處理
            Console.WriteLine("重新命名" + e.OldName + "->" + e.Name);
            //Console.WriteLine("重新命名: OldPath:{0} NewPath:{1} OldFileName{2} NewFileName:{3}", e.OldFullPath, e.FullPath, e.OldName, e.Name);
        }
    }
}

新建外掛實現類庫:PluginOne,繼承IPlugin介面

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

namespace PluginOne
{
    [Serializable]
    public class PluginEntry:MarshalByRefObject,IPlugin.IPlugin
    {
        public Guid Guid => new Guid("21EC2020-3AEA-1069-A2DD-08002B30309D");

        public string Menu => "使用者系統";

        public string Name => "測試許可權";

        public void Dispose()
        {
            throw new NotImplementedException();
        }

        public void Execute()
        {
            throw new NotImplementedException();
        }

        public void Load()
        {
            throw new NotImplementedException();
        }

        public string ShowName()
        {
            return "PluginOne";
        }
    }
}

新增啟動專案main,新建winform窗體,新增兩個按鈕;新建代理類ProxyObject.cs。

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Windows.Forms;

using IPlugin;

namespace PluginMain
{
    public partial class Form1 : Form
    {
        string assemblyName = Assembly.GetExecutingAssembly().GetName().FullName;

        public Form1()
        {
            InitializeComponent();
        }

        private void button1_Click(object sender, EventArgs e)
        {
            AppDomainSetup setup = new AppDomainSetup();
            setup.ApplicationBase = AppDomain.CurrentDomain.BaseDirectory;
            setup.PrivateBinPath = setup.ApplicationBase;
            setup.ApplicationName = "DllTest";
            setup.ShadowCopyDirectories = setup.ApplicationBase + "@dlls";
            setup.ShadowCopyFiles = "true";
            AppDomain ad = AppDomain.CreateDomain(assemblyName, null, setup);
            var proxy = (ProxyObject)ad.CreateInstanceFromAndUnwrap("PluginMain.exe", typeof(ProxyObject).FullName);
            //var obj =Activator.CreateInstanceFrom(ad.BaseDirectory + "\\Plugin\\PluginOne.dll", "PluginOne.PluginEntry");
            IPlugin.IPlugin Plugn = proxy.Create(ad.BaseDirectory + "\\Plugin\\PluginOne.dll", "PluginOne.PluginEntry", null);
            MessageBox.Show(Plugn.ShowName());
            AppDomain.Unload(ad);
        }

        private void button2_Click(object sender, EventArgs e)
        {
            AppDomainSetup setup = new AppDomainSetup();
            setup.ApplicationBase = AppDomain.CurrentDomain.BaseDirectory;
            setup.PrivateBinPath = setup.ApplicationBase;
            setup.ApplicationName = "DllTest";
            setup.ShadowCopyDirectories = setup.ApplicationBase + "@dlls";
            setup.ShadowCopyFiles = "true";
            AppDomain ad = AppDomain.CreateDomain(assemblyName, null, setup);
            var proxy = (ProxyObject)ad.CreateInstanceFromAndUnwrap("PluginMain.exe", typeof(ProxyObject).FullName);
            //var obj =Activator.CreateInstanceFrom(ad.BaseDirectory + "\\Plugin\\PluginOne.dll", "PluginOne.PluginEntry");
            IPlugin.IPlugin Plugn = proxy.Create(ad.BaseDirectory + "\\Plugin\\PluginTow.dll", "PluginTow.PluginEntry", null);
            MessageBox.Show(Plugn.ShowName());
            AppDomain.Unload(ad);
        }
    }
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;

namespace PluginMain
{
    public class ProxyObject :MarshalByRefObject
    {
        private const BindingFlags bfi = BindingFlags.Instance | BindingFlags.Public | BindingFlags.CreateInstance;


        public ProxyObject() { }

        public IPlugin.IPlugin Create(string assemblyFile, string typeName, object[] args)
        {
            return (IPlugin.IPlugin)Activator.CreateInstanceFrom(assemblyFile, typeName, false, bfi, null, args, null, null).Unwrap();
        }
        
    }
}

相關文章