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 code :
https://github.com/lamondlu/DynamicPlugins

In the previous two articles, I showed you how to use the Application Part to dynamically load controllers and views, and how to create plug-in templates to simplify operations.
After the last one was written, I suddenly thought of a problem. If I designed the plug-in system like the previous two, there will be a serious problem, namely

When you add a plugin, the entire program can’t be enabled immediately. Only after restarting the entire ASP.NET Core application can the plugin be loaded correctly. Because all plugins are loaded in the ConfigureServicemethod at program startup .

Plug-in systems in this way can be difficult to use. The effect we expect is to dynamically enable and disable plug-ins at runtime. Is there any solution? The answer is yes. Below, I will explain step by step to my own ideas, problems encountered in coding, and solutions to these problems.

In order to complete this function, I have taken a lot of detours. The current solution may not be the best, but it is indeed a feasible solution. If you have a better solution, we can discuss it together.

Activate component in Action

When I encountered this problem, my first thought was to ApplicationPartManagermove the code that loads the plugin library into an Action. So I created one in the main site PluginsControllerand added an EnableAction method named Enabled .

Copypublic class PluginsController : Controller
{
    public IActionResult Enable()
    {
        var assembly = Assembly.LoadFile(AppDomain.CurrentDomain.BaseDirectory + "DemoPlugin1\\DemoPlugin1.dll");
        var viewAssembly = Assembly.LoadFile(AppDomain.CurrentDomain.BaseDirectory + "DemoPlugin1\\DemoPlugin1.Views.dll");
        var viewAssemblyPart = new CompiledRazorAssemblyPart(viewAssembly);

        var controllerAssemblyPart = new AssemblyPart(assembly);
        _partManager.ApplicationParts.Add(controllerAssemblyPart);
        _partManager.ApplicationParts.Add(viewAssemblyPart);

        return Content("Enabled");
    }
}

After modifying the code, run the program, here we first call /Plugins/Enableto try to activate the component, after activation, we call again/Plugin1/HelloWorld

Here you will find that the program returned 404, ie the controller and view are not properly activated.


Plug-in_development_of_ASP.NET_Core_MVC(3)_-_How_to_enable_components_at_runtime_1.png

 

Here you may have questions, why do you fail to activate?

The reason here is that the controller and view assemblies are loaded into the ApplicationPart manager only when the ASP.NET Core application starts, so although the new controller assembly is added to the ApplicationPartmanager at runtime , ASP.NET Core does not automatically update, so here we need to find a way to get ASP.NET Core to reload the controller.

By querying various materials, I finally found a pointcut. In ASP.NET Core 2.2, there is a class ActionDescriptorCollectionProviderwhose subclass DefaultActionDescriptorCollectionProvideris used to configure Controller and Action.

Source code:

Copy    internal class DefaultActionDescriptorCollectionProvider : ActionDescriptorCollectionProvider
    {
        private readonly IActionDescriptorProvider[] _actionDescriptorProviders;
        private readonly IActionDescriptorChangeProvider[] _actionDescriptorChangeProviders;
        private readonly object _lock;
        private ActionDescriptorCollection _collection;
        private IChangeToken _changeToken;
        private CancellationTokenSource _cancellationTokenSource;
        private int _version = 0;

        public DefaultActionDescriptorCollectionProvider(
            IEnumerable<IActionDescriptorProvider> actionDescriptorProviders,
            IEnumerable<IActionDescriptorChangeProvider> actionDescriptorChangeProviders)
        {
            ...
            ChangeToken.OnChange(
                GetCompositeChangeToken,
                UpdateCollection);
        }
       
        public override ActionDescriptorCollection ActionDescriptors
        {
            get
            {
                Initialize();

                return _collection;
            }
        }

        ...

        private IChangeToken GetCompositeChangeToken()
        {
            if (_actionDescriptorChangeProviders.Length == 1)
            {
                return _actionDescriptorChangeProviders[0].GetChangeToken();
            }

            var changeTokens = new IChangeToken[_actionDescriptorChangeProviders.Length];
            for (var i = 0; i < _actionDescriptorChangeProviders.Length; i++)
            {
                changeTokens[i] = _actionDescriptorChangeProviders[i].GetChangeToken();
            }

            return new CompositeChangeToken(changeTokens);
        }

        ...

        private void UpdateCollection()
        {
            lock (_lock)
            {
                var context = new ActionDescriptorProviderContext();

                for (var i = 0; i < _actionDescriptorProviders.Length; i++)
                {
                    _actionDescriptorProviders[i].OnProvidersExecuting(context);
                }

                for (var i = _actionDescriptorProviders.Length - 1; i >= 0; i--)
                {
                    _actionDescriptorProviders[i].OnProvidersExecuted(context);
                }
                
                var oldCancellationTokenSource = _cancellationTokenSource;
           
                _collection = new ActionDescriptorCollection(
                    new ReadOnlyCollection<ActionDescriptor>(context.Results),
                    _version++);

                _cancellationTokenSource = new CancellationTokenSource();
                _changeToken = new CancellationChangeToken(_cancellationTokenSource.Token);

                oldCancellationTokenSource?.Cancel();
            }
        }
    }
  • This ActionDescriptorsproperty records all the Controller/Action collections that match when the ASP.NET Core program is launched.
  • The UpdateCollection method is used to update the ActionDescriptorscollection.
  • A trigger is designed in the constructor ChangeToken.OnChange(GetCompositeChangeToken,UpdateCollection). Here the program will listen for a Token object, and when the Token object changes, it will automatically trigger the UpdateCollectionmethod.
  • Here Token is a IActionDescriptorChangeProvidercombination of a set of interface objects.

