MEF mit MVC 4 oder 5 - Pluggable Architecture (2014)

80

Ich versuche, eine MVC4 / MVC5-Anwendung mit einer steckbaren Architektur wie Orchard CMS zu erstellen. Ich habe also eine MVC-Anwendung, die das Startprojekt sein wird und sich um Authentifizierung, Navigation usw. kümmert. Dann werden mehrere Module separat als asp.net-Klassenbibliotheken oder abgespeckte MVC-Projekte erstellt und verfügen über Controller, Ansichten, Datenrepos usw.

Ich habe den ganzen Tag Tutorials im Internet durchgesehen und Beispiele usw. heruntergeladen und festgestellt, dass Kenny das beste Beispiel dafür hat - http://kennytordeur.blogspot.in/2012/08/mef-in-aspnet-mvc-4-and -webapi.html

Ich kann die Controller aus den Modulen importieren (separate DLLs), wenn ich Verweise auf diese DLLs hinzufüge. Der Grund für die Verwendung von MEF ist jedoch, dass zur Laufzeit Module hinzugefügt werden können. Ich möchte, dass die DLLs zusammen mit den Ansichten in ein Verzeichnis ~ / Modules // im Startprojekt kopiert werden (dies ist mir gelungen), und MEF würde sie einfach abholen. Es fällt mir schwer, MEF dazu zu bringen, diese Bibliotheken zu laden.

Es gibt auch MefContrib, wie in dieser Antwort erläutert. ASP.NET MVC 4.0-Controller und MEF, wie können diese beiden zusammengeführt werden? Das ist das nächste, was ich versuchen werde. Aber ich bin überrascht, dass MEF mit MVC nicht sofort funktioniert.

Hat jemand eine ähnliche Architektur (mit oder ohne MefContrib)? Anfangs dachte ich sogar daran, Orchard CMS zu entfernen und als Framework zu verwenden, aber es ist zu komplex. Es wäre auch schön, die App in MVC5 zu entwickeln, um WebAPI2 zu nutzen.

Yashvit
quelle
1
Haben Sie alle dieses Setup für MVC5? Ich versuche, dasselbe mit MVC 5 einzurichten. Ihre Hilfe wird geschätzt
Junior
1
Hier ist ein Konkurrenzbeispiel, bei dem Versionen, die sowohl EF als auch Strait ASP.net implementieren, vollständig zu sein scheinen. codeproject.com/Articles/1109475/…
BrownPony
Warum verwenden nicht mehr Anwendungen MEF? Jeder scheint seine eigene Rolle zu spielen.
Johnny

Antworten:

105

Ich habe an einem Projekt gearbeitet, das eine ähnliche steckbare Architektur wie die von Ihnen beschriebene hatte und die gleichen Technologien wie ASP.NET MVC und MEF verwendete. Wir hatten eine ASP.NET MVC-Hostanwendung, die die Authentifizierung, Autorisierung und alle Anforderungen abwickelte. Unsere Plugins (Module) wurden in einen Unterordner kopiert. Die Plugins waren auch ASP.NET MVC-Anwendungen mit eigenen Modellen, Controllern, Ansichten, CSS- und JS-Dateien. Dies sind die Schritte, die wir befolgt haben, damit es funktioniert:

MEF einrichten

Wir haben eine auf MEF basierende Engine erstellt, die beim Start der Anwendung alle zusammensetzbaren Teile erkennt und einen Katalog der zusammensetzbaren Teile erstellt. Dies ist eine Aufgabe, die beim Start der Anwendung nur einmal ausgeführt wird. Die Engine muss alle steckbaren Teile erkennen, die sich in unserem Fall entweder im binOrdner der Host-Anwendung oder im Modules(Plugins)Ordner befanden .

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

Dies ist der Beispielcode der Klasse, die die Ermittlung aller MEF-Teile durchführt. Die ComposeMethode der Klasse wird von der Application_StartMethode in der Global.asax.csDatei aufgerufen . Der Code wird der Einfachheit halber reduziert.

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

Es wird davon ausgegangen, dass alle Plugins in einen separaten Unterordner des Ordners kopiert werden Modules, der sich im Stammverzeichnis der Hostanwendung befindet. Jeder Plugin-Unterordner enthält den ViewsUnterordner und die DLL von jedem Plugin. In der Application_Startobigen Methode werden auch die benutzerdefinierte Controller-Factory und die benutzerdefinierte Ansichts-Engine initialisiert, die ich unten definieren werde.

Erstellen einer Controller-Factory, die von MEF liest

Hier ist der Code zum Definieren der benutzerdefinierten Controller-Factory, die den Controller erkennt, der die Anforderung verarbeiten muss:

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

Zusätzlich muss jeder Controller mit folgenden ExportAttributen gekennzeichnet sein:

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

Der erste Parameter des ExportAttributkonstruktors muss eindeutig sein, da er den Vertragsnamen angibt und jeden Controller eindeutig identifiziert. Das PartCreationPolicymuss auf NonShared gesetzt sein, da Controller nicht für mehrere Anforderungen wiederverwendet werden können.

Erstellen einer View Engine, die die Ansichten der Plugins findet

Die Erstellung einer benutzerdefinierten Ansichts-Engine ist erforderlich, da die Ansichts-Engine gemäß Konvention nur im ViewsOrdner der Host-Anwendung nach Ansichten sucht . Da sich die Plugins in einem separaten ModulesOrdner befinden, müssen wir der View Engine mitteilen, dass sie auch dort suchen soll.

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

Lösen Sie das Problem mit stark typisierten Ansichten in den Plugins

Wenn wir nur den obigen Code verwenden, können wir in unseren Plugins (Modulen) keine stark typisierten Ansichten verwenden, da Modelle außerhalb des binOrdners vorhanden sind. Um dieses Problem zu lösen, folgen Sie dem folgenden Link .

Ilija Dimov
quelle
1
Wie wäre es mit einer benutzerdefinierten Route für jedes einzelne Modul? Ich denke, jedes Modul, das benötigt wird, um eine Referenz für Routetable und Global Asax zu erhalten, sollte eine Routenschnittstelle haben, in der die Routenschnittstelle sowohl im Modulordner als auch im Kern angezeigt wird.
Sharif y
3
Wir haben das gelöst, indem wir für jedes Plugin einen eigenen Bereich definiert haben. In jedem Plugin haben wir eine Klasse erstellt, die von AreaRegistration erbt, und durch Überschreiben der RegisterArea-Methode konnten wir Routen definieren, die wir in den Plugins verwenden wollten.
Ilija Dimov
10
Haben Sie ein Beispielprojekt für diese Lösung?
CPODesign
2
Ich stimme cpoDesign zu. ein beispielprojekt wäre schön
chris vietor
2
Ich bin auch damit einverstanden, dass ein Beispielprojekt auf GitHub großartig zum Herunterladen wäre :)
Kbdavis07
5

Beachten Sie jedoch, dass der MEF-Container über eine "nette Funktion" verfügt, die Verweise auf jedes von ihm erstellte IDisposable-Objekt enthält und zu einem großen Speicherverlust führt. Angeblich kann der Speicherverlust mit diesem Nuget behoben werden - http://nuget.org/packages/NCode.Composition.DisposableParts.Signed

Aleksandar
quelle
Ein weiterer Grund zu sagen, DryIoc ist besser :)
Hassan Tareq
3

Es gibt Projekte, die eine Plugin-Architektur implementieren. Vielleicht möchten Sie eine davon verwenden oder sich den Quellcode ansehen, um zu sehen, wie sie diese Dinge erreichen:

Auch 404 über Steuerungen in externen Baugruppen verfolgt einen interessanten Ansatz. Ich habe viel gelernt, indem ich nur die Frage gelesen habe.

Dejan
quelle