从零开始实现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.mzph.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>最新公布净值<…

项目管理的测试版发布

最近有时间将以前没有写完的项目管理程序进一步完善&#xff0c;加入了项目任务之间的关连。功能&#xff1a;1、任务的关连Start to finishStart to startFinish to startFinish to finish2、任务关连表环路检测3、采用MVC模式进行开发4、自动导出XML5、双击连接线可以设置、删…

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…

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;难懂的部分…

最简方式 表格编辑 基于 el-table

共下面5点1.新增一个显示和隐藏的参数2.在显示那边新增一个input框&#xff0c;用v-model绑定数据&#xff0c;用v-if来显示和隐藏3.给之前的显示的span标签添加v-else 和上面形成if else4.编辑和保存按钮同理&#xff0c;然后编辑按钮触发的任务将所有输入打开。即seen置为tru…

Spring Boot 自动配置原理

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

C#Socket编程详解(一)TCP与UDP简介

一、TCP与UDP&#xff08;转载&#xff09; 1、TCP 1.1 定义 TCP&#xff08;TransmissionControl Protocol&#xff09;传输控制协议。 是一种可靠的、面向连接的协议&#xff08;eg:打电话&#xff09;、传输效率低全双工通信&#xff08;发送缓存&接收缓存&#xff09;、…

动态创建表格数据

<input type"button" value"创建"><style>*{margin: 0;padding: 0;}table{width: 980px;margin: 50px auto;}table,th,tr,td{text-align: center;border: 1px solid #ccc;}</style><script>var heads [姓名, 年龄, 性别, 学号, 薪…

第四节:EF Core的并发处理

1.说明 和EF版本的并发处理方案一致&#xff0c;需要知道乐观并发和悲观并发的区别&#xff0c;EF Core只支持乐观并发&#xff1b;监控并发的两种方案&#xff1a;监测单个字段和监测整条数据&#xff0c;DataAnnotations 和 FluentApi的两种配置方式。 &#xff08;PS&#x…

JS中的prototype

JS中的phototype是JS中比较难理解的一个部分(转自出处&#xff1a;&#xff08;http://www.cnblogs.com/yjf512/&#xff09;) 本文基于下面几个知识点: 1 原型法设计模式 在.Net中可以使用clone()来实现原型法 原型法的主要思想是&#xff0c;现在有1个类A,我想要创建一个类B,…

微博发布案例

推荐在写动态生成标签数据的时候&#xff0c;提前写一遍htmlcss的结构&#xff0c;方便提供写照模板 <div class"box"><!-- 顶部搜索框 --><div class"inputBox"><textarea maxlength"200"></textarea></div&…

1.3 Go语言基础之数据类型

Go语言中有丰富的数据类型&#xff0c;除了基本的整型、浮点型、布尔型、字符串外&#xff0c;还有数组、切片、结构体、函数、map、通道&#xff08;channel&#xff09;等。Go 语言的基本类型和其他语言大同小异。 一、整型 1.1 基本类型 整型分为以下两个大类&#xff1a; 按…

【网络安全】关于ARP攻击的原理以及在Kali Linux环境下的实现

转自&#xff1a;https://www.cnblogs.com/rebrust/p/6096101.html 全文摘要 本文讲述内容分为两部分&#xff0c;前半部分讲述ARP协议及ARP攻击原理&#xff0c;后半部分讲述在Kali Linux环境下如何实现ARP攻击以及ARP欺骗&#xff0c;如果对于ARP攻击的背景和原理不感兴趣的话…

动态创建英雄图片

推荐在写动态生成标签数据的时候&#xff0c;提前写一遍htmlcss的结构&#xff0c;方便提供写照模板 <input type"button" value"按钮"><style type"text/css">* {margin: 0;padding: 0;list-style: none;}ul {width: 600px;margin…

第五节:EF Core中的三类事务(SaveChanges、DbContextTransaction、TransactionScope)

一. 说明 EF版本的事务介绍详见&#xff1a; 第七节: EF的三种事务的应用场景和各自注意的问题(SaveChanges、DBContextTransaction、TransactionScope)。 本节主要介绍EF Core下的三种事务的用法和各自的使用场景&#xff0c;其中SaveChanges和DBContextTransaction事务与EF版…