ASP.NET MVC: Плагины для ASP.NET MVC или Autofac Modules как plugins

ru-RU | создано: 03.09.2014 | опубликовано: 03.09.2014 | обновлено: 02.01.2018 | просмотров за всё время: 9094

Модульные приложения - это хорошо масштабируемые приложения, которые достаточно просто поддаются расширению, то есть добавлению нового функционала. В этой статье описывается один из примеров организации модульного приложения на основе DI-контейнера Autofac.

Что там впереди?

Целью моей статьи показать, как можно организовать структуру приложения, в котором реализованы обособленные модули (плагины). Примером для такой реализации я взял принцип калькулятора с удаленными операциями. Представьте, что у вас есть разборный калькулятор, у которого кнопки операций (+,-, и т.д.) находятся в другой коробке. Какие кнопки вставите в корпус, такие операции можно выполнять. Для реализации задачи потребуется Visual Studio 2013 или Visual Studio 2012 c установленным пакетом обновления (Update 2013.1). Хотя на самом деле для реализации можно взять и другую версию ASP.NET MVC, не обязательно пятую версию.

Архитектура решения

На этот раз сначала создадим чистое решение (blank solution). Для этого надо нажать "Новый проект (New Project)", далее на закладке слева выбрать "Installed -> Other Project Types -> Visual Studio Solution". Вводим название для нашего решения “ModulesMvc”. После этого нажать кнопку "Ok". Теперь во вновь созданное решениедобавим первый проект. Правой кнопкой на решении, затем "добавить новый проект (New Project)". Создаем новое приложение из шаблона ASP.NET MVC 5.

Не лишним было бы запустить в Package Manager Console команду upadate-package, чтобы обновить все сборки (в том числе и сами ASP.NET MVC). После этого добавляем в решение новый проект. Тип этого проекта Class Library. Название для в моем случае будет “ModulesContracts”. Этот проект будет содержать все необходимые интерфейсы и базовые классы для “передачи” их во внешние модули. Этот проект, так сказать, является “корневым” по части зависимостей в архитектуре решения (solution).

А еще я создал Solution Folder под названием Plugins. В общем итоге моё решение выглядит так:

153-10

Конечно же, в проект ModulesMvc я добавил ссылку (reference) на проект ModulesContracts.

Установка Autofac

Первым делом установим nuget-пакет Autofac.Mvc5. Для этого в 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> 

После того, как установлен пакет надо его подключить и настроить.

Создадим файл AutofacConfig.cs в папке App_start с таким содержанием:

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

Ключевым моментом является строка 11. Это метод-расширение для ContainerBuilder. Я создал его в другом файле, а вот что он содержит:

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

На самом деле, последний листинг является ключевым. Остановимся и прокомментируем основные моменты. Предыдущий листинг является “стандартным” по отношению к основным настройками работы DI-контейнера. Примеры конфигураций и варианты настроек можно найти на официальном сайте. Единственное исключение составляет 11 строка, в которой указано, что требуется подключить все модули в папки “plugins”. Итак, в строке 5 определяем путь по которому, расширение будет искать модули. В строках 6-7 собираем файлы, в нашем случае, это файлы с расширением *.dll. В строке 10 все найденные файлы перебираем, и если файл (смотри строку 12 ) имеет в названии слово “module”, то регистрируем его в контейнере. Теперь осталось в файле global.asax.cs зарегистрировать AutofacConfig.Initialize() метод.

Разберемся с контрактами

Autofac как DI-контейнер имеет достаточно большие возможности. Одно из них – это поддержка модулей. Для того, чтобы создать модуль для своей системы требуется использование класса Module из пространства имен Autofac, который представлен интерфейсом IModule. Я немного добавлю функционала. Создам свой класс IMvcModule как наследника от IModule в проекте ModuleContracts:

public interface IMvcModule: IModule
{
    string Name { get; } 
}

Это я сделал для того, чтобы можно было вывести список подключенных модулей. Далее я создал наследника от Module, который будет являться базовым классом для всех моих плагинов к моему приложению.

public abstract class ModuleBase : Module, IMvcModule
{
    public virtual string Name
    {
        get { return GetType().Name; }
    }
}

Этих приготовлений для моей демонстрации достаточно. Теперь создадим обобщающий интерфейс для плагинов. Он максимально упрошен для демонстрации принципа:

public interface ICalculatorOperation
{
    int Calculate(int x, int y);
    string Name{ get; }
}

В строке 3 метод будет выполнять вычисления. Свойство Name предназначено для идентификации операции вычисления. В этой строке будут храниться операции: Add, Devide, Multiply и т.д. Обратите внимание, что класс AutofacConfig не содержит никакой информации о регистрации перечисленных выше интерфейсов, а вся работа по регистрации ложиться на модули.

А что в контролере?

Давайте предположим, что у меня уже есть контролер под названием HomeController, и в этом контролере есть метод Index. У этого контролера есть конструктор:

public HomeController(ILogger logger, IEnumerable<ICalculatorOperation> operations)
{
    _logger = logger;
    _operations = operations;
}

Как инъекция вливания в конструктор “проваливается” коллекция операций калькулятора (operations). Список операций мы используем в методе Index:

public ActionResult Index()
{
    var modules = DependencyResolver.Current.GetServices<IMvcModule>();
    _logger.Log("Loaded {0} modules", modules.Count());
    return View(modules);
}

Представление Index.cshtml имеет такой вид:

@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>
}

В строке 11 перебираем все операции и выводим их список на главной странице. Я уже создал четыре плагина (операции сложения, вычитания, умножения и деления) поэтому мой список имеет такой вид:

153-20

“Сложный” калькулятор

Осталось только показать как делается плагин и показать что в итоге получилось. Вот выглядит калькулятор.

153-30

Кнопки (операции) калькулятора загружаются из сторонних плагинов. Один из них называется AppendMudule, и его я покажу как сделать. Остальные модули можно посмотреть, скачав демонстрационный проект в конце статьи.

Операция “сложения”

Я создал новый проект в папке Plugins, Шаблон для проекта я взял Class Library. Сгенерированный класс Class1 переименовал в AppendModule. Приведу весь код этого класса, а потом его прокомментирую:

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

Чтобы наследоваться от класса ModuleBase я добавил ссылку на проект ModuleContracts. Далее в строке 5 я переопределил метод ToString() чтобы использовать его в как описание в списке модулей. В строке 10 вызывается перегрузка метода Load. Этот метод определен в базовом классе Module из пространства имен Autofac. Видно, что в 12 строке идет регистрация операции “сложения”. В строке 16 представлен класс реализующий интерфейс ICalculatorOperation.

Ничего сложного.

Заключение

В качестве заключения приведу код страницы калькулятора:

@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>
}

Скачать на Github

Комментарии к статье (2)

24.12.2014 15:26:31 Дмитрий

Здравствуйте, нравятся Ваши статьи. В этой заметил что забыли указать инициализацию AutofacConfig и на сколько я понимаю Solution Folder создаст именно папку проекта, т.е. "виртуальную". HttpContext.Current.Server вернет папку текущего проекта - ModulesMVC, следовательно папки plugins там не будет

30.12.2014 10:12:56 Calabonga

Спасибо, Дмитрий, за подсказку на счет инициализации. Действительно ни строчки про инициализацию. Поправил. А вот на счет "виртуальной" папки не совсем. На самом деле в статье и речи нет про какие-либо папки виртуальные. Всё реальное! :)