标题:从零开始实现ASP.NET Core MVC的插件式开发(四) - 插件安装

作者:Lamond Lu

地址:https://www.cnblogs.com/lwqlun/p/11343141.html

源代码:https://github.com/lamondlu/Mystique

前情回顾

上一篇中,我们针对运行时启用/禁用组件做了一些尝试,最终我们发现借助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个接口方法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中给大家演示。

从零开始实现ASP.NET Core MVC的插件式开发(四) - 插件安装的更多相关文章

  1. 从零开始实现ASP.NET Core MVC的插件式开发(五) - 插件的删除和升级

    标题:从零开始实现ASP.NET Core MVC的插件式开发(五) - 使用AssemblyLoadContext实现插件的升级和删除 作者:Lamond Lu 地址:https://www.cnb ...

  2. 从零开始实现ASP.NET Core MVC的插件式开发(六) - 如何加载插件引用

    标题:从零开始实现ASP.NET Core MVC的插件式开发(六) - 如何加载插件引用. 作者:Lamond Lu 地址:https://www.cnblogs.com/lwqlun/p/1171 ...

  3. 从零开始实现ASP.NET Core MVC的插件式开发(七) - 近期问题汇总及部分解决方案

    标题:从零开始实现ASP.NET Core MVC的插件式开发(七) - 问题汇总及部分解决方案 作者:Lamond Lu 地址:https://www.cnblogs.com/lwqlun/p/12 ...

  4. 从零开始实现ASP.NET Core MVC的插件式开发(八) - Razor视图相关问题及解决方案

    标题:从零开始实现ASP.NET Core MVC的插件式开发(八) - Razor视图相关问题及解决方案 作者:Lamond Lu 地址:https://www.cnblogs.com/lwqlun ...

  5. 从零开始实现ASP.NET Core MVC的插件式开发(九) - 升级.NET 5及启用预编译视图

    标题:从零开始实现ASP.NET Core MVC的插件式开发(九) - 如何启用预编译视图 作者:Lamond Lu 地址:https://www.cnblogs.com/lwqlun/p/1399 ...

  6. 从零开始实现ASP.NET Core MVC的插件式开发(一) - 使用ApplicationPart动态加载控制器和视图

    标题:从零开始实现ASP.NET Core MVC的插件式开发(一) - 使用Application Part动态加载控制器和视图 作者:Lamond Lu 地址:http://www.cnblogs ...

  7. 从零开始实现ASP.NET Core MVC的插件式开发(二) - 如何创建项目模板

    标题:从零开始实现ASP.NET Core MVC的插件式开发(二) - 如何创建项目模板 作者:Lamond Lu 地址:https://www.cnblogs.com/lwqlun/p/11155 ...

  8. 从零开始实现ASP.NET Core MVC的插件式开发(三) - 如何在运行时启用组件

    标题:从零开始实现ASP.NET Core MVC的插件式开发(三) - 如何在运行时启用组件 作者:Lamond Lu 地址:https://www.cnblogs.com/lwqlun/p/112 ...

  9. 使用 ASP.NET Core MVC 创建 Web API(四)

    使用 ASP.NET Core MVC 创建 Web API 使用 ASP.NET Core MVC 创建 Web API(一) 使用 ASP.NET Core MVC 创建 Web API(二) 使 ...

随机推荐

  1. Django中使用JS通过DataTable实现表格前端分页,每页显示页数,搜索等功能

    Django架构中自带了后端分页的技术,通过Paginator进行分页,前端点击按钮提交后台进行页面切换. 优缺点:后端分页对于数据量大的场景有其优势,但页面切换比较慢. 后端分页python3代码如 ...

  2. bzoj3316 JC loves Mkk题解

    3316: JC loves Mkk Time Limit: 5 Sec  Memory Limit: 64 MBSubmit: 979  Solved: 316[Submit][Status][Di ...

  3. NOIP 2004 虫食算题解

    问题 E: [Noip2004]虫食算 时间限制: 1 Sec  内存限制: 128 MB 题目描述 所谓虫食算,就是原先的算式中有一部分被虫子啃掉了,需要我们根据剩下的数字来判定被啃掉的字母.来看一 ...

  4. 理解vue-loader

    事情的起源是被人问到,一个以.vue结尾的文件,是如何被编译然后运行在浏览器中的?突然发现,对这一块模糊的很,而且看mpvue的文档,甚至小程序之类的都是实现了自己的loader,所以十分必要抽时间去 ...

  5. ~~在python中踩过的坑以及问题~~(不断更新)

    python说简单也不难,但是在这其中大大小小的点 真的是有够折磨人欸!  1.   input 输入的时候,即使输入的是数字,数据类型也是字符串   2.   字符串本质上来看可以看作有序数组  3 ...

  6. yum只下载不安装软件包

    一.通过yum自带的工具yumdownloader [root@host---- interpreter]# rpm -ql yum-utils package yum-utils is not in ...

  7. .NET CORE 多语言实现方案

    根据市场需求,基于.NET CORE平台开发的RoadFlow工作流平台需要实现多语言版本.经过方案讨论和比对,决定采用.NET自带的本地化功能来实现多语言.话不多说,直接上实现方式. 首先修改Sta ...

  8. 6.1.初识Flutter应用之实现一个计数器

    用Android Studio和VS Code创建的Flutter应用模板是一个简单的计数器示例,本节先仔细讲解一下这个计数器Demo的源码,让读者对Flutter应用程序结构有个基本了解,在随后小节 ...

  9. C#后台HttpWebRequest模拟跨域Ajax请求,注册Windows服务到服务器上

    项目需求,暂且叫A.B公司吧.我们公司需要从A公司哪里读取机器上的数据,放到我们数据库中.然后再将数据库中存的数据,提供一个接口,B公司来调用,大概这个意思. 好了,言归正传.这个是之前做好的界面,用 ...

  10. 微信小程序全局状态管理 wxscv

    微信小程序中,数据状态不同页面中不能跨页面同步更新,也就是缺失类似vuex,mobx,redux全局的数据状态管理功能. 有些人移植了这些库,但是毕竟不是微信小程序生态的东西. Tencent也发布了 ...