標(biāo)題:從零開始實現(xiàn)ASP.NET Core MVC的插件式開發(fā)(四) - 插件安裝
作者:Lamond Lu
地址:https://www.cnblogs.com/lwqlun/p/11343141.html
<https://www.cnblogs.com/lwqlun/p/11343141.html>
源代碼:https://github.com/lamondlu/DynamicPlugins
<https://github.com/lamondlu/DynamicPlugins>
前情回顧
* 從零開始實現(xiàn)ASP.NET Core MVC的插件式開發(fā)(一) - 使用Application Part動態(tài)加載控制器和視圖
<https://www.cnblogs.com/lwqlun/p/11137788.html#4310745>
* 從零開始實現(xiàn)ASP.NET Core MVC的插件式開發(fā)(二) - 如何創(chuàng)建項目模板
<https://www.cnblogs.com/lwqlun/p/11155666.html>
* 從零開始實現(xiàn)ASP.NET Core MVC的插件式開發(fā)(三) - 如何在運行時啟用組件
<https://www.cnblogs.com/lwqlun/p/11260750.html>
上一篇中,我們針對運行時啟用/禁用組件做了一些嘗試,最終我們發(fā)現(xiàn)借助IActionDescriptorChangeProvider
可以幫助我們實現(xiàn)所需的功能。本篇呢,我們就來繼續(xù)研究如何完成插件的安裝,畢竟之前的組件都是我們預(yù)先放到主程序中的,這樣并不是一種很好的安裝插件方式。
準(zhǔn)備階段
創(chuàng)建數(shù)據(jù)庫
為了完成插件的安裝,我們首先需要為主程序創(chuàng)建一個數(shù)據(jù)庫,來保存插件信息。 這里為了簡化邏輯,我只創(chuàng)建了2個表,Plugins表是用來記錄插件信息的,
PluginMigrations表是用來記錄插件每個版本的升級和降級腳本的。
設(shè)計說明:這里我的設(shè)計是將所有插件使用的數(shù)據(jù)庫表結(jié)構(gòu)都安裝在主程序的數(shù)據(jù)庫中,暫時不考慮不同插件的數(shù)據(jù)庫表結(jié)構(gòu)沖突,也不考慮插件升降級腳本的破壞性操作檢查,所以有類似問題的小伙伴可以先假設(shè)插件之間的表結(jié)構(gòu)沒有沖突,插件遷移腳本中也不會包含破壞主程序所需系統(tǒng)表的問題。
備注:數(shù)據(jù)庫腳本可查看源代碼的DynamicPlugins.Database項目
創(chuàng)建一個安裝包
為了模擬安裝的效果,我決定將插件做成插件壓縮包,所以需要將之前的DemoPlugin1項目編譯后的文件以及一個plugin.json
文件打包。安裝包的內(nèi)容如下:
這里暫時使用手動的方式來實現(xiàn),后面我會創(chuàng)建一個Global Tools來完成這個操作。
在plugin.json文件中記錄當(dāng)前插件的一些元信息,例如插件名稱,版本等。
{ "name": "DemoPlugin1", "uniqueKey": "DemoPlugin1", "displayName":"Lamond
Test Plugin1", "version": "1.0.0" }
編碼階段
在創(chuàng)建完插件安裝包,并完成數(shù)據(jù)庫準(zhǔn)備操作之后,我們就可以開始編碼了。
抽象插件邏輯
為了項目擴展,我們需要針對當(dāng)前業(yè)務(wù)進(jìn)行一些抽象和建模。
創(chuàng)建插件接口和插件基類
首先我們需要將插件的概念抽象出來,所以這里我們首先定義一個插件接口IModule以及一個通用的插件基類ModuleBase。
IModule.cs
public interface IModule { string Name { get; } DomainModel.Version Version {
get; } }
在IModule接口中我們定義了當(dāng)前插件的名稱和插件的版本號。
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類實現(xiàn)了IModule接口,并進(jìn)行了一些初始化的操作。后續(xù)的插件類都需要繼承ModuleBase類。
解析插件配置
為了完成插件包的解析,這里我創(chuàng)建了一個PluginPackage類,其中封裝了插件包的相關(guān)操作。
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類來進(jìn)行解壓縮,解壓縮之后,程序會嘗試讀取臨時解壓目錄中的plugin.json
文件,如果文件不存在,就會報出異常。
* 如果主程序中沒有當(dāng)前插件,就會解壓到定義好的插件目錄中。(這里暫時不考慮插件升級,下一篇中會做進(jìn)一步說明)
* GetAllMigrations方法的作用是從程序集中加載當(dāng)前插件所有的遷移腳本。
新增腳本遷移功能
為了讓插件在安裝時,自動實現(xiàn)數(shù)據(jù)庫表的創(chuàng)建,這里我還添加了一個腳本遷移機制,這個機制類似于EF的腳本遷移,以及之前分享過的FluentMigrator遷移。
這里我們定義了一個遷移接口IMigration, 并在其中定義了2個接口方法MigrationUp和MigrationDown來完成插件升級和降級的功能。
public interface IMigration { DomainModel.Version Version { get; } void
MigrationUp(Guid pluginId); void MigrationDown(Guid pluginId); }
然后我們實現(xiàn)了一個遷移腳本基類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()); } }
代碼解釋
* 這里的WriteMigrationScripts和RemoveMigrationScripts
的作用是用來將插件升級和降級的遷移腳本的保存到數(shù)據(jù)庫中。因為我并不想每一次都通過加載程序集的方式讀取遷移腳本,所以這里在安裝插件時,我會將每個插件版本的遷移腳本導(dǎo)入到數(shù)據(jù)庫中。
* SQL方法是用來運行遷移腳本的,這里為了簡化代碼,缺少了事務(wù)處理,有興趣的同學(xué)可以自行添加。
為之前的腳本添加遷移程序
這里我們假設(shè)安裝DemoPlugin1插件1.0.0版本之后,需要在主程序的數(shù)據(jù)庫中添加一個名為Test的表。
根據(jù)以上需求,我添加了一個初始的腳本遷移類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); } }
代碼解釋:
* 這里我們通過實現(xiàn)MigrationUp和MigrationDown
方法來完成新表的創(chuàng)建和刪除,當(dāng)然本文只實現(xiàn)了插件的安裝,并不涉及刪除或降級,這部分代碼在后續(xù)文章中會被使用。
* 這里注意在運行升級腳本之后,會將當(dāng)前插件版本的升降級腳本通過base.WriteMigrationScripts方法保存到數(shù)據(jù)庫。
添加安裝插件包的業(yè)務(wù)處理類
為了完成插件包的安裝邏輯,這里我創(chuàng)建了一個PluginManager類, 其中AddPlugins方法使用來進(jìn)行插件安裝的。
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即包含了插件包的所有信息
* 這里我們首先將插件的信息,通過工作單元保存到了數(shù)據(jù)庫
* 保存成功之后,我通過pluginPackage對象,獲取了當(dāng)前插件包中所包含的所有遷移腳本,并依次運行這些腳本來完成數(shù)據(jù)庫的遷移。
在主站點中添加插件管理界面
這里為了管理插件,我在主站點中創(chuàng)建了2個新頁面,插件列表頁以及添加新插件頁面。這2個頁面的功能非常的簡單,這里我就不進(jìn)一步介紹了,大部分的處理都是復(fù)用了之前的代碼,例如插件的安裝,啟用和禁用,相關(guān)的代碼大家可以自行查看。
設(shè)置已安裝插件默認(rèn)啟動
在完成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); } } }
設(shè)置完成之后,整個插件的安裝編碼就告一段落了。
最終效果
總結(jié)以及待解決的問題
本篇中,我給大家分享了如果將打包的插件安裝到系統(tǒng)中,并完成對應(yīng)的腳本遷移。不過在本篇中,我們只完成了插件的安裝,針對插件的刪除,以及插件的升降級我們還未解決,有興趣的同學(xué),可以自行嘗試一下,你會發(fā)現(xiàn)在.NET
Core 2.2版本,我們沒有任何在運行時Unload程序集能力,所以在從下一篇開始,我將把當(dāng)前項目的開發(fā)環(huán)境升級到.NET Core 3.0
Preview, 針對插件的刪除和升降級我將在.NET Core 3.0中給大家演示。
熱門工具 換一換
感谢您访问我们的网站,您可能还对以下资源感兴趣:
调教肉文小说-国产成本人片免费av-空姐av种子无码-在线观看免费午夜视频-综合久久精品激情-国产成人丝袜视频在线观看软件-大芭区三区四区无码-啊啊好爽啊啊插啊用力啊啊-wanch视频网-国产精品成人a免费观看