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

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

65831-20190812224744047-709725655.jpg

前情回顧

  • 從零開始實現ASP.NET Core MVC的插件式開發(一) - 使用Application Part動態加載控制器和視圖
  • 從零開始實現ASP.NET Core MVC的插件式開發(二) - 如何創建項目模板
  • 從零開始實現ASP.NET Core MVC的插件式開發(三) - 如何在運行時啟用組件

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

準備階段

創建數據庫

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

65831-20190812224754255-1841432938.png

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

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

創建一個安裝包

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

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

65831-20190812224810950-1336822430.png

在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個頁面的功能非常的簡單,這里我就不進一步介紹了,大部分的處理都是復用了之前的代碼,例如插件的安裝,啟用和禁用,相關的代碼大家可以自行查看。

65831-20190812224832286-1246070962.png
65831-20190813060855711-1765272294.png

設置已安裝插件默認啟動

在完成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);}}   }

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

最終效果

65831-20190812224842508-1192748458.gif

總結以及待解決的問題

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

轉載于:https://www.cnblogs.com/lwqlun/p/11343141.html

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/248247.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/248247.shtml
英文地址,請注明出處:http://en.pswp.cn/news/248247.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

立體導航翻轉案例

<div class"box"><!-- 立方體 --><ul><li><img src"img1/1.jpg" alt""></li><li><img src"img1/2.jpg" alt""></li><li><img src"img1/3.jpg" a…

Uncontrolled memory mapping in camera driver (CVE-2013-2595)

版權聲明&#xff1a;本文為博主原創文章&#xff0c;未經博主同意不得轉載。https://blog.csdn.net/hu3167343/article/details/34434235 /* 本文章由 莫灰灰 編寫&#xff0c;轉載請注明出處。 作者&#xff1a;莫灰灰 郵箱&#xff1a; minzhenfei163.com */ 1漏洞描寫…

表格隔行變色

<body><table border"0" align"center" cellspacing"1" cellpadding"0"><caption>恭喜發財</caption><thead><tr><th>代碼</th><th>名稱</th><th>最新公布凈值<…

啟動Cognos時報0106錯誤

1. 問題描述 啟動Cognos失敗&#xff0c;報錯代碼為0106。 2. 問題分析 是jdk版本不兼容。 3. 解決方案 方案一&#xff1a;更換jdk1.6&#xff0c;可以使用免安裝版&#xff0c;不需要卸載原有的jdk將java_home的路徑替換成jdk1.6的路徑。 方案二&#xff1a;使用Cognos自帶jd…

項目管理的測試版發布

最近有時間將以前沒有寫完的項目管理程序進一步完善&#xff0c;加入了項目任務之間的關連。功能&#xff1a;1、任務的關連Start to finishStart to startFinish to startFinish to finish2、任務關連表環路檢測3、采用MVC模式進行開發4、自動導出XML5、雙擊連接線可以設置、刪…

劍指offer.機器人的運動范圍

地上有一個 m 行和 n 列的方格&#xff0c;橫縱坐標范圍分別是 0~m?1 和 0~~n?1。一個機器人從坐標0,0的格子開始移動&#xff0c;每一次只能向左&#xff0c;右&#xff0c;上&#xff0c;下四個方向移動一格。但是不能進入行坐標和列坐標的數位之和大于 kk 的格子。請問…

Tab欄切換布局分析

<body><div class"tab"><div class"tab_list"><ul><li class"current">商品介紹</li><li>規格與包裝</li><li>售后包裝</li><li>商品評價(50000)</li><li>手機社…

CLR基礎,CLR運行過程,使用dos命令創建、編譯、運行C#文件,查看IL代碼

CLR是Common Language Runtime的縮寫&#xff0c;是.NET程序集或可執行程序運行的一個虛擬環境。CLR用于管理托管代碼&#xff0c;但是它本身是由非托管代碼編寫的&#xff0c;并不是一個包含了托管代碼的程序集&#xff0c;所以不能使用IL DASM進行查看&#xff0c;但CLR以dll…

表單的全選取消全選

<div class"wrap"><table border"1" cellspacing"0" cellpadding"0"><caption>恭喜發財</caption><thead><tr><th><input type"checkbox" id"j_cbAll" checked&quo…

VUE 數據綁定模塊渲染 computed(實現通過路由id 查詢數據json結構,對應的值來放在面包屑中)...

