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

标题:从零开始实现ASP.NET Core MVC的插件式开发(四) - 插件安装
做者:Lamond Lu
地址:http://www.javashuo.com/article/p-ythsmsav-ha.html
源代码:https://github.com/lamondlu/Mystiquehtml

前情回顾git

上一篇中,咱们针对运行时启用/禁用组件作了一些尝试,最终咱们发现借助IActionDescriptorChangeProvider能够帮助咱们实现所需的功能。本篇呢,咱们就来继续研究如何完成插件的安装,毕竟以前的组件都是咱们预先放到主程序中的,这样并非一种很好的安装插件方式。github

准备阶段

建立数据库

为了完成插件的安装,咱们首先须要为主程序建立一个数据库,来保存插件信息。 这里为了简化逻辑,我只建立了2个表,Plugins表是用来记录插件信息的,PluginMigrations表是用来记录插件每一个版本的升级和降级脚本的。sql

设计说明:这里个人设计是将全部插件使用的数据库表结构都安装在主程序的数据库中,暂时不考虑不一样插件的数据库表结构冲突,也不考虑插件升降级脚本的破坏性操做检查,因此有相似问题的小伙伴能够先假设插件之间的表结构没有冲突,插件迁移脚本中也不会包含破坏主程序所需系统表的问题。数据库

备注:数据库脚本可查看源代码的DynamicPlugins.Database项目json

建立一个安装包

为了模拟安装的效果,我决定将插件作成插件压缩包,因此须要将以前的DemoPlugin1项目编译后的文件以及一个plugin.json文件打包。安装包的内容以下:c#

这里暂时使用手动的方式来实现,后面我会建立一个Global Tools来完成这个操做。mvc

在plugin.json文件中记录当前插件的一些元信息,例如插件名称,版本等。ide

{
    "name": "DemoPlugin1",
    "uniqueKey": "DemoPlugin1",
    "displayName":"Lamond Test Plugin1",
    "version": "1.0.0"
}

编码阶段

在建立完插件安装包,并完成数据库准备操做以后,咱们就能够开始编码了。ui

抽象插件逻辑

为了项目扩展,咱们须要针对当前业务进行一些抽象和建模。

建立插件接口和插件基类

首先咱们须要将插件的概念抽象出来,因此这里咱们首先定义一个插件接口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中给你们演示。

相关文章
相关标签/搜索