So here we can customize the IActionDescriptorChangeProviderinterface object, and modify the interface Token in the component activation method Enable, so DefaultActionDescriptorCollectionProviderthat the CompositeChangeTokenchange occurs in the controller, so that the controller is reloaded.

Use IActionDescriptorChangeProviderto activate controller at runtime

Here we first create a MyActionDescriptorChangeProviderclass and let it implement the IActionDescriptorChangeProviderinterface

Copy    public class MyActionDescriptorChangeProvider : IActionDescriptorChangeProvider
    {
        public static MyActionDescriptorChangeProvider Instance { get; } = new MyActionDescriptorChangeProvider();

        public CancellationTokenSource TokenSource { get; private set; }

        public bool HasChanged { get; set; }

        public IChangeToken GetChangeToken()
        {
            TokenSource = new CancellationTokenSource();
            return new CancellationChangeToken(TokenSource.Token);
        }
    }

Then we need Startup.csa ConfigureServicesmethod, the MyActionDescriptorChangeProvider.Instanceproperty as to register a single embodiment of the container dependency injection.

Copy    public void ConfigureServices(IServiceCollection services)
    {
        ...

        services.AddSingleton<IActionDescriptorChangeProvider>(MyActionDescriptorChangeProvider.Instance);
        services.AddSingleton(MyActionDescriptorChangeProvider.Instance);
        
        ...
    }

Finally, we Enablemodify MyActionDescriptorChangeProviderthe Token of the current object with two lines of code in the method .

Copy    public class PluginsController : Controller
    {
        public IActionResult Enable()
        {
            var assembly = Assembly.LoadFile(AppDomain.CurrentDomain.BaseDirectory + "DemoPlugin1\\DemoPlugin1.dll");
            var viewAssembly = Assembly.LoadFile(AppDomain.CurrentDomain.BaseDirectory + "DemoPlugin1\\DemoPlugin1.Views.dll");
            var viewAssemblyPart = new CompiledRazorAssemblyPart(viewAssembly);

            var controllerAssemblyPart = new AssemblyPart(assembly);
            _partManager.ApplicationParts.Add(controllerAssemblyPart);
            _partManager.ApplicationParts.Add(viewAssemblyPart);
            
            MyActionDescriptorChangeProvider.Instance.HasChanged = true;
            MyActionDescriptorChangeProvider.Instance.TokenSource.Cancel();

            return Content("Enabled");
        }
    }

After re-running the program after modifying the code, we will still call /Plugins/Enableit first and then call /Plugin1/Helloworldit again . At this time, you will find that the Action is triggered, but the corresponding Views are not found.


Plug-in_development_of_ASP.NET_Core_MVC(3)_-_How_to_enable_components_at_runtime_2.png

 

How to solve the problem that the plugin’s precompiled Razor view can’t be reloaded?

In the above way, we finally got the ability to load the plugin controller assembly at runtime, but the plugin’s precompiled Razor view assembly is not loaded correctly, which means that IActionDescriptorChangeProvideronly the controller’s reload will be triggered and will not trigger. Precompile the reload of the Razor view. ASP.NET Core only loads the precompiled Razor assembly of the plugin when the entire application starts, so we don’t get the ability to reload the precompiled Razor view at runtime.

In view of this, I also consulted a lot of information, and ultimately there is no feasible solution, maybe using Razor Runtime Compilation of ASP.NET Core 3.0, but in ASP.NET Core 2.2, we have not yet obtained this ability. .

In order to overcome this difficulty, I finally chose to abandon the pre-compiled Razor view and use the original Razor view.

