Подтвердить что ты не робот

Динамически загружаемые плагины MVC4 MEF

обновлено: читайте ниже в этом сообщении для минимального решения

У меня есть кое-какие вопросы о MVC4 с плагинами. Я немного погубил и нашел некоторые хорошие вещи, но это точно не соответствует моим требованиям, поэтому я прошу здесь дать некоторые советы.

Похоже, что лучшим решением для виджет-подобных плагинов в MVC является портативная область (в пакете MvcContrib). Я нашел здесь основные рекомендации:

http://lostechies.com/erichexter/2009/11/01/asp-net-mvc-portable-areas-via-mvccontrib/

и некоторые полезные советы здесь:

http://geekswithblogs.net/michelotti/archive/2010/04/05/mvc-portable-areas-ndash-web-application-projects.aspx

Больше сообщений в этом посте:

Как создать область ASP.NET MVC в качестве DLL плагина?

Что все круто, но, к сожалению, мои требования немного разные:

  • К сожалению, мне нужна система, в которой плагины добавляются и обнаруживаются динамически, и это не относится к портативным областям, на которые должен ссылаться основной проект сайта MVC. Я хотел бы просто загрузить что-то на сайт и открыть его и использовать новые компоненты, поэтому я буду использовать MEF для этого.

  • К счастью, мои плагины не будут похожи на виджеты, которые могут быть очень сложными и неоднородными; скорее, они являются компонентами, которые должны следовать общей общей схеме. Думайте о них как о специализированных редакторах: для каждого типа данных я предлагаю компонент с функциями редактирования: новый, редактировать, удалять. Поэтому я думал о плагинах-контроллерах, которые реализуют общий интерфейс и предоставляют такие действия, как New, Edit, Delete и т.п.

  • Я должен использовать MVC4, и в будущем мне придется добавлять настройки локализации и мобильных устройств.

  • Я должен избегать зависимостей от сложных фреймворков и держать код как можно более простым.

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

В конце концов я мог бы включить представления в самой DLL (я нашел это: http://razorgenerator.codeplex.com, и этот учебник: http://www.chrisvandesteeg.nl/2010/11/22/embedding-pre-compiled-razor-views-in-your-dll/, который, я полагаю, я мог бы использовать с разборгенератором codeplex, поскольку код, к которому он относится, несовместим с VS2012), но, вероятно, мне будет лучше держать их отделенными (также из-за требований к локализации и мобильности); Я думал о добавлении механизма загрузки в область администрирования сайта, где вы можете загрузить один zip с помощью DLL с контроллерами и папками с представлениями, а затем позволить серверу разархивировать и хранить файлы там, где это необходимо. Это позволило бы мне легко изменять виды без необходимости развертывания всей надстройки.

Итак, я начал искать MEF и MVC, но большинство сообщений относятся к MVC2 и несовместимы. Мне повезло с этим, что в основном сосредоточено на веб-API, но выглядит многообещающим и достаточно простым:

http://kennytordeur.blogspot.it/2012/08/mef-in-aspnet-mvc-4-and-webapi.html

Это существенно добавляет преобразователь зависимостей на основе MEF и контроллер factory к стандартным приложениям MVC. В любом случае образец в сообщении ссылается на односегментное решение, в то время как мне нужно развернуть несколько разных плагинов. Поэтому я немного изменил код, чтобы использовать каталог MEF DirectoryCatalog (а не AssemblyCatalog), указывающий на мою папку с плагинами, а затем создал тестовое MVC-решение с одним плагином в библиотеке классов.

В любом случае, когда я пытаюсь загрузить контроллер плагина, фреймворк вызывает мой factory GetControllerInstance с нулевым типом, так что, конечно, MEF не может перейти к составу. Вероятно, я пропускаю что-то очевидное, но я новичок в MVC 4, и любое предложение или полезная (MVC4-совместимая) ссылка приветствуется. Спасибо!

Вот важный код:

public static class MefConfig
{
    public static void RegisterMef()
    {
        CompositionContainer container = ConfigureContainer();

        ControllerBuilder.Current.SetControllerFactory(new MefControllerFactory(container));

        System.Web.Http.GlobalConfiguration.Configuration.DependencyResolver =
            new MefDependencyResolver(container);
    }

    private static CompositionContainer ConfigureContainer()
    {
        //AssemblyCatalog assemblyCatalog = new AssemblyCatalog(Assembly.GetExecutingAssembly());

        DirectoryCatalog catalog = new DirectoryCatalog(
            HostingEnvironment.MapPath("~/Plugins"));
        CompositionContainer container = new CompositionContainer(catalog);
        return container;
    }
}

public class MefDependencyResolver : IDependencyResolver
{
    private readonly CompositionContainer _container;

    public MefDependencyResolver(CompositionContainer container)
    {
        _container = container;
    }

    public IDependencyScope BeginScope()
    {
        return this;
    }

    public object GetService(Type serviceType)
    {
        var export = _container.GetExports(serviceType, null, null).SingleOrDefault();
        return (export != null ? export.Value : null);
    }

    public IEnumerable GetServices(Type serviceType)
    {
        var exports = _container.GetExports(serviceType, null, null);
        List createdObjects = new List();

        if (exports.Any())
            createdObjects.AddRange(exports.Select(export => export.Value));

        return createdObjects;
    }

    public void Dispose()
    {
    }
}

public class MefControllerFactory : DefaultControllerFactory
{
    private readonly CompositionContainer _compositionContainer;

    public MefControllerFactory(CompositionContainer compositionContainer)
    {
        _compositionContainer = compositionContainer;
    }

    protected override IController GetControllerInstance(
        System.Web.Routing.RequestContext requestContext, Type controllerType)
    {
        if (controllerType == null) throw new ArgumentNullException("controllerType");
        var export = _compositionContainer.GetExports(controllerType, null, null).SingleOrDefault();

        IController result;

        if (null != export) result = export.Value as IController;
        else
        {
            result = base.GetControllerInstance(requestContext, controllerType);
            _compositionContainer.ComposeParts(result);
        } //eelse

        return result;
    }
}

Вы можете скачать полное тестовое решение отсюда:

http://www.filedropper.com/mvcplugins

Изменить: первое рабочее минимальное решение

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

1) во-первых, источником путаницы для меня были два разных интерфейса с тем же именем: IDependencyResolver. Если я хорошо понимаю, один (System.Web.Http.Dependencies.IDependencyResolver) используется для webapi, а другой (System.Web.Mvc.IDependencyResolver) для общего DI. Этот пост помог мне здесь: http://lucid-nonsense.co.uk/dependency-injection-web-api-and-mvc-4-rc/.

2), третий компонент - это контроллер, основанный на стандарте DefaultControllerFactory factory, который имеет решающее значение для этого сообщения, потому что это factory, используемый для контроллеров, размещенных в плагинах.

Вот мои реализации для всех этих, слегка измененные из нескольких образцов: сначала HTTP-резольвер:

public sealed class MefHttpDependencyResolver : IDependencyResolver
{
    private readonly CompositionContainer _container;

    public MefHttpDependencyResolver(CompositionContainer container)
    {
        if (container == null) throw new ArgumentNullException("container");
        _container = container;
    }

    public object GetService(Type serviceType)
    {
        if (serviceType == null) throw new ArgumentNullException("serviceType");

        string name = AttributedModelServices.GetContractName(serviceType);

        try
        {
            return _container.GetExportedValue(name);
        }
        catch
        {
            return null;
        }
    }

    public IEnumerable GetServices(Type serviceType)
    {
        if (serviceType == null) throw new ArgumentNullException("serviceType");

        string name = AttributedModelServices.GetContractName(serviceType);

        try
        {
            return _container.GetExportedValues(name);
        }
        catch
        {
            return null;
        }
    }

    public IDependencyScope BeginScope()
    {
        return this;
    }

    public void Dispose()
    {
    }
}

Тогда MVC-резольвер, который очень схож, даже если он строго не нужен для фиктивной выборки в этом сценарии:

public class MefDependencyResolver : IDependencyResolver
{
    private readonly CompositionContainer _container;

    public MefDependencyResolver(CompositionContainer container)
    {
        if (container == null) throw new ArgumentNullException("container");
        _container = container;
    }

    public object GetService(Type type)
    {
        if (type == null) throw new ArgumentNullException("type");

        string name = AttributedModelServices.GetContractName(type);

        try
        {
            return _container.GetExportedValue(name);
        }
        catch
        {
            return null;
        }
    }

    public IEnumerable GetServices(Type type)
    {
        if (type == null) throw new ArgumentNullException("type");

        string name = AttributedModelServices.GetContractName(type);

        try
        {
            return _container.GetExportedValues(name);
        }
        catch
        {
            return null;
        }
    }
}

И, наконец, контроллер factory:

[Export(typeof(IControllerFactory))]
public class MefControllerFactory : DefaultControllerFactory
{
    private readonly CompositionContainer _container;

    [ImportingConstructor]
    public MefControllerFactory(CompositionContainer container)
    {
        if (container == null) throw new ArgumentNullException("container");
        _container = container;
    }

    public override IController CreateController(RequestContext requestContext, string controllerName)
    {
        var controller = _container
            .GetExports()
            .Where(c => c.Metadata.Name.Equals(controllerName, StringComparison.OrdinalIgnoreCase))
            .Select(c => c.Value)
            .FirstOrDefault();

        return controller ?? base.CreateController(requestContext, controllerName);
    }
}

Что касается контроллера образца, я создал его в проекте библиотеки классов:

[Export(typeof(IController))]
[PartCreationPolicy(CreationPolicy.NonShared)]
[ExportMetadata("Name", "Alpha")]
public sealed class AlphaController : Controller
{
    public ActionResult Index()
    {
        ViewBag.Message = "Hello, this is the PLUGIN controller!";

        return View();
    }
}

В основном проекте (на сайте MVC) у меня есть папка Plugins, в которой я копирую эту DLL, а также "стандартный" набор представлений в своих папках для этих представлений контроллера.

Это самый простой возможный сценарий, и, вероятно, есть намного больше, чтобы узнать и усовершенствовать, но мне нужно было просто начать с самого начала. В любом случае, любое предложение приветствуется.

4b9b3361

Ответ 1

В настоящее время я работаю над той же проблемой. Я нашел это решение:

В основном он загружает сборки из указанного местоположения и с некоторым шаблоном имени при запуске веб-приложения:

AssemblyInfo.cs:

[assembly: PreApplicationStartMethod(
  typeof(PluginAreaBootstrapper), "Init")]

PluginAreaBootstrapper.cs:

public class PluginAreaBootstrapper
{
    public static readonly List<Assembly> PluginAssemblies = new List<Assembly>();

    public static List<string> PluginNames()
    {
        return PluginAssemblies.Select(
            pluginAssembly => pluginAssembly.GetName().Name)
            .ToList();
    }

    public static void Init()
    {
        var fullPluginPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Areas");

        foreach (var file in Directory.EnumerateFiles(fullPluginPath, "*Plugin*.dll", SearchOption.AllDirectories))
            PluginAssemblies.Add(Assembly.LoadFile(file));

        PluginAssemblies.ForEach(BuildManager.AddReferencedAssembly);

        // Add assembly handler for strongly-typed view models
        AppDomain.CurrentDomain.AssemblyResolve += AssemblyResolve;
    }

    private static Assembly AssemblyResolve(object sender, ResolveEventArgs resolveArgs)
    {
        var currentAssemblies = AppDomain.CurrentDomain.GetAssemblies();
        // Check we don't already have the assembly loaded
        foreach (var assembly in currentAssemblies)
        {
            if (assembly.FullName == resolveArgs.Name || assembly.GetName().Name == resolveArgs.Name)
            {
                return assembly;
            }
        }

        return null;
    }
}

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

По-моему, это соответствует вашим потребностям 1, 2 и 4. Он очень прост, не требует каких-либо фреймворков, имеет минимальную конфигурацию, позволяет динамическую загрузку плагинов и работает с MVC 4.

Это решение подключает сборки в каталог Area, но я считаю, что вы можете легко настроить его, чтобы играть, как вам нравится, используя маршрутизацию.

Ответ 2

Вы можете найти более полные решения, здесь, и больше фона здесь. Наши требования были также DI, поэтому это поможет немного, хотя и не нужно (первые ссылки предоставляют решение для DI. Удачи вам в этом, это не сложно. Также обратите внимание, что это для MVC3, но легко конвертировать/мигрировать в MVC 4