從零開始實現ASP.NET Core MVC的插件式開發(四) - 插件安裝


標題:從零開始實現ASP.NET Core MVC的插件式開發(四) - 插件安裝
作者:Lamond Lu
地址:https://www.cnblogs.com/lwqlun/p/11343141.html
源代碼:https://github.com/lamondlu/Mystique

系列文章

上一篇中,我們針對運行時啟用/禁用組件做了一些嘗試,最終我們發現借助IActionDescriptorChangeProvider可以幫助我們實現所需的功能。本篇呢,我們就來繼續研究如何完成插件的安裝,畢竟之前的組件都是我們預先放到主程序中的,這樣並不是一種很好的安裝插件方式。

准備階段

創建數據庫

為了完成插件的安裝,我們首先需要為主程序創建一個數據庫,來保存插件信息。 這里為了簡化邏輯,我只創建了2個表,Plugins表是用來記錄插件信息的,PluginMigrations表是用來記錄插件每個版本的升級和降級腳本的。

設計說明:這里我的設計是將所有插件使用的數據庫表結構都安裝在主程序的數據庫中,暫時不考慮不同插件的數據庫表結構沖突,也不考慮插件升降級腳本的破壞性操作檢查,所以有類似問題的小伙伴可以先假設插件之間的表結構沒有沖突,插件遷移腳本中也不會包含破壞主程序所需系統表的問題。

備注:數據庫腳本可查看源代碼的DynamicPlugins.Database項目

創建一個安裝包

為了模擬安裝的效果,我決定將插件做成插件壓縮包,所以需要將之前的DemoPlugin1項目編譯后的文件以及一個plugin.json文件打包。安裝包的內容如下:

這里暫時使用手動的方式來實現,后面我會創建一個Global Tools來完成這個操作。

在plugin.json文件中記錄當前插件的一些元信息,例如插件名稱,版本等。

{
    "name": "DemoPlugin1",
    "uniqueKey": "DemoPlugin1",
    "displayName":"Lamond Test Plugin1",
    "version": "1.0.0"
}

編碼階段

在創建完插件安裝包,並完成數據庫准備操作之后,我們就可以開始編碼了。

抽象插件邏輯

為了項目擴展,我們需要針對當前業務進行一些抽象和建模。

創建插件接口和插件基類

首先我們需要將插件的概念抽象出來,所以這里我們首先定義一個插件接口IModule以及一個通用的插件基類ModuleBase

IModule.cs

	public interface IModule
    {
        string Name { get; }

        DomainModel.Version Version { get; }
    }

IModule接口中我們定義了當前插件的名稱和插件的版本號。

ModuleBase.cs

	public class ModuleBase : IModule
    {
        public ModuleBase(string name)
        {
            Name = name;
            Version = "1.0.0";
        }

        public ModuleBase(string name, string version)
        {
            Name = name;
            Version = version;
        }

        public ModuleBase(string name, Version version)
        {
            Name = name;
            Version = version;
        }

        public string Name
        {
            get;
            private set;
        }

        public Version Version
        {
            get;
            private set;
        }
    }

ModuleBase類實現了IModule接口,並進行了一些初始化的操作。后續的插件類都需要繼承ModuleBase類。

解析插件配置

為了完成插件包的解析,這里我創建了一個PluginPackage類,其中封裝了插件包的相關操作。

	public class PluginPackage
    {
        private PluginConfiguration _pluginConfiguration = null;
        private Stream _zipStream = null;

        private string _folderName = string.Empty;

        public PluginConfiguration Configuration
        {
            get
            {
                return _pluginConfiguration;
            }
        }

        public PluginPackage(Stream stream)
        {
            _zipStream = stream;
            Initialize(stream);
        }

        public List<IMigration> GetAllMigrations(string connectionString)
        {
            var assembly = Assembly.LoadFile($"{_folderName}/{_pluginConfiguration.Name}.dll");

            var dbHelper = new DbHelper(connectionString);

            var migrationTypes = assembly.ExportedTypes.Where(p => p.GetInterfaces().Contains(typeof(IMigration)));

            List<IMigration> migrations = new List<IMigration>();
            foreach (var migrationType in migrationTypes)
            {
                var constructor = migrationType.GetConstructors().First(p => p.GetParameters().Count() == 1 && p.GetParameters()[0].ParameterType == typeof(DbHelper));

                migrations.Add((IMigration)constructor.Invoke(new object[] { dbHelper }));
            }

            assembly = null;

            return migrations.OrderBy(p => p.Version).ToList();
        }

        public void Initialize(Stream stream)
        {
            var tempFolderName = $"{ AppDomain.CurrentDomain.BaseDirectory }{ Guid.NewGuid().ToString()}";
            ZipTool archive = new ZipTool(stream, ZipArchiveMode.Read);

            archive.ExtractToDirectory(tempFolderName);

            var folder = new DirectoryInfo(tempFolderName);

            var files = folder.GetFiles();

            var configFiles = files.Where(p => p.Name == "plugin.json");

            if (!configFiles.Any())
            {
                throw new Exception("The plugin is missing the configuration file.");
            }
            else
            {
                using (var s = configFiles.First().OpenRead())
                {
                    LoadConfiguration(s);
                }
            }

            folder.Delete(true);

            _folderName = $"{AppDomain.CurrentDomain.BaseDirectory}Modules\\{_pluginConfiguration.Name}";

            if (Directory.Exists(_folderName))
            {
                throw new Exception("The plugin has been existed.");
            }

            stream.Position = 0;
            archive.ExtractToDirectory(_folderName);
        }

        private void LoadConfiguration(Stream stream)
        {
            using (var sr = new StreamReader(stream))
            {
                var content = sr.ReadToEnd();
                _pluginConfiguration = JsonConvert.DeserializeObject<PluginConfiguration>(content);

                if (_pluginConfiguration == null)
                {
                    throw new Exception("The configuration file is wrong format.");
                }
            }
        }
    }