Because when ASP.NET Core starts, we can configure the Razor view engine to retrieve the rules of the view in Startup.csthe ConfigureServicesmethod.

Here we can organize each plugin into an Area in ASP.NET Core MVC, the name of the Area is the name of the plugin, so we can add a rule to retrieve the view for the Razor view engine, the code is as follows

Copy    services.Configure<RazorViewEngineOptions>(o =>
    {
        o.AreaViewLocationFormats.Add("/Modules/{2}/{1}/Views/{0}" + RazorViewEngine.ViewExtension);
    });

This {2}represents the name of the Area, which {1}represents the name of the Controller and {0}represents the name of the Action.

Here Modules is a directory I recreated, and all subsequent plugins will be placed in this directory.

Similarly, we also need Configureto register the route for the Area in the method.

Copy    app.UseMvc(routes =>
    {
        routes.MapRoute(
        name: "default",
        template: "{controller=Home}/{action=Index}/{id?}");

        routes.MapRoute(
        name: "default",
        template: "Modules/{area}/{controller=Home}/{action=Index}/{id?}");
    });

Because we don’t need to use Razor’s precompiled view, so Enablethe final code of our method is as follows

Copy    public IActionResult Enable()
    {
        var assembly = Assembly.LoadFile(AppDomain.CurrentDomain.BaseDirectory + "Modules\\DemoPlugin1\\DemoPlugin1.dll");

        var controllerAssemblyPart = new AssemblyPart(assembly);
        _partManager.ApplicationParts.Add(controllerAssemblyPart);

        MyActionDescriptorChangeProvider.Instance.HasChanged = true;
        MyActionDescriptorChangeProvider.Instance.TokenSource.Cancel();

        return Content("Enabled");
    }

The above is the modification of the main site, let’s modify the plugin project.

First we need to change the Sdk type of the entire project from Microsoft.Net.Sdk.Razor to Microsoft.Net.Sdk.Web. Since we used the pre-compiled Razor view, we used Microsoft.Net. Sdk.Razor, which compiles the view into a dll file. But now we need to use the original Razor view, so we need to change it to Microsoft.Net.Sdk.Web, using this Sdk, the files in the final Views folder will be published in their original form.

Copy<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <TargetFramework>netcoreapp2.2</TargetFramework>
  </PropertyGroup>

  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
    <OutputPath></OutputPath>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.AspNetCore.App" Version="2.2.0" />
    <PackageReference Include="Microsoft.AspNetCore.Razor" Version="2.2.0" />
    <PackageReference Include="Microsoft.AspNetCore.Razor.Design" Version="2.2.0" />
  </ItemGroup>

  <ItemGroup>
    <ProjectReference Include="..\DynamicPlugins.Core\DynamicPlugins.Core.csproj" />
  </ItemGroup>



</Project>

Finally, we need to add the Area configuration on the Plugin1Controller, and put the compiled assembly and Views directory into the Modules directory of the main site project.

Copy    [Area("DemoPlugin1")]
    public class Plugin1Controller : Controller
    {
        public IActionResult HelloWorld()
        {
            return View();
        }
    }

Final master site project directory structure

CopyThe files tree is:
=================

  |__ DynamicPlugins.Core.dll
  |__ DynamicPlugins.Core.pdb
  |__ DynamicPluginsDemoSite.deps.json
  |__ DynamicPluginsDemoSite.dll
  |__ DynamicPluginsDemoSite.pdb
  |__ DynamicPluginsDemoSite.runtimeconfig.dev.json
  |__ DynamicPluginsDemoSite.runtimeconfig.json
  |__ DynamicPluginsDemoSite.Views.dll
  |__ DynamicPluginsDemoSite.Views.pdb
  |__ Modules
    |__ DemoPlugin1
      |__ DemoPlugin1.dll
      |__ Views
        |__ Plugin1
          |__ HelloWorld.cshtml
        |__ _ViewStart.cshtml

Now we restart the project, re-activate the plugin in the previous order, and then access the new plugin route /Modules/DemoPlugin1/plugin1/helloworld, the page is displayed normally.


Plug-in_development_of_ASP.NET_Core_MVC(3)_-_How_to_enable_components_at_runtime_3.png

 

Summary

In this article, I showed you how to enable a plugin at runtime. Here we use IActionDescriptorChangeProvider, let ASP.NET Core reload the controller at runtime, although it does not support preloading Razor view loading, but we configure the original The directory rules loaded by the Razor view also implement the function of dynamically reading the view.

In the next article I will continue to refactor this project, write a business model, and try to write plug-in installations and up-and-down versions of the code.

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