Plugins for ASP.NET MVC based on Autofac modules
Development | создано: 27.12.2017 | опубликовано: 28.12.2017 | обновлено: 13.01.2024 | просмотров: 5883
Modular applications are well-scalable applications that which are simply amenable to expansion, i.e. adding new functionality. This article describes one of the examples of how to organize a modular application based on a DI container Autofac.
What is this article about?
The purpose of my article is to show how you can organize the structure of an application in which separate modules (plugins) are implemented. An example for this implementation I took the principle of the calculator with remote operations. Imagine that you have a collapsible calculator whose operation buttons (+,-, x, etc.) are in another box. Which buttons you insert into the chassis, you can perform these operations.
Architecture
First, let's create a black solution in Visual Studio. Press New Project, then on the left tab select Installed -> Other Project Types -> Visual Studio Solution. Enter the name "ModulesMvc". Press "Ok" button. Then we going to add new project. Open context menu by clicking right mouse button on the solution in Solution Explorer. Select "New Project". Select ASP.NET MVC 5 template.
After project have been created by Visual Studio I always run command to update all nuget-packages.
PM> Update-Package
This guarantee me that is all packages has latest version. Next, we need to add new Class Library. I named it ModulesContracts. This project will contains the contracts, interfaces, base classes, enums and other stuff for using in plugins. This library is a root of architecture tree. In the solution I have been created a Plugins folder in the Solution Folder.
As you have guessed I've added reference ModulesContracts to ModulesMvc.
Autofac
Let's install Autofac.Mvc5 package using Console Package Manager:
PM> Install-Package Autofac.Mvc5 Attempting to resolve dependency 'Autofac (≥ 3.4.0 && < 4.0.0)'. Attempting to resolve dependency 'Microsoft.AspNet.Mvc (≥ 5.1.0 && < 6.0.0)'. Attempting to resolve dependency 'Microsoft.AspNet.WebPages (≥ 3.1.0 && < 3.2.0)'. Attempting to resolve dependency 'Microsoft.Web.Infrastructure (≥ 1.0.0.0)'. Attempting to resolve dependency 'Microsoft.AspNet.Razor (≥ 3.1.0 && < 3.2.0)'. ... Successfully added 'Microsoft.AspNet.Mvc 5.1.0' to ModulesMvc. Adding 'Autofac.Mvc5 3.3.2' to ModulesMvc. Successfully added 'Autofac.Mvc5 3.3.2' to ModulesMvc. PM>
Next, we have to set up the Autofac container. In the folder App_Start create a new file AutofacConfig.cs:
public static class AutofacConfig { public static void Initialize() { var builder = new ContainerBuilder(); var assembly = typeof(MvcApplication).Assembly; builder.RegisterControllers(assembly); builder.RegisterFilterProvider(); builder.RegisterModule(new AutofacWebTypesModule()); builder.RegisterType<SimlpeLogger>().As<ILogger>(); builder.RegisterModules("plugins"); var container = builder.Build(); DependencyResolver.SetResolver(new AutofacDependencyResolver(container)); } }
Look at line 11. This is it:
public static class AutofacExtension { public static void RegisterModules(this ContainerBuilder builder, string pluginsPath) { var mapPath = HttpContext.Current.Server.MapPath(string.Concat("~/", pluginsPath)); var dir = new DirectoryInfo(mapPath); var dlls = dir.GetFiles("*.dll"); if (dlls != null && dlls.Any()) { foreach (var item in dlls) { if (item.Name.Contains("Module")) { var asmb = Assembly.LoadFile(Path.Combine(mapPath, item.Name)); builder.RegisterAssemblyTypes(asmb).AsImplementedInterfaces(); } } } } }
In fact, the last listing is very important. Let's stop and comment on the main points. Examples of configurations and configuration options can be found on the official website (http://autofac.org/). The only exception is the 11 line, which specifies that all modules should be connected to the Plugins folder. So, in line 5 we define the path by which the extension will look for modules. In lines 6-7 we collect files, in our case, these are files with extension *. dll. In line 10, all found files are searched, and if the file (see Line 12) has the word "module" in the title, then register it in the container. Now left in the file Global.asax.cs register Autofacconfig. Initialize() method.
What about contracts
Autofac as a DI container has quite a large capacity. One of them is support of modules. To create a module for your system, you need to use the module class from Autofac namespace, which is represented by the IModule interface. I'll add some functionality. Create custom class IMvcModule and inherites it from IModule in the project ModuleContracts:
public interface IMvcModule: IModule { string Name { get; } }
I did this to make it possible to list the connected modules. Next I created a successor from Module, which will be a base class for all my plugins in my application.
public abstract class ModuleBase : Module, IMvcModule { public virtual string Name { get { return GetType().Name; } } }
These preparations for my demonstration are enough. Now let's create a synthesis interface for plugins. It is as uproshen as possible to demonstrate the principle:
public interface ICalculatorOperation { int Calculate(int x, int y); string Name { get; } }
In line 3, the method will perform calculations. The Name property is used to identify the calculation operation. In this line will be stored operations: Add, Devide, Multiply, etc. Note that the AutofacConfig class does not contain any information about the registration of the above interfaces, and all the work of registering for the modules.
The Controller story
Let's assume that I already have a controller called HomeController, and this controller has an Index method. This controller has a constructor:
public HomeController(ILogger logger, IEnumerable<ICalculatorOperation> operations) { _logger = logger; _operations = operations; }
I injecting ICalculatorOperation as Enumerable collection because calculator should have a lot of operations in his arcenal. The list of operations we use in the Index method:
public ActionResult Index() { var modules = DependencyResolver.Current.GetServices<IMvcModule>(); _logger.Log("Loaded {0} modules", modules.Count()); return View(modules); }
The view for this action is:
@model System.Collections.Generic.IEnumerable<ModulesContracts.IMvcModule> @{ ViewBag.Title = "Avaliable Modules"; } @if (Model != null) { <h2>Avaliable Modules</h2> foreach (var module in Model) { <p> <b>@module.Name</b> - @module.ToString() </p> } <p>Go to <a href="@Url.Action("calculator", "home")">Calculator</a></p> } else { <h2>No modules avaliable</h2> }
In line 11, we select all operations and list them on the main page. I have already created four plugins (addition, subtraction, multiplication, and division) so my list looks like this:
Everything is works as expected. :)
Making plugins
It's time show how to make a plugin. And this is how the working calculator looks like:
All operations for this calculator are loading from the DLL-plugins. The key point that the main project does not have references to plugins. The plugins are uploaded to the special folder. The main application on start attach all searched plugins. No magic it just modularity in action. :) You can open repository with demo project from GitHub (link below).
Operation "Addition"
I have created new project in folder Plugins (Class Library). Then I rename generated file Class1 to AppendModule. After that, I wrote a few lines of code:
namespace AppendModule { public class AppendOperation : ModuleBase { public override string ToString() { return "The operation returns the sum of the two numbers"; } protected override void Load(ContainerBuilder builder) { builder.RegisterType<CalculatorOperation>().As<ICalculatorOperation>(); } } public class CalculatorOperation : ICalculatorOperation { public int Calculate(int x, int y) { return x + y; } public string Name { get { return "Append"; } } } }
To inherit from the Modulebase class, I added a reference to the ModuleContracts project. Next in line 5, I overrided the ToString() method to use it as a description in the module list. The Load method is called in line 10. This method is defined in the base Module class from the Autofac namespace. You can see that the "Addition" operation is registered in line 12. Line 16 shows the class implementing the ICalculatorOperation interface.
Conclusion
As a conclusion, you can see the markup of the calculator page (View):
@using System.Linq @using ModulesContracts @{ ViewBag.Title = "Calculator"; } <h2>Calculator</h2> <p> <input type="text" id="x" name="x" value="@ViewBag.X" /> </p> <p> <input type="text" id="y" name="y" value="@ViewBag.Y" /> </p> <p> @if (ViewBag.Operations != null && ((IEnumerable<ICalculatorOperation>)ViewBag.Operations).Any()) { foreach (var operation in (IEnumerable<ICalculatorOperation>)ViewBag.Operations) { <form method="POST"> <input type="hidden" id="@(operation.Name)_x" name="x" value="@ViewBag.X" /> <input type="hidden" id="@(operation.Name)_y" name="y" value="@ViewBag.Y" /> <input type="hidden" name="operation" value="@operation.Name" /> <p> <button type="submit">@operation.Name</button> </p> </form> } } </p> @section scripts{ <script> $(function () { $('#x').on('change', function () { var x = $(this).val(); $('input[id$=_x]').val(x); }); $('#y').on('change', function () { var y = $(this).val(); $('input[id$=_y]').val(y); }); }); </script> }
That's all. Download solution from GitHub.