代碼解釋:

  • 這里在Initialize方法中我使用了ZipTool類來進行解壓縮,解壓縮之后,程序會嘗試讀取臨時解壓目錄中的plugin.json文件,如果文件不存在,就會報出異常。
  • 如果主程序中沒有當前插件,就會解壓到定義好的插件目錄中。(這里暫時不考慮插件升級,下一篇中會做進一步說明)
  • GetAllMigrations方法的作用是從程序集中加載當前插件所有的遷移腳本。

新增腳本遷移功能

為了讓插件在安裝時,自動實現數據庫表的創建,這里我還添加了一個腳本遷移機制,這個機制類似於EF的腳本遷移,以及之前分享過的FluentMigrator遷移。

這里我們定義了一個遷移接口IMigration, 並在其中定義了2個接口方法MigrationUpMigrationDown來完成插件升級和降級的功能。

	public interface IMigration
    {
        DomainModel.Version Version { get; }

        void MigrationUp(Guid pluginId);

        void MigrationDown(Guid pluginId);
    }	

然后我們實現了一個遷移腳本基類BaseMigration

	public abstract class BaseMigration : IMigration
    {
        private Version _version = null;
        private DbHelper _dbHelper = null;

        public BaseMigration(DbHelper dbHelper, Version version)
        {
            this._version = version;
            this._dbHelper = dbHelper;
        }

        public Version Version
        {
            get
            {
                return _version;
            }
        }

        protected void SQL(string sql)
        {
            _dbHelper.ExecuteNonQuery(sql);
        }

        public abstract void MigrationDown(Guid pluginId);

        public abstract void MigrationUp(Guid pluginId);

        protected void RemoveMigrationScripts(Guid pluginId)
        {
            var sql = "DELETE PluginMigrations WHERE PluginId = @pluginId AND Version = @version";

            _dbHelper.ExecuteNonQuery(sql, new List<SqlParameter>
            {
                new SqlParameter{ ParameterName = "@pluginId", SqlDbType = SqlDbType.UniqueIdentifier, Value = pluginId },
                new SqlParameter{ ParameterName = "@version", SqlDbType = SqlDbType.NVarChar, Value = _version.VersionNumber }
            }.ToArray());
        }

        protected void WriteMigrationScripts(Guid pluginId, string up, string down)
        {
            var sql = "INSERT INTO PluginMigrations(PluginMigrationId, PluginId, Version, Up, Down) VALUES(@pluginMigrationId, @pluginId, @version, @up, @down)";

            _dbHelper.ExecuteNonQuery(sql, new List<SqlParameter>
            {
                new SqlParameter{ ParameterName = "@pluginMigrationId", SqlDbType = SqlDbType.UniqueIdentifier, Value = Guid.NewGuid() },
                new SqlParameter{ ParameterName = "@pluginId", SqlDbType = SqlDbType.UniqueIdentifier, Value = pluginId },
                new SqlParameter{ ParameterName = "@version", SqlDbType = SqlDbType.NVarChar, Value = _version.VersionNumber },
                new SqlParameter{ ParameterName = "@up", SqlDbType = SqlDbType.NVarChar, Value = up},
                new SqlParameter{ ParameterName = "@down", SqlDbType = SqlDbType.NVarChar, Value = down}
            }.ToArray());
        }
    }

代碼解釋

  • 這里的WriteMigrationScriptsRemoveMigrationScripts的作用是用來將插件升級和降級的遷移腳本的保存到數據庫中。因為我並不想每一次都通過加載程序集的方式讀取遷移腳本,所以這里在安裝插件時,我會將每個插件版本的遷移腳本導入到數據庫中。
  • SQL方法是用來運行遷移腳本的,這里為了簡化代碼,缺少了事務處理,有興趣的同學可以自行添加。

