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

標題:從零開始實現ASP.NET Core MVC的插件式開發(四) - 插件安裝
做者:Lamond Lu
地址:http://www.javashuo.com/article/p-ythsmsav-ha.html
源代碼:https://github.com/lamondlu/Mystiquehtml

前情回顧git

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

準備階段

建立數據庫

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

設計說明:這裏個人設計是將全部插件使用的數據庫表結構都安裝在主程序的數據庫中,暫時不考慮不一樣插件的數據庫表結構衝突,也不考慮插件升降級腳本的破壞性操做檢查,因此有相似問題的小夥伴能夠先假設插件之間的表結構沒有衝突,插件遷移腳本中也不會包含破壞主程序所需系統表的問題。數據庫

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

建立一個安裝包

爲了模擬安裝的效果,我決定將插件作成插件壓縮包,因此須要將以前的DemoPlugin1項目編譯後的文件以及一個plugin.json文件打包。安裝包的內容以下:c#

這裏暫時使用手動的方式來實現,後面我會建立一個Global Tools來完成這個操做。mvc

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

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

編碼階段

在建立完插件安裝包,並完成數據庫準備操做以後,咱們就能夠開始編碼了。ui

抽象插件邏輯

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

建立插件接口和插件基類

首先咱們須要將插件的概念抽象出來,因此這裏咱們首先定義一個插件接口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中給你們演示。

相關文章
相關標籤/搜索