Plug-in development of ASP.NET Core MVC(1) – Dynamically load controllers and views using ApplicationPart

Plug-in development of ASP.NET Core MVC(2) How to create a project template

Plug-in development of ASP.NET Core MVC(3) – How to enable components at runtime

Plug-in development of ASP.NET Core MVC(4) – Plugin installation

Plug-in development of ASP.NET Core MVC(5)- Plugin removal and upgrade

Plug-in development of ASP.NET Core MVC(6) – How to load plugin references

Source:
HTTPS: / /github.com/lamondlu/Mystique

In the previous article, we did some experiments on the runtime enable/disable components, and finally we found that we IActionDescriptorChangeProvidercan help us achieve the required functionality. In this article, we will continue to study how to complete the installation of the plug-in. After all, the previous components are pre-released into the main program, which is not a good way to install plug-ins.

Preparation stage

Create database

In order to complete the installation of the plugin, we first need to create a database for the main program to save the plugin information. Here to simplify the logic, I only created two tables, the Pluginstable is used to record plug-in information, the PluginMigrationstable is used to record the upgrade and downgrade script for each version of the plug-in.

 

 

Design Description: My design here is to install the database table structure used by all plugins in the database of the main program, temporarily do not consider the database table structure conflict of different plugins, and do not consider the destructive operation check of the plug-in upgrade script, so A small partner with similar problems can assume that there is no conflict in the table structure between the plugins, and the plugin migration script will not contain the problem of destroying the system tables required by the main program.

Note: Database scripts can view the source code of the DynamicPlugins.Databaseproject

Create an installation package

In order to simulate the effect of the installation, I decided to make the plugin into a plugin compression package, so I need to package the compiled files and a plugin.jsonfile of the previous DemoPlugin1 project . The contents of the installation package are as follows:

This is temporarily done manually, and I will create a Global Tools to do this later.


Plug-in_development_of_ASP.NET_Core_MVC(4)_-_Plugin_installation_2.png

 

Record some meta information of the current plugin in the plugin.json file, such as the plugin name, version, and so on.

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

Coding stage

After creating the plugin installation package and completing the database preparation, we can start coding.

Abstract plugin logic

For project expansion, we need to do some abstraction and modeling for the current business.

Create plugin interface and plugin base class

First we need to abstract the concept of the plugin, so here we first define a plugin interface IModuleand a generic plugin base class ModuleBase.

Module.cs

Copy    public interface IModule
    {
        string Name { get; }

        DomainModel.Version Version { get; }
    }

In the IModuleinterface we define the name of the current plugin and the version number of the plugin.

ModuleBase.cs

Copy    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;
        }
    }

ModuleBaseThe class implements the IModuleinterface and does some initialization. Subsequent plugin classes need to inherit ModuleBaseclasses.

Parsing Plugin Configuration

In order to complete the parsing of the plugin package, here I created a PluginPackageclass that encapsulates the operation of the plugin package.

Copy    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.");
                }
            }
        }
    }

Code explanation:

  • Here in the Initializemethod I used the ZipToolclass to decompress, after decompression, the program will try to read the plugin.jsonfiles in the temporary decompression directory , if the file does not exist, it will report an exception.
  • If there is no current plugin in the main program, it will be extracted into the defined plugin directory. (The plug-in upgrade is not considered here, and will be further explained in the next article)
  • GetAllMigrationsThe purpose of the method is to load all migration scripts for the current plugin from the assembly.

New script migration feature

In order to allow the plugin to automatically create database tables when it is installed, I also added a script migration mechanism, which is similar to EF script migration and the previously shared FluentMigrator migration.

Here we define a migration interface IMigration, and define two interface methods MigrationUpand functions MigrationDownto complete the plug-in upgrade and downgrade.

Copy    public interface IMigration
    {
        DomainModel.Version Version { get; }

        void MigrationUp(Guid pluginId);

        void MigrationDown(Guid pluginId);
    }   

Then we implemented a migration script base classBaseMigration

Copy    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());
        }
    }

Code interpretation

  • The purpose of the WriteMigrationScriptssum here RemoveMigrationScriptsis to save the plugin upgrade and downgrade migration script to the database. Because I don’t want to read the migration script every time by loading the assembly, I will import the migration script for each plugin version into the database when I install the plugin.
  • SQLThe method is used to run the migration script. In order to simplify the code, there is a lack of transaction processing. Interested students can add it themselves.

Add a migration program for the previous script

Here we assume that after installing the DemoPlugin1 plugin version 1.0.0, you need to add a Testtable named in the main program’s database .

Based on the above requirements, I added an initial script migration class Migration.1.0.0.csthat inherits from the BaseMigrationclass.

Copy    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);
        }
    }

Code explanation :

  • Here we implement MigrationUpand MigrationDowncreate new tables by implementation and methods. Of course, this article only implements the installation of plugins, and does not involve deletion or downgrade. This part of the code will be used in subsequent articles.
  • Note here that after running the upgrade script, the upgrade level script of the current plugin version will be base.WriteMigrationScriptssaved to the database by method.

Add a business processing class that installs the plugin package

In order to complete the installation logic of the plugin package, here I created a PluginManagerclass, which AddPluginsis used to install the plugin.

Copy    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);
        }
    }

Code interpretation

  • The method signature pluginPackagecontains all the information about the plugin package.
  • Here we first save the information of the plugin to the database through the unit of work.
  • After the save is successful, I obtained pluginPackageall the migration scripts included in the current plug-in package through the object, and then run these scripts to complete the migration of the database.

Add plugin management interface to the main site

Here to manage the plugin, I created 2 new pages, a plugin list page and a new plugin page in the main site. The functions of these two pages are very simple, I will not introduce them further here. Most of the processing is to reuse the previous code, such as the installation, enabling and disabling of the plug-in. The relevant code can be viewed by yourself.


Plug-in_development_of_ASP.NET_Core_MVC(4)_-_Plugin_installation_3.png

 


Plug-in_development_of_ASP.NET_Core_MVC(4)_-_Plugin_installation_4.png

 

Set the installed plugin default startup

After completing the 2 plugin management pages, the last step, we still need to do is to load the installed plugins into the runtime and enable them during the startup phase of the program.

Copy    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);
            }
        }   
    }

After the setup is complete, the installation code for the entire plugin comes to an end.

Final effect


Plug-in_development_of_ASP.NET_Core_MVC(4)_-_Plugin_installation_5.gif

 

Summary and issues to be solved

In this article, I shared with you if you installed the packaged plugin into the system and completed the corresponding script migration. However, in this article, we have only completed the installation of the plugin, for the removal of the plugin, and the upgrade level of the plugin, we have not yet resolved, interested students can try it on their own, you will find in the .NET Core 2.2 version, we There is no Unload assembly capability at runtime, so from the next one, I will upgrade the current project development environment to .NET Core 3.0 Preview, for plugin removal and lifting level I will give in .NET Core 3.0 Everyone demonstrates.

Orignal link:https://www.cnblogs.com/lwqlun/p/11343141.html