标题:从零开始实现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. ElasticStack学习(五):ElasticSearch索引与分词

    一.正排索引与倒排索引 1.什么是正排索引呢? 以一本书为例,一般在书的开始都会有书的目录,目录里面列举了一本书有哪些章节,大概有哪些内容,以及所对应的页码数.这样,我们在查找一些内容时,就可以通过目 ...

  2. c# override用法

    要扩展或修改继承的方法.属性.索引器或事件的抽象实现或虚实现,必须使用 override 修饰符. 在此例中,类 Square 必须提供 Area 的重写实现,因为 Area 是从抽象的 Shapes ...

  3. Linux 安装 lanmp

    Lanmp介绍 lanmp一键安装包是wdlinux官网2010年底开始推出的web应用环境的快速简易安装包. 执行一个脚本,整个环境就安装完成就可使用,快速,方便易用,安全稳定 lanmp一键安装包 ...

  4. Spring Cloud学习(一):Eureka服务注册与发现

    1.Eureka是什么 Eureka是Netflix开发的服务发现框架,本身是一个基于REST的服务,主要用于定位运行在AWS域中的中间层服务,以达到负载均衡和中间层服务故障转移的目的. Eureka ...

  5. 【EdgeBoard体验】开箱与上手

    简介 市面上基于嵌入式平台的神经网络加速平台有很多,今天给大家带来是百度大脑出品的EdgeBoard.按照官网文档的介绍,EdgeBoard是基于Xilinx Zynq Ultrascale+ MPS ...

  6. springcloud入门系列

    关于springcloud 1.写在前面 写着写这,不知不觉springcloud写了7,8篇了,今天把文章分下类,写下感受及后面的计划吧. (1)springcloud中最最重要的是eureka注册 ...

  7. 洛谷P3324 [SDOI2015]星际战争 题解

    题目链接: https://www.luogu.org/problemnew/show/P3324 分析: 因为本题的时间点较多,不能枚举,但发现有单调性,于是二分答案,二分使用的时间TTT 每个攻击 ...

  8. DML语言DDL

    DML(data manipulation language): 它们是SELECT.UPDATE.INSERT.DELETE,就象它的名字一样,这4条命令是用来对数据库里的数据进行操作的语言 . D ...

  9. koa2服务端使用jwt进行鉴权及路由权限分发

    大体思路 后端书写REST api时,有一些api是非常敏感的,比如获取用户个人信息,查看所有用户列表,修改密码等.如果不对这些api进行保护,那么别人就可以很容易地获取并调用这些 api 进行操作. ...

  10. C#3.0新增功能09 LINQ 基础02 LINQ 查询简介

    连载目录    [已更新最新开发文章,点击查看详细] 查询 是一种从数据源检索数据的表达式. 查询通常用专门的查询语言来表示. 随着时间的推移,人们已经为各种数据源开发了不同的语言:例如,用于关系数据 ...