标题:从零开始实现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. lleetcode 1 two sum c++

    Problem describe:https://leetcode.com/problems/two-sum/ Given an array of integers, return indices o ...

  2. .NetCore应用多个target framework

    <Project Sdk="Microsoft.NET.Sdk.Web"> <PropertyGroup> <TargetFrameworks> ...

  3. 附录:1-Grain生命周期-译注

    Grain Lifecycle Grains are logical entities that always exist, virtually, and have stable logical id ...

  4. 使用MyBatis在控制台动态打印执行的sql语句

    使用MyBatis进行数据库操作的时候,sql语句都是写在相应的mapper文件中,参数也是使用占位符取值表示的,mapper文件中看不到实时执行的完整sql,有时候sql语句错误或者参数类型不对的时 ...

  5. .NET多线程之线程安全,Lock(锁)、Monitor(同步访问)、LazyInitializer(延迟初始化)、Interlocked(原子操作)、static(静态)构造函数、volatile、

    1.什么是线程安全 线程安全是编程中的术语,指某个函数.函数库在多线程环境中被调用时,能够正确地处理多个线程之间的共享变量,使程序功能正确完成.一般来说,线程安全的函数应该为每个调用它的线程分配专门的 ...

  6. Flink实战(六) - Table API & SQL编程

    1 意义 1.1 分层的 APIs & 抽象层次 Flink提供三层API. 每个API在简洁性和表达性之间提供不同的权衡,并针对不同的用例. 而且Flink提供不同级别的抽象来开发流/批处理 ...

  7. Elasticsearch 7.x Nested 嵌套类型查询 | ES 干货

    一.什么是 ES Nested 嵌套 Elasticsearch 有很多数据类型,大致如下: 基本数据类型: string 类型.ES 7.x 中,string 类型会升级为:text 和 keywo ...

  8. 【带着canvas去流浪(13)】用Three.js制作简易的MARVEL片头动画(下)

    目录 一. 模型的制作 1.1 生成字体模型 1.2 多表面贴图 二. 镜头及动画 三. 大作业总结 示例代码托管在:http://www.github.com/dashnowords/blogs 博 ...

  9. 《VR入门系列教程》之21---使用Unity开发GearVR应用

    使用Unity开发GearVR应用     上一章我们介绍了如何运用Unity3D开发Oculus Rift应用,当然,这个便宜且强大的游戏引擎也可以用于GearVR的应用开发,这时我们需要用到Ocu ...

  10. 带新手玩转MVC——不讲道理就是干(下)

    带新手玩转MVC——不讲道理就是干(下) 前言:废话不多说,直接开干 完整案例演示 案例代码 LoginServlet package servlet; import domain.User; imp ...