📜 ⬆️ ⬇️

Plugin system on ASP.NET. Or a site with plugins, mademoiselles and preference

image

Instead of the preface


This material is solely the result of the work of collecting information online and creating a site that works on the basis of plug-ins. Here I will try to describe the idea of ​​how such a system works and the main components necessary for its work.
This article does not claim originality, and the described system is not the only correct and beautiful. But if you, dear $ habrauser $, I wonder how to create such a system, you are welcome under the cat


Problematics and problem statement


At the moment I am conducting a fairly large project, divided into four large parts. Each of these parts is divided into at least 2 small tasks. While the system had 2 parts, the support was not stressful and the implementation of changes did not cause any problems. But over time, the system has grown to gigantic proportions and such a simple task as maintaining the code has become quite difficult. Therefore, I came to the conclusion that the whole zoo should be split into pieces. The project is an ASP.NET MVC site on the intranet on which employees work. Over time, five to seven views and two to three controllers grew into a huge pile that became difficult to maintain.
')
Finding a solution

For a start, I started looking for standard solutions to the problem of splitting a project into parts. Immediately came up with the thought of areas (Areas). But this option was discarded immediately, because in fact it was just crushing the project into even smaller elements, which did not solve the problem. All the “standard” methods of solution were also considered, but even there I did not find anything that satisfied me.
The basic idea was simple. Each system component must be a plugin with a simple way to connect to a working system, consisting of as few files as possible. Creating and connecting a new plug-in should not affect the root application. And the plug-in connection itself should not be more difficult than a couple of mouse clicks.

Approach one

During the long googling and searches on the Internet, a description of the plug-in system was found. The author provided the source of the project, which was downloaded immediately. The project is beautiful, and even performs everything that I want to see in the finished solution. Launched, looked. Indeed, plug-ins presented as separate projects are compiled and “automatically connected” (here I wrote in quotes for a reason. Then I will write why) to the site. I was already ready to jump for joy, but ... Here it was BUT, which I did not expect. After looking at the parameters of the plug-in projects, I found the build parameters. They were written post-build parameters that were used to copy the class library and areas (Areas) in the folder with the site. It was a big upset. Not at all like a “convenient plug-in system.” Therefore, I continued to search. And, oddly enough, this search was a success.

Approach number two

So, on one of the forums I found a description of a very interesting system. I will not paint for a long time all the way, so I will immediately say that I found exactly what I was looking for. The system works on the basis of pre-compiled plugins with built-in views. Connecting a new plug-in is done by copying the dll's to the plug-in folder and restarting the application. I took this system as a basis for work.

Beginning of work


To begin, open Visual Studio and create a new Web Application MVC 4 project (in fact, the MVC version does not play a huge role). But we will not rush into writing code. We will create the necessary basic components. Therefore, we will add a project of type Class Library to the solution and name it Infrastructure.
First you need to create a plug-in interface that all plug-ins must implement.
The code is simple and I will not write about it.
IModule
namespace EkzoPlugin.Infrastructure { public interface IModule { /// <summary> /// ,      /// </summary> string Title { get; } /// <summary> ///    /// </summary> string Name { get; } /// <summary> ///   /// </summary> Version Version { get; } /// <summary> ///  ,     /// </summary> string EntryControllerName { get; } } } 


Now we will create another project of type Class Library, call it PluginManager. In this project there will be all the necessary classes responsible for connecting to the base project.
Create a class file and write the following code:
Pluginmanager
 namespace EkzoPlugin.PluginManager { public class PluginManager { public PluginManager() { Modules = new Dictionary<IModule, Assembly>(); } private static PluginManager _current; public static PluginManager Current { get { return _current ?? (_current = new PluginManager()); } } internal Dictionary<IModule, Assembly> Modules { get; set; } //    public IEnumerable<IModule> GetModules() { return Modules.Select(m => m.Key).ToList(); } //    public IModule GetModule(string name) { return GetModules().Where(m => m.Name == name).FirstOrDefault(); } } } 


This class implements the plugin manager, which will store the list of loaded plugins and manipulate them.

Now create a new class file and name it PreApplicationInit. This is where the magic will be created, which will allow you to automatically connect plug-ins when launching applications. The PreApplicationStartMethod attribute is responsible for “magic” (you can read about it here ). In short, the method specified when it is declared will be executed before the start of the web application. Even earlier than Application_Start. This will allow us to download our plugins before the application starts.
PreApplicationInit
 [assembly: PreApplicationStartMethod(typeof(EkzoPlugin.PluginManager.PreApplicationInit), "InitializePlugins")] namespace EkzoPlugin.PluginManager { public class PreApplicationInit { static PreApplicationInit() { //      string pluginsPath = HostingEnvironment.MapPath("~/plugins"); //    ,     string pluginsTempPath = HostingEnvironment.MapPath("~/plugins/temp"); //    ,   if (pluginsPath == null || pluginsTempPath == null) throw new DirectoryNotFoundException("plugins"); PluginFolder = new DirectoryInfo(pluginsPath); TempPluginFolder = new DirectoryInfo(pluginsTempPath); } /// <summary> ///        /// </summary> /// <remarks> ///          /// </remarks> private static readonly DirectoryInfo PluginFolder; /// <summary> ///       ///    ,        /// </summary> private static readonly DirectoryInfo TempPluginFolder; /// <summary> /// Initialize method that registers all plugins /// </summary> public static void InitializePlugins() { Directory.CreateDirectory(TempPluginFolder.FullName); //     foreach (var f in TempPluginFolder.GetFiles("*.dll", SearchOption.AllDirectories)) { try { f.Delete(); } catch (Exception) { } } //  foreach (var plug in PluginFolder.GetFiles("*.dll", SearchOption.AllDirectories)) { try { var di = Directory.CreateDirectory(TempPluginFolder.FullName); File.Copy(plug.FullName, Path.Combine(di.FullName, plug.Name), true); } catch (Exception) { } } // *     'Load' //      'probing'   web.config // : <probing privatePath="plugins/temp" /> var assemblies = TempPluginFolder.GetFiles("*.dll", SearchOption.AllDirectories) .Select(x => AssemblyName.GetAssemblyName(x.FullName)) .Select(x => Assembly.Load(x.FullName)); foreach (var assembly in assemblies) { Type type = assembly.GetTypes().Where(t => t.GetInterface(typeof(IModule).Name) != null).FirstOrDefault(); if (type != null) { //      BuildManager.AddReferencedAssembly(assembly); //       var module = (IModule)Activator.CreateInstance(type); PluginManager.Current.Modules.Add(module, assembly); } } } } } 


This is almost all that is needed to organize the work of the plug-in system. Now download the library that will provide work with the built-in views, and add a link to it to the project.
It remains to create the code required for registering plugins.
Hidden text
 namespace EkzoPlugin.PluginManager { public static class PluginBootstrapper { public static void Initialize() { foreach (var asmbl in PluginManager.Current.Modules.Values) { BoC.Web.Mvc.PrecompiledViews.ApplicationPartRegistry.Register(asmbl); } } } } 


This module downloads and registers precompiled resources. This needs to be done so that the requests that come to the server are correctly routed to the precompiled views.

Now you can go to our base system and configure it to work with plugins. First we will open the web.config file and add the following line to the runtime section
  <runtime> <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1"> <probing privatePath="plugins/temp" /> 

Without this setting, plugins will not work.

Now add to the project links to previously created EkzoPlugin.PluginManager project.
Now open Global.asax and add just two lines. First we connect the namespace EkzoPlugin.PluginManager
 using EkzoPlugin.PluginManager; 

And add the second one in the first line to Applicaton_Start ()
  protected void ApplicationStart() { PluginBootstrapper.Initialize(); 

Here I will explain a little. Remember the PreApplicationInit attribute? So, before ApplicationStart got control, modules were initialized and loaded into the plugin manager. And when the ApplicationStart procedure got control, we perform the registration of loaded modules so that the program knows how to handle the routes to the plugins.
That's all. Our basic application is ready. It can work with plugins located in the plugins folder.

Writing a plugin


Let's now write a simple plugin to demonstrate the work. I just want to make a reservation that all plugins should have a common namespace with the base project and be located in the Plugin namespace (in fact, this is not a hard limitation, but I advise you to stick to it in order to avoid trouble). This is the price to pay.
We take the Web Application MVC project as a basis. Create an empty project.
Add a new Controllers folder and add a new controller. Let's call it SampleMvcController.
By default, Visual Studio creates a controller with a single Index () action. Since we make a simple plugin for example, we will not change it, but simply add a presentation for it.
After adding the view, open it and write something that will identify our plugin.
For example:
 <h3>Sample Mvc Plugin</h3> 

Now open the Visual Studio Add-ons Manager and install the RazorGenerator. This extension allows you to add views to the compiled dll file.
After installation, select the index.cshtml view in solution explorer and set the following values ​​in the properties window:
Build Action: EmbeddedResource
Custom Tool: RazorGenerator
These settings indicate that the representation is included in the resources of the compiled library.
We are almost done. We need to do all two simple steps to make our plugin work.
First of all, we need to add a link to the EkzoPlugin.Infrastructure project created earlier, which contains the interface of the plug-in, which we are implementing.
Add a class to the plugin project and name it SampleMVCModule.cs
SampleMVCModule
 using EkzoPlugin.Infrastructure; namespace EkzoPlugin.Plugins.SampleMVC { public class SampleMVCModule : IModule { public string Title { get { return "SampleMVCPlugin"; } } public string Name { get { return Assembly.GetAssembly(GetType()).GetName().Name; } } public Version Version { get { return new Version(1, 0, 0, 0); } } public string EntryControllerName { get { return "SampleMVC"; } } } } 


That's all. Plugin ready. Just isn't it?
Now we will compile the solution and copy the library resulting from the plugin assembly into the plugins folder of the base site.
Add the following lines to the base site _Layout.cshtml file
 @using EkzoPlugin.Infrastructure @using EkzoPlugin.PluginManager @{ IEnumerable<IModule> modules = PluginManager.Current.GetModules(); Func<string, IModule> getModule = name => PluginManager.Current.GetModule(name); } <html ...... <head>....</head> <body> <ul id="pluginsNavigation"> <li class="MenuItem">@Html.ActionLink("Home","Index","Home",null,null)</li> @foreach (IModule module in modules) { <li class="MenuItem">@Html.ActionLink(module.Title, "Index", module.EntryControllerName)</li> } </ul> ... </body> </html> 

Thus we will add links to the loaded modules.

Instead of conclusion


Here is a plug-in site on ASP.NET MVC. Not everything is perfect, not everything is beautiful, I agree. But it performs its main task. I want to note that when working with a real project, it will be very convenient to set up commands for post-compiling plug-in projects that will upload the result to the plug-ins folder of the base site.
I also want to note that each module can be developed and tested as a separate site, and after compiling it into a ready-made module, it is simply connected to the project.
There is a small subtlety of working with scripts and links to third-party libraries. First, the plugin will use scripts located in the folders of the base site. That is, if you add a script at the development stage of the plugin, do not forget to put it in the appropriate base class directory (this is also relevant for styles, images, etc.).
Secondly, the connected third-party library should be placed in the bin folder of the base site. Otherwise, you will get an error stating that the necessary assembly could not be found.
Thirdly, your plugin will work with the web.config file of the base site. Thus, if the plugin uses the connection string or another section that is read from the configuration file, you need to manually transfer it.

I hope that this article will be interesting to someone.

Project on GitHub - link
Project with MetroUI template - link

Source: https://habr.com/ru/post/202776/


All Articles