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

image

Instead of the foreword


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


Problems and statement of the problem


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, support was not stressful and the implementation of the changes did not cause any problems. But over time, the system grew to gigantic proportions and such a simple task as maintaining the code became quite difficult. Therefore, I came to the conclusion that this whole zoo should be split into parts. The project is an ASP.NET MVC intranet site where employees of the company work. Over time, five to seven views and two to three controllers grew into a huge pile, which became difficult to maintain.

Search for a solution

To begin, I began to look for standard ways to solve the problem of dividing a project into parts. Immediately the thought of areas (Areas) came to my mind. But this option was discarded immediately, since in fact it simply split the project into even smaller elements, which did not solve the problem. All “standard” solution methods were also considered, but even there I did not find anything that satisfied me.
The basic idea was simple. Each component of the system should be a plug-in with a simple way to connect to a working system, consisting of as few files as possible. Creating and connecting a new plugin should not affect the root application in any way. And plugin connection itself should not be more difficult than a couple of mouse clicks.

First approach

During a long googling and searches on the Internet, a description of the plug-in system was found. The author provided the source code for the project, which were downloaded immediately. The project is beautiful, and even fulfills everything that I want to see in a ready-made solution. Launched, looked. Indeed, plugins presented as separate projects are compiled and “automatically connected” (here I wrote in quotation marks for a reason. I will write why later) to the site. I was already ready to jump for joy, but ... Here BUT arose, which I did not expect. Having looked at the parameters of the plug-in projects, I found the construction parameters. They wrote the parameters of post-construction, which were used to copy the library of classes and areas (Areas) into the folder with the site. This was a big upset. Not at all like a "convenient plug-in system." Therefore, I continued the search. AND,

Approach number two

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

Beginning of work


First, 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 to write code. We will create the necessary basic components. Therefore, we add a project of the Class Library type to the solution and call it Infrastructure.
First you need to create a plugin interface that all plugins must implement.
The code is simple and I will not write about it.
IModule
namespace EkzoPlugin.Infrastructure
{
    public interface IModule
    {
        /// 
        /// Имя, которое будет отображаться на сайте
        /// 
        string Title { get; }
        /// 
        /// Уникальное имя плагина
        /// 
        string Name { get; }
        /// 
        /// Версия плагина
        /// 
        Version Version { get; }
        /// 
        /// Имя контроллера, который будет обрабатывать запросы
        /// 
        string EntryControllerName { get; }
    }
}


Now we will create another project of the Class Library type, call it PluginManager. This project will have 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();
        }
        private static PluginManager _current;
        public static PluginManager Current 
        { 
	        get { return _current ?? (_current = new PluginManager()); }
        }
    	internal Dictionary Modules { get; set; }
        //Возвращаем все загруженные модули
        public IEnumerable GetModules()
        {
            return Modules.Select(m => m.Key).ToList();
        }
        //Получаем плагин по имени
        public IModule GetModule(string name)
        {
            return GetModules().Where(m => m.Name == name).FirstOrDefault();
        }
    }
}


This class implements a plugin manager that will store a list of loaded plugins and manipulate them.

Now create a new class file and name it PreApplicationInit. This is where magic will work, which will automatically connect plugins when launching applications. The PreApplicationStartMethod attribute is responsible for the “magic” (you can read about it here ). In short, the method specified when it was declared will be executed before starting 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);
        }
        /// 
        /// Папка из которой будут копироваться файлы плагинов
        /// 
        /// 
        /// Папка может содержать подпапки для разделения плагинов по типам
        /// 
        private static readonly DirectoryInfo PluginFolder;
        /// 
        /// Папка в которую будут скопированы плагины
        /// Если не скопировать плагин, его будет невозможно заменить при запущенном приложении
        /// 
        private static readonly DirectoryInfo TempPluginFolder;
        /// 
        /// Initialize method that registers all plugins
        /// 
        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
            // так: 
            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 operation of the plug-in system. Now download the library, which will provide work with built-in views, and add a link to it to the project.
It remains to create the code needed to register the 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 must be done so that requests arriving at the server are correctly directed to precompiled views.

Now you can go to our base system and configure it to work with plugins. First, we open the web.config file and add the following line to the runtime section

Plugins will not work without this setting.

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

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

Here I will explain a little. Remember the PreApplicationInit attribute? So, before ApplicationStart gained control, the modules were initialized and loaded into the plugin manager. And when the ApplicationStart procedure got control, we register the loaded modules so that the program knows how to process the routes to the plug-ins.
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 want to make a reservation right away 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 restriction, but I advise you to stick to it to avoid troubles). This is the price you have to pay.
We take the Web Application MVC project as the 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 are making a simple plugin for an example, we will not change it, but simply add a view for it.
After adding the view, open it and write something that will identify our plugin.
For example, like this:

Sample Mvc Plugin


Now open the Visual Studio Add-ons Manager and install RazorGenerator. This extension allows you to add views to the compiled dll file.
After installation, select the index.cshtml view in the solution explorer and set the following values ​​in the properties window:
Build Action: EmbeddedResource
Custom Tool: RazorGenerator
These settings indicate the inclusion of the view in the resources of the compiled library.
We are almost done. We need to take all two simple steps in order for our plugin to work.
First of all, we should add a link to the EkzoPlugin.Infrastructure project created earlier, which contains the plugin interface, which we implement.
Add a class to the plugin project and call 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. The plugin is ready. Isn’t it just that?
Now compile the solution and copy the library obtained as a result of the plug-in assembly into the plugins folder of the base site.
Add the following lines to the _Layout.cshtml file of the base site
@using EkzoPlugin.Infrastructure
@using EkzoPlugin.PluginManager
@{
    IEnumerable modules = PluginManager.Current.GetModules();
    Func getModule = name => PluginManager.Current.GetModule(name);
}
....

    @foreach (IModule module in modules) { }
...

Thus, we will add links to the loaded modules.

Instead of a conclusion


So the plugin site on ASP.NET MVC is ready. Not everything is perfect, not everything is beautiful, I agree. But he performs his main task. I want to note that when working with a real project, it will be very convenient to configure post-compilation teams of plug-in projects that will themselves upload the result to the plug-in folder of the base site.
I also want to note that each module can be developed and tested as a separate site, and after compilation into a ready-made module, it is quite simply connected to the project.
There is a small subtlety of working with scripts and links to third-party libraries. Firstly, the plugin will use scripts located in the folders of the base site. That is, if you added some kind of script at the plug-in development stage, do not forget to put it in the appropriate base class directory (this is also true for styles, images, etc.).
Secondly, the connected third-party library must be located in the bin folder of the base site. Otherwise, you will receive an error stating that the required 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 read from the configuration file, you need to manually transfer it.

I hope that this article will be of interest to someone.

Project on GitHub - link
Project with MetroUI template - link

Also popular now: