MEF с MVC 4 или 5 - подключаемая архитектура (2014)

Я пытаюсь создать приложение MVC4/MVC5 с подключаемой архитектурой, такой как Orchard CMS. Таким образом, у меня есть приложение MVC, которое будет проектом запуска и позаботится о auth, навигации и т.д. Тогда будет несколько модулей, построенных отдельно как библиотеки классов asp.net или разделенные проекты mvc и имеющие контроллеры, представления, репозитории данных и т.д.

Я целый день проводил обучающие программы в Интернете и загружал образцы и т.д., и обнаружил, что у Kenny лучший пример - http://kennytordeur.blogspot.in/2012/08/mef-in-aspnet-mvc-4-and-webapi.html

Я могу импортировать контроллеры из модулей (отдельные библиотеки DLL), если добавить ссылку на эти DLL. Но причина использования MEF заключается в возможности добавления модулей во время выполнения. Я хочу, чтобы библиотеки DLL вместе с представлениями были скопированы в каталог ~/Modules//в стартовом проекте (мне это удалось), и MEF просто заберет их. Борясь за то, чтобы MEF загружал эти библиотеки.

Существует также MefContrib, как описано в этом ответе Контроллеры ASP.NET MVC 4.0 и MEF, как свести эти два вместе?, что является следующей вещью, о которой я говорю пытаться. Но я удивлен, что MEF не работает из коробки с MVC.

Есть ли у кого-нибудь подобная архитектура, работающая (с MefContrib или без нее)? Первоначально я даже думал о том, чтобы скрыть Orchard CMS и использовать его в качестве рамки, но он слишком сложный. Также было бы неплохо разработать приложение в MVC5, чтобы воспользоваться WebAPI2.

Ответ 1

Я работал над проектом, который имел аналогичную архитектуру, подобную той, которую вы описали, и использовал те же технологии ASP.NET MVC и MEF. У нас было хост-приложение ASP.NET MVC, которое обрабатывало аутентификацию, авторизацию и все запросы. Наши плагины (модули) были скопированы в подпапку. Плагины также были ASP.NET MVC приложениями, в которых были свои собственные модели, контроллеры, представления, css и js файлы. Это шаги, которые мы выполнили, чтобы заставить его работать:

Настройка MEF

Мы создали движок на основе MEF, который обнаруживает все составные части при запуске приложения и создает каталог составных частей. Это задача, которая выполняется только один раз при запуске приложения. Двигатель должен обнаружить все подключаемые компоненты, которые в нашем случае были расположены либо в папке bin хост-приложения, либо в папке Modules(Plugins).

public class Bootstrapper
{
    private static CompositionContainer CompositionContainer;
    private static bool IsLoaded = false;

    public static void Compose(List<string> pluginFolders)
    {
        if (IsLoaded) return;

        var catalog = new AggregateCatalog();

        catalog.Catalogs.Add(new DirectoryCatalog(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "bin")));

        foreach (var plugin in pluginFolders)
        {
            var directoryCatalog = new DirectoryCatalog(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Modules", plugin));
            catalog.Catalogs.Add(directoryCatalog);

        }
        CompositionContainer = new CompositionContainer(catalog);

        CompositionContainer.ComposeParts();
        IsLoaded = true;
    }

    public static T GetInstance<T>(string contractName = null)
    {
        var type = default(T);
        if (CompositionContainer == null) return type;

        if (!string.IsNullOrWhiteSpace(contractName))
            type = CompositionContainer.GetExportedValue<T>(contractName);
        else
            type = CompositionContainer.GetExportedValue<T>();

        return type;
    }
}

Это пример кода класса, который выполняет обнаружение всех частей MEF. Метод Compose класса вызывается из метода Application_Start в файле Global.asax.cs. Код упрощен для простоты.

public class MvcApplication : System.Web.HttpApplication
{
    protected void Application_Start()
    {
        var pluginFolders = new List<string>();

        var plugins = Directory.GetDirectories(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Modules")).ToList();

        plugins.ForEach(s =>
        {
            var di = new DirectoryInfo(s);
            pluginFolders.Add(di.Name);
        });

        AreaRegistration.RegisterAllAreas();
        RouteConfig.RegisterRoutes(RouteTable.Routes);
        Bootstrapper.Compose(pluginFolders);
        ControllerBuilder.Current.SetControllerFactory(new CustomControllerFactory());
        ViewEngines.Engines.Add(new CustomViewEngine(pluginFolders));
    }
}

Предполагается, что все плагины копируются в отдельную подпапку в папке Modules, которая находится в корневом каталоге хост-приложения. Каждая подпапка плагина содержит подпапку Views и dll из каждого плагина. В приведенном выше методе Application_Start также инициализируется пользовательский контроллер factory и настраиваемый механизм просмотра, который я буду определять ниже.

Создание контроллера factory, который читает из MEF

Вот код для определения пользовательского контроллера factory, который обнаружит контроллер, который должен обрабатывать запрос:

public class CustomControllerFactory : IControllerFactory
{
    private readonly DefaultControllerFactory _defaultControllerFactory;

    public CustomControllerFactory()
    {
        _defaultControllerFactory = new DefaultControllerFactory();
    }

    public IController CreateController(RequestContext requestContext, string controllerName)
    {
        var controller = Bootstrapper.GetInstance<IController>(controllerName);

        if (controller == null)
            throw new Exception("Controller not found!");

        return controller;
    }

    public SessionStateBehavior GetControllerSessionBehavior(RequestContext requestContext, string controllerName)
    {
        return SessionStateBehavior.Default;
    }

    public void ReleaseController(IController controller)
    {
        var disposableController = controller as IDisposable;

        if (disposableController != null)
        {
            disposableController.Dispose();
        }
    }
}

Кроме того, каждый контроллер должен быть отмечен атрибутом Export:

[Export("Plugin1", typeof(IController))]
[PartCreationPolicy(CreationPolicy.NonShared)]
public class Plugin1Controller : Controller
{
    //
    // GET: /Plugin1/
    public ActionResult Index()
    {
        return View();
    }
}

Первый параметр конструктора атрибутов Export должен быть уникальным, поскольку он определяет имя контракта и однозначно идентифицирует каждый контроллер. PartCreationPolicy должен быть установлен в NonShared, поскольку контроллеры не могут использоваться повторно для нескольких запросов.

Создание механизма просмотра, который знает, чтобы найти представления из плагинов

Создание настраиваемого механизма просмотра необходимо, потому что механизм просмотра по соглашению ищет представления только в папке Views хост-приложения. Поскольку плагины находятся в отдельной папке Modules, нам нужно также указать движку просмотра, чтобы посмотреть там.

public class CustomViewEngine : RazorViewEngine
{
    private List<string> _plugins = new List<string>();

    public CustomViewEngine(List<string> pluginFolders)
    {
        _plugins = pluginFolders;

        ViewLocationFormats = GetViewLocations();
        MasterLocationFormats = GetMasterLocations();
        PartialViewLocationFormats = GetViewLocations();
    }

    public string[] GetViewLocations()
    {
        var views = new List<string>();
        views.Add("~/Views/{1}/{0}.cshtml");

        _plugins.ForEach(plugin =>
            views.Add("~/Modules/" + plugin + "/Views/{1}/{0}.cshtml")
        );
        return views.ToArray();
    }

    public string[] GetMasterLocations()
    {
        var masterPages = new List<string>();

        masterPages.Add("~/Views/Shared/{0}.cshtml");

        _plugins.ForEach(plugin =>
            masterPages.Add("~/Modules/" + plugin + "/Views/Shared/{0}.cshtml")
        );

        return masterPages.ToArray();
    }
}

Решите проблему с сильно типизированными представлениями в плагинах

Используя только вышеуказанный код, мы не могли использовать строго типизированные представления в наших плагинах (модулях), потому что модели существовали вне папки bin. Чтобы решить эту проблему, выполните следующую ссылку .

Ответ 2

Просто помните, что в контейнере MEF есть "хорошая функция", которая поддерживает ссылки на любой создаваемый объект IDisposable и приведет к огромной утечке памяти. Предположительно, утечка памяти может быть решена с помощью этого nuget - http://nuget.org/packages/NCode.Composition.DisposableParts.Signed

Ответ 3

Существуют проекты, которые реализуют архитектуру плагина. Возможно, вы захотите использовать один из них или посмотреть исходный код, чтобы узнать, как они выполняют эти задачи:

Кроме того, 404 на контроллерах во внешних сборках делает интересный подход. Я многому научился, просто прочитав вопрос.