為之前的腳本添加遷移程序

這里我們假設安裝DemoPlugin1插件1.0.0版本之后,需要在主程序的數據庫中添加一個名為Test的表。

根據以上需求,我添加了一個初始的腳本遷移類Migration.1.0.0.cs, 它繼承了BaseMigration類。

	public class Migration_1_0_0 : BaseMigration
    {
        private static DynamicPlugins.Core.DomainModel.Version _version = new DynamicPlugins.Core.DomainModel.Version("1.0.0");
        private static string _upScripts = @"CREATE TABLE [dbo].[Test](
                        TestId[uniqueidentifier] NOT NULL,
                    );";
        private static string _downScripts = @"DROP TABLE [dbo].[Test]";

        public Migration_1_0_0(DbHelper dbHelper) : base(dbHelper, _version)
        {

        }

        public DynamicPlugins.Core.DomainModel.Version Version
        {
            get
            {
                return _version;
            }
        }

        public override void MigrationDown(Guid pluginId)
        {
            SQL(_downScripts);

            base.RemoveMigrationScripts(pluginId);
        }

        public override void MigrationUp(Guid pluginId)
        {
            SQL(_upScripts);

            base.WriteMigrationScripts(pluginId, _upScripts, _downScripts);
        }
    }

代碼解釋

  • 這里我們通過實現MigrationUpMigrationDown方法來完成新表的創建和刪除,當然本文只實現了插件的安裝,並不涉及刪除或降級,這部分代碼在后續文章中會被使用。
  • 這里注意在運行升級腳本之后,會將當前插件版本的升降級腳本通過base.WriteMigrationScripts方法保存到數據庫。

添加安裝插件包的業務處理類

為了完成插件包的安裝邏輯,這里我創建了一個PluginManager類, 其中AddPlugins方法使用來進行插件安裝的。

    public void AddPlugins(PluginPackage pluginPackage)
    {
        var plugin = new DTOs.AddPluginDTO
        {
            Name = pluginPackage.Configuration.Name,
            DisplayName = pluginPackage.Configuration.DisplayName,
            PluginId = Guid.NewGuid(),
            UniqueKey = pluginPackage.Configuration.UniqueKey,
            Version = pluginPackage.Configuration.Version
        };

        _unitOfWork.PluginRepository.AddPlugin(plugin);
        _unitOfWork.Commit();

        var versions = pluginPackage.GetAllMigrations(_connectionString);

        foreach (var version in versions)
        {
            version.MigrationUp(plugin.PluginId);
        }
    }

代碼解釋

  • 方法簽名中的pluginPackage即包含了插件包的所有信息
  • 這里我們首先將插件的信息,通過工作單元保存到了數據庫
  • 保存成功之后,我通過pluginPackage對象,獲取了當前插件包中所包含的所有遷移腳本,並依次運行這些腳本來完成數據庫的遷移。

在主站點中添加插件管理界面

這里為了管理插件,我在主站點中創建了2個新頁面,插件列表頁以及添加新插件頁面。這2個頁面的功能非常的簡單,這里我就不進一步介紹了,大部分的處理都是復用了之前的代碼,例如插件的安裝,啟用和禁用,相關的代碼大家可以自行查看。


設置已安裝插件默認啟動

在完成2個插件管理頁面之后,最后一步,我們還需要做的就是在注程序啟動階段,將已安裝的插件加載到運行時,並啟用。

    public void ConfigureServices(IServiceCollection services)
    {
        ...

        var provider = services.BuildServiceProvider();
        using (var scope = provider.CreateScope())
        {
            var unitOfWork = scope.ServiceProvider.GetService<IUnitOfWork>();
            var allEnabledPlugins = unitOfWork.PluginRepository.GetAllEnabledPlugins();

            foreach (var plugin in allEnabledPlugins)
            {
                var moduleName = plugin.Name;
                var assembly = Assembly.LoadFile($"{AppDomain.CurrentDomain.BaseDirectory}Modules\\{moduleName}\\{moduleName}.dll");

                var controllerAssemblyPart = new AssemblyPart(assembly);
                mvcBuilders.PartManager.ApplicationParts.Add(controllerAssemblyPart);
            }
        }   
    }

設置完成之后,整個插件的安裝編碼就告一段落了。

最終效果

總結以及待解決的問題

本篇中,我給大家分享了如果將打包的插件安裝到系統中,並完成對應的腳本遷移。不過在本篇中,我們只完成了插件的安裝,針對插件的刪除,以及插件的升降級我們還未解決,有興趣的同學,可以自行嘗試一下,你會發現在.NET Core 2.2版本,我們沒有任何在運行時Unload程序集能力,所以在從下一篇開始,我將把當前項目的開發環境升級到.NET Core 3.0 Preview, 針對插件的刪除和升降級我將在.NET Core 3.0中給大家演示。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM