MEF z MVC 4 lub 5 - Plugable Architecture (2014)

80

Próbuję zbudować aplikację MVC4 / MVC5 z wtykową architekturą, taką jak Orchard CMS. Mam więc aplikację MVC, która będzie projektem startowym i zajmie się uwierzytelnianiem, nawigacją itp. Następnie będzie wiele modułów zbudowanych osobno jako biblioteki klas asp.net lub okrojone projekty mvc i posiadające kontrolery, widoki, repozytoria danych itp.

Spędziłem cały dzień przeglądając samouczki w sieci i pobierając próbki itp. I stwierdziłem, że Kenny ma najlepszy przykład w okolicy - http://kennytordeur.blogspot.in/2012/08/mef-in-aspnet-mvc-4-and -webapi.html

Jestem w stanie zaimportować kontrolery z modułów (oddzielne biblioteki DLL), jeśli dodam odniesienie do tych bibliotek DLL. Ale powodem używania MEF jest możliwość dodawania modułów w czasie wykonywania. Chcę, aby biblioteki DLL wraz z widokami zostały skopiowane do katalogu ~ / Modules // w projekcie startowym (udało mi się to zrobić), a MEF po prostu je odebrał. Próbując zmusić MEF do załadowania tych bibliotek.

Istnieje również MefContrib, jak wyjaśniono w tej odpowiedzi Kontrolery ASP.NET MVC 4.0 i MEF, jak połączyć te dwa elementy? co jest następną rzeczą, której spróbuję. Ale jestem zaskoczony, że MEF nie działa po wyjęciu z pudełka z MVC.

Czy ktoś ma podobną architekturę działającą (z MefContrib lub bez)? Początkowo myślałem nawet o usunięciu Orchard CMS i wykorzystaniu go jako frameworka, ale jest to zbyt skomplikowane. Przydałoby się również opracowanie aplikacji w MVC5, aby korzystać z WebAPI2.

Yashvit
źródło
1
Czy masz tę konfigurację do pracy z MVC5? Próbuję ustawić to samo z MVC 5. Twoja pomoc jest mile widziana
Junior
1
Oto konkurencyjny przykład, który ma wersje implementujące zarówno EF, jak i strait ASP.net wydaje się kompletne. codeproject.com/Articles/1109475/…
BrownPony
Dlaczego więcej aplikacji nie używa MEF? Wydaje się, że na tym każdy pracuje samodzielnie.
johnny

Odpowiedzi:

105

Pracowałem nad projektem, który miał podobną architekturę wtykową, jak ta, którą opisałeś i wykorzystywał te same technologie ASP.NET MVC i MEF. Mieliśmy hostową aplikację ASP.NET MVC, która obsługiwała uwierzytelnianie, autoryzację i wszystkie żądania. Nasze wtyczki (moduły) zostały skopiowane do jego podfolderu. Wtyczki były również aplikacjami ASP.NET MVC, które miały własne modele, kontrolery, widoki, pliki css i js. Oto kroki, które wykonaliśmy, aby to zadziałało:

Konfigurowanie MEF

Stworzyliśmy silnik oparty na MEF, który wykrywa wszystkie komponenty komponowalne na starcie aplikacji i tworzy katalog tych części. Jest to zadanie wykonywane tylko raz podczas uruchamiania aplikacji. Silnik musi wykryć wszystkie podłączane części, które w naszym przypadku znajdowały się w binfolderze aplikacji hosta lub w Modules(Plugins)folderze.

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

To jest przykładowy kod klasy, która wykonuje odnajdywanie wszystkich części MEF. ComposeMetoda klasy jest wywoływana z Application_Startmetody w Global.asax.cspliku. Kod jest zredukowany ze względu na prostotę.

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

Zakłada się, że wszystkie wtyczki są kopiowane do oddzielnego podfolderu Modulesfolderu znajdującego się w katalogu głównym aplikacji hosta. Każdy podfolder wtyczek zawiera podfolder Viewsi bibliotekę DLL z każdej wtyczki. W Application_Startpowyższej metodzie są również zainicjowane niestandardowa fabryka kontrolerów i niestandardowy silnik widoku, który zdefiniuję poniżej.

Tworzę fabrykę kontrolerów, która czyta z MEF

Oto kod definiujący fabrykę niestandardowych kontrolerów, który wykryje kontroler, który musi obsłużyć żądanie:

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

Dodatkowo każdy kontroler musi być oznaczony Exportatrybutem:

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

Pierwszy parametr Exportkonstruktora atrybutów musi być unikatowy, ponieważ określa nazwę kontraktu i jednoznacznie identyfikuje każdy kontroler. PartCreationPolicyMusi być ustawiony na NonShared bo kontrolerzy nie mogą być ponownie wykorzystane dla wielu żądań.

Tworzenie silnika widoku, który wie, jak znajdować widoki z wtyczek

Utworzenie niestandardowego mechanizmu widoku jest potrzebne, ponieważ zgodnie z konwencją silnik widoku szuka widoków tylko w Viewsfolderze aplikacji hosta. Ponieważ wtyczki znajdują się w osobnym Modulesfolderze, musimy powiedzieć silnikowi widoku, aby również tam zajrzał.

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

Rozwiąż problem z silnie wpisanymi widokami we wtyczkach

Używając tylko powyższego kodu, nie mogliśmy używać silnie wpisanych widoków w naszych wtyczkach (modułach), ponieważ modele istniały poza binfolderem. Aby rozwiązać ten problem, skorzystaj z następującego łącza .

Ilija Dimov
źródło
1
co z niestandardową trasą dla każdego modułu? Myślę, że każdy moduleneed, aby uzyskać ref of routetable i global asax, powinien mieć interfejs trasy, w którym interfejs trasy będzie wyglądał zarówno w folderze modułu, jak iw rdzeniu.
sharif y
3
Rozwiązaliśmy to, definiując oddzielny obszar dla każdej wtyczki. W każdej wtyczce stworzyliśmy klasę, która dziedziczy po AreaRegistration i poprzez nadpisanie metody RegisterArea byliśmy w stanie zdefiniować trasy, których chcieliśmy używać we wtyczkach.
Ilija Dimov
10
Masz gdzieś przykładowy projekt tego rozwiązania?
cpoDesign
2
zgadzam się z cpoDesign. przykładowy projekt byłby fajny
chris vietor
2
Zgadzam się również, że przykładowy projekt na GitHub byłby świetny do pobrania :)
Kbdavis07
5

Należy tylko pamiętać, że kontener MEF ma „fajną funkcję”, która zachowuje odwołania do dowolnego obiektu IDisposable, który tworzy, i prowadzi do ogromnego wycieku pamięci. Podobno wyciek pamięci można rozwiązać za pomocą tego nuget - http://nuget.org/packages/NCode.Composition.DisposableParts.Signed

Aleksandar
źródło
Kolejny powód, by powiedzieć, że DryIoc jest lepszy :)
Hassan Tareq
3

Istnieją projekty, które implementują architekturę wtyczek. Możesz użyć jednego z nich lub rzucić okiem na ich kod źródłowy, aby zobaczyć, jak osiągają te rzeczy:

Również 404 na kontrolerach w zewnętrznych zespołach przyjmuje interesujące podejście. Wiele się nauczyłem, po prostu czytając pytanie.

Dejan
źródło