从零开始实现ASP.NET Core MVC的插件式开发(四) - 插件安装
标题:从零开始实现ASP.NET Core MVC的插件式开发(四) - 插件安装
作者:Lamond Lu
地址:https://www.cnblogs.com/lwqlun/p/11343141.html
源代码:https://github.com/lamondlu/Mystique
前情回顾
- 从零开始实现ASP.NET Core MVC的插件式开发(一) - 使用Application Part动态加载控制器和视图
- 从零开始实现ASP.NET Core MVC的插件式开发(二) - 如何创建项目模板
- 从零开始实现ASP.NET Core MVC的插件式开发(三) - 如何在运行时启用组件
上一篇中,我们针对运行时启用/禁用组件做了一些尝试,最终我们发现借助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个接口方法MigrationUp
和MigrationDown
来完成插件升级和降级的功能。
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());
}
}
代码解释
- 这里的
WriteMigrationScripts
和RemoveMigrationScripts
的作用是用来将插件升级和降级的迁移脚本的保存到数据库中。因为我并不想每一次都通过加载程序集的方式读取迁移脚本,所以这里在安装插件时,我会将每个插件版本的迁移脚本导入到数据库中。 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);
}
}
代码解释:
- 这里我们通过实现
MigrationUp
和MigrationDown
方法来完成新表的创建和删除,当然本文只实现了插件的安装,并不涉及删除或降级,这部分代码在后续文章中会被使用。 - 这里注意在运行升级脚本之后,会将当前插件版本的升降级脚本通过
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中给大家演示。
从零开始实现ASP.NET Core MVC的插件式开发(四) - 插件安装的更多相关文章
- 从零开始实现ASP.NET Core MVC的插件式开发(五) - 插件的删除和升级
标题:从零开始实现ASP.NET Core MVC的插件式开发(五) - 使用AssemblyLoadContext实现插件的升级和删除 作者:Lamond Lu 地址:https://www.cnb ...
- 从零开始实现ASP.NET Core MVC的插件式开发(六) - 如何加载插件引用
标题:从零开始实现ASP.NET Core MVC的插件式开发(六) - 如何加载插件引用. 作者:Lamond Lu 地址:https://www.cnblogs.com/lwqlun/p/1171 ...
- 从零开始实现ASP.NET Core MVC的插件式开发(七) - 近期问题汇总及部分解决方案
标题:从零开始实现ASP.NET Core MVC的插件式开发(七) - 问题汇总及部分解决方案 作者:Lamond Lu 地址:https://www.cnblogs.com/lwqlun/p/12 ...
- 从零开始实现ASP.NET Core MVC的插件式开发(八) - Razor视图相关问题及解决方案
标题:从零开始实现ASP.NET Core MVC的插件式开发(八) - Razor视图相关问题及解决方案 作者:Lamond Lu 地址:https://www.cnblogs.com/lwqlun ...
- 从零开始实现ASP.NET Core MVC的插件式开发(九) - 升级.NET 5及启用预编译视图
标题:从零开始实现ASP.NET Core MVC的插件式开发(九) - 如何启用预编译视图 作者:Lamond Lu 地址:https://www.cnblogs.com/lwqlun/p/1399 ...
- 从零开始实现ASP.NET Core MVC的插件式开发(一) - 使用ApplicationPart动态加载控制器和视图
标题:从零开始实现ASP.NET Core MVC的插件式开发(一) - 使用Application Part动态加载控制器和视图 作者:Lamond Lu 地址:http://www.cnblogs ...
- 从零开始实现ASP.NET Core MVC的插件式开发(二) - 如何创建项目模板
标题:从零开始实现ASP.NET Core MVC的插件式开发(二) - 如何创建项目模板 作者:Lamond Lu 地址:https://www.cnblogs.com/lwqlun/p/11155 ...
- 从零开始实现ASP.NET Core MVC的插件式开发(三) - 如何在运行时启用组件
标题:从零开始实现ASP.NET Core MVC的插件式开发(三) - 如何在运行时启用组件 作者:Lamond Lu 地址:https://www.cnblogs.com/lwqlun/p/112 ...
- 使用 ASP.NET Core MVC 创建 Web API(四)
使用 ASP.NET Core MVC 创建 Web API 使用 ASP.NET Core MVC 创建 Web API(一) 使用 ASP.NET Core MVC 创建 Web API(二) 使 ...
随机推荐
- django基础知识之HttpReqeust对象:
HttpReqeust对象 服务器接收到http协议的请求后,会根据报文创建HttpRequest对象 视图函数的第一个参数是HttpRequest对象 在django.http模块中定义了HttpR ...
- 关于AndroidStudio在编译时无法解析和拉取依赖的问题和无法访问Jcenter服务器的问题
问题描述:在编译时出现如下错误:Unknown host 'd29vzk4ow07wi7.cloudfront.net'. You may need to adjust the....一般是被墙了.偶 ...
- c++ 二分答案
c++ 二分答案 问题 使得x^x达到或超过n位数字的最小正整数x是多少?n<=2000000000 分析 对与这种较难求解的问题,我们很难想出较好的解决策略.但是,我们至少知道答案一定在1与2 ...
- 如何编写无须人工干预的shell脚本
在使用基本的一些shell命令时,机器需要与人进行互动来确定命令的执行.比如 cp test.txt boo/test.txt,会询问是否覆盖?ssh远程登陆时,需要输入人工密码后,才可以继续执行ss ...
- wincc C脚本如何调用第三方动态链接库dll
就Wincc本身脚本功能而言并不强大,但是wincc 脚本提供了第三方接口,如通用的Kernel32.dll,User32.dll,Gdi32.dll,大家如果对这些API接口感兴趣,可网上查找关于w ...
- Gin 框架 - 安装和路由配置
目录 概述 Gin 安装 路由配置 推荐阅读 概述 看下 Gin 框架的官方介绍: Gin 是一个用 Go (Golang) 编写的 web 框架. 它是一个类似于 martini 但拥有更好性能的 ...
- C语言指针专题——指针怎么就很灵活?
最近在研读 C Primer pkus(第五版)中文版,老外写的还是很经典的,推荐给朋友们,购买地址:C primer plus 5版中文版购买 另外再推荐两本书: 1. 2017年9月全国计算机二级 ...
- spark 源码分析之十六 -- Spark内存存储剖析
上篇spark 源码分析之十五 -- Spark内存管理剖析 讲解了Spark的内存管理机制,主要是MemoryManager的内容.跟Spark的内存管理机制最密切相关的就是内存存储,本篇文章主要介 ...
- 后端 - Lession 01 PHP 基础
目录 Lession 01 php 基础 1. php 基础 2. php 变量 3. php 单引号 和 双引号区别 4. 数据类型 5. 数据类型转换 6. 常量 7. 运算符 8. 为 fals ...
- html+css-->background-img(背景图的设置)
背景图:(相关验证代码请查看代码,在验证时需将当前不需要验证的代码注释掉) 1.inherit:从父元素继承属性设置 2.background-repeat:平铺(在图片大小小于元素尺寸时 ...