異步請求的值放在vuex中&#xff0c;然后頁面掛載該數據和渲染頁面 computed 計算屬性是基于它的依賴緩存的。計算屬性在它的相關依賴發生改變時會重新取值&#xff0c;所以當ajax請求回來的數據發生變化時&#xff0c;計算屬性的值會進行更新&#xff0c;相關的模板引用也會重…

ThinkJs筆記瑣碎

ThinkJs筆記瑣碎 記錄一些瑣碎的在使用ThinkJs遇到的問題 靜態資源訪問 ThinkJs默認production環境關閉對www下資源的相對路徑的訪問&#xff0c;官方建議通過nginx轉向的地址的絕對路徑訪問&#xff0c;想要在production環境訪問相對路徑的話需要到src/config/middleware.js里…

js(Dom+Bom)第二天(1)

JavaScript-DOM操作 核心知識點 className操作標簽樣式style屬性方式操作標簽樣式操作表單控件 學習目標 能夠通過className方式給標簽設置樣式能夠通過style方式給標簽設置樣式能夠獲取表單控件中的值 操作元素樣式 語法&#xff1a;1.dom.className 類名;2.dom.style.屬…

HDU 4339 Query

算法: 比賽時沒有想到好的算法&#xff0c;暴力是O&#xff08; Q * N &#xff09;肯定超時。 賽后&#xff0c;線段樹&#xff0c;樹狀數組&#xff0c;HASH都能AC&#xff0c;想了下&#xff0c;的確用樹狀數組 時間復雜度就可以優化到O&#xff08;Q * lgN * lgN) 2000msAC…

201904快速閱讀術

在看過了幾本數之后&#xff0c;發現原來培養讀書的習慣好像也不太難&#xff0c;“將讀書融入生活&#xff0c;框定讀書時間” 生活中&#xff0c;我確實也是這樣執行了。利用每天上下班的時間聽書&#xff0c;有些覺得可以讀快的書籍用了1.5倍速度在聽&#xff0c;難懂的部分…

js(Dom+Bom)第二天(2)

webAPI 00-操作圖片 知識點-獲取圖片src屬性 圖片對象.src ----> 獲取圖片路徑注意: 1. 獲取到的圖片路徑是一個絕對路徑知識點-動態的給圖片標簽設置路徑 圖片對象.src 圖片路徑;注意: 1.可以設置絕對路徑(不推薦) 2.可以設置相對路徑課堂案例-切換圖片案例 01-操作標…

javaScript今日總結

javascript簡單介紹ECMAScript 1.語法 2.變量&#xff1a;只能使用var定義&#xff0c;如果在函數的內容使用var定義&#xff0c;那么它是一個局部變量&#xff0c;如果沒有使用var它是一個全局的。弱類型&#xff01; 3.數據類型&#xff1a;原始數據類型(undefined/null/stri…

使用Connector / Python連接MySQL/查詢數據

使用Connector / Python連接MySQL connect()構造函數創建到MySQL服務器的連接并返回一個 MySQLConnection對象 在python中有以下幾種方法可以連接到MySQL數據庫&#xff1a; 1.使用connect&#xff08;&#xff09;構造函數import mysql.connectorcnx mysql.connector.connect…

最簡方式 表格編輯 基于 el-table

共下面5點1.新增一個顯示和隱藏的參數2.在顯示那邊新增一個input框&#xff0c;用v-model綁定數據&#xff0c;用v-if來顯示和隱藏3.給之前的顯示的span標簽添加v-else 和上面形成if else4.編輯和保存按鈕同理&#xff0c;然后編輯按鈕觸發的任務將所有輸入打開。即seen置為tru…

js(Dom+Bom)第三天(1)

JavaScript-DOM 節點的層次結構 hasChildNodes() 【父元素中是否包含子節點】 dom.hasChildNodes() 總結&#xff1a;1.該方法返回的是一個布爾類型的結果用來判斷當前元素中是否存在子節點。2.該方法會將元素中所有的節點都獲取&#xff08;包括空格&#xff0c;回車符&#…

Spring Boot 自動配置原理

自動配置原理配置文件到底能寫什么&#xff1f;怎么寫&#xff1f;自動配置原理&#xff1b; 參考&#xff1a;https://docs.spring.io/spring-boot/docs/1.5.9.RELEASE/reference/htmlsingle/#common-application-properties配置文件能配置的屬性參照1、自動配置原理&#xff…