Plug-In-Architektur für ASP.NET MVC

70

Ich habe einige Zeit damit verbracht, mir Phil Haacks Artikel über das Gruppieren von Controllern mit sehr interessanten Dingen anzusehen.

Im Moment versuche ich herauszufinden, ob es möglich wäre, dieselben Ideen zu verwenden, um eine Plug-in / modulare Architektur für ein Projekt zu erstellen, an dem ich arbeite.

Meine Frage lautet also: Ist es möglich, die Bereiche in Phils Artikel auf mehrere Projekte aufzuteilen?

Ich kann sehen, dass die Namensräume von selbst funktionieren, aber ich bin besorgt darüber, dass die Ansichten an der richtigen Stelle landen. Ist es etwas, das mit Build-Regeln aussortiert werden kann?

Unter der Annahme, dass das oben Genannte mit mehreren Projekten in einer einzigen Lösung möglich ist, hat jemand Ideen, wie dies mit einer separaten Lösung und Codierung für einen vordefinierten Satz von Schnittstellen am besten möglich ist? Wechseln von einem Bereich zu einem Plug-In.

Ich habe einige Erfahrungen mit Plug-In-Architekturen, aber keine Massen, daher wäre jede Anleitung in diesem Bereich nützlich.

Simon Farrow
quelle

Antworten:

52

Ich habe vor einigen Wochen einen Proof of Concept durchgeführt, bei dem ich einen vollständigen Stapel von Komponenten: eine Modellklasse, eine Controller-Klasse und die zugehörigen Ansichten in eine DLL eingefügt und eines der Beispiele für die VirtualPathProvider-Klassen hinzugefügt / optimiert habe , mit denen die Ansichten abgerufen werden Sie würden diese in der DLL angemessen ansprechen.

Am Ende habe ich die DLL einfach in eine entsprechend konfigurierte MVC-App verschoben und es hat so funktioniert, als wäre sie von Anfang an Teil der MVC-App gewesen. Ich habe es etwas weiter vorangetrieben und es hat mit 5 dieser kleinen Mini-MVC-Plugins ganz gut funktioniert. Natürlich müssen Sie Ihre Referenzen und Konfigurationsabhängigkeiten beobachten, wenn Sie alles herummischen, aber es hat funktioniert.

Die Übung zielte auf die Plugin-Funktionalität für eine MVC-basierte Plattform ab, die ich für einen Client erstelle. Es gibt einen Kernsatz von Controllern und Ansichten, die in jeder Instanz der Site durch optionalere ergänzt werden. Wir werden diese optionalen Bits in diese modularen DLL-Plugins einbauen. So weit, ist es gut.

Ich habe auf meiner Site einen Überblick über meinen Prototyp und eine Beispiellösung für ASP.NET MVC-Plugins geschrieben .

BEARBEITEN: 4 Jahre später habe ich einige ASP.NET MVC-Apps mit Plugins erstellt und verwende die oben beschriebene Methode nicht mehr. Zu diesem Zeitpunkt führe ich alle meine Plugins über MEF aus und setze überhaupt keine Controller in Plugins ein. Vielmehr mache ich generische Controller, die die Routing-Informationen verwenden, um MEF-Plugins auszuwählen und die Arbeit an das Plugin usw. weiterzugeben. Ich dachte nur, ich würde hinzufügen, da diese Antwort ein gutes Stück getroffen wird.

J Wynia
quelle
1
Ihre Links funktionieren nicht Ich hatte gehofft zu sehen, was Sie erstellen. Ich habe ähnliche Probleme mit meinem Projekt, bei dem ich ein steckbares Projekt erstellen möchte, damit ich bei Bedarf Funktionen hinzufügen / entfernen kann. genauso wie die Leute in WordPress.
Alok
14

Ich arbeite derzeit an einem Erweiterungsframework, das zusätzlich zu ASP.NET MVC verwendet werden kann. Mein Erweiterbarkeits-Framework basiert auf dem berühmten Ioc-Container: Structuremap.

Der Anwendungsfall, den ich zu erfüllen versuche, ist einfach: Erstellen Sie eine Anwendung mit einigen grundlegenden Funktionen, die für jeden Kunden erweitert werden können (= Mandantenfähigkeit). Es sollte nur eine Instanz der Anwendung gehostet werden, aber diese Instanz kann für jeden Kunden angepasst werden, ohne Änderungen an der Kernwebsite vorzunehmen.

Ich wurde von dem Artikel über Multitätigkeit von Ayende Rahien inspiriert: http://ayende.com/Blog/archive/2008/08/16/Multi-Tenancy--Approaches-and-Applicability.aspx Eine weitere Inspirationsquelle war die Buch von Eric Evans über Domain Driven Design. Mein Extensibility-Framework basiert auf dem Repository-Muster und dem Konzept der Root-Aggregate. Um das Framework verwenden zu können, sollte die Hosting-Anwendung auf Repositorys und Domänenobjekten basieren. Die Controller, Repositorys oder Domänenobjekte werden zur Laufzeit von der ExtensionFactory gebunden.

Ein Plug-In ist einfach eine Zusammenstellung, die Controller oder Repositorys oder Domänenobjekte enthält, die eine bestimmte Namenskonvention einhalten. Die Namenskonvention ist einfach. Jeder Klasse sollte die Kunden-ID vorangestellt werden, z. B.: AdventureworksHomeController.

Um eine Anwendung zu erweitern, kopieren Sie eine Plug-In-Assembly in den Erweiterungsordner der Anwendung. Wenn ein Benutzer eine Seite im Stammverzeichnis des Kunden anfordert, z. B.: Http://multitenant-site.com/[customerID‹/[controller‹/[action], überprüft das Framework, ob für diesen bestimmten Kunden ein Plug-In vorhanden ist, und instanziiert die benutzerdefinierten Plug-In-Klassen, andernfalls wird der Standard einmal geladen. Die benutzerdefinierten Klassen können Controller - Repositorys oder Domänenobjekte sein. Dieser Ansatz ermöglicht die Erweiterung einer Anwendung auf allen Ebenen, von der Datenbank bis zur Benutzeroberfläche, über das Domänenmodell und die Repositorys.

Wenn Sie einige vorhandene Funktionen erweitern möchten, erstellen Sie ein Plug-In für eine Assembly, die Unterklassen der Kernanwendung enthält. Wenn Sie völlig neue Funktionen erstellen müssen, fügen Sie dem Plug-In neue Controller hinzu. Diese Controller werden vom MVC-Framework geladen, wenn die entsprechende URL angefordert wird. Wenn Sie die Benutzeroberfläche erweitern möchten, können Sie eine neue Ansicht im Erweiterungsordner erstellen und auf die Ansicht eines neuen oder untergeordneten Controllers verweisen. Um das vorhandene Verhalten zu ändern, können Sie neue Repositorys oder Domänenobjekte erstellen oder vorhandene Unterklassen unterordnen. Die Framework-Verantwortung besteht darin, zu bestimmen, welcher Controller / Repository / Domänenobjekt für einen bestimmten Kunden geladen werden soll.
Ich empfehle einen Blick auf die Strukturkarte ( http://structuremap.sourceforge.net/Default.htm)) und insbesondere bei der Registrierung DSL-Funktionen http://structuremap.sourceforge.net/RegistryDSL.htm .

Dies ist der Code, den ich beim Start der Anwendung verwende, um alle Plug-in-Controller / Repositorys oder Domänenobjekte zu registrieren:

protected void ScanControllersAndRepositoriesFromPath(string path)
        {
            this.Scan(o =>
            {
                o.AssembliesFromPath(path);
                o.AddAllTypesOf<SaasController>().NameBy(type => type.Name.Replace("Controller", ""));
                o.AddAllTypesOf<IRepository>().NameBy(type => type.Name.Replace("Repository", ""));
                o.AddAllTypesOf<IDomainFactory>().NameBy(type => type.Name.Replace("DomainFactory", ""));
            });
        }

Ich verwende auch eine ExtensionFactory, die von System.Web.MVC erbt. DefaultControllerFactory. Diese Factory ist dafür verantwortlich, die Erweiterungsobjekte (Controller / Registries oder Domänenobjekte) zu laden. Sie können Ihre eigenen Fabriken anschließen, indem Sie sie beim Start in der Datei Global.asax registrieren:

protected void Application_Start()
        {
            ControllerBuilder.Current.SetControllerFactory(
                new ExtensionControllerFactory()
                );
        }

Dieses Framework als voll funktionsfähige Beispielsite finden Sie unter: http://code.google.com/p/multimvc /

Geo
quelle
2
Das ist wirklich interessant, ich mag die Idee, Funktionen für verschiedene Mieter zu überladen. Ayendes Artikel war interessant.
Simon Farrow
4

Also habe ich ein bisschen mit dem Beispiel von J Wynia oben herumgespielt . Vielen Dank dafür übrigens.

Ich habe die Dinge so geändert, dass die Erweiterung des VirtualPathProvider einen statischen Konstruktor verwendete, um eine Liste aller verfügbaren Ressourcen zu erstellen, die mit .aspx in den verschiedenen DLLs im System enden. Es ist mühsam, aber wir machen es nur einmal.

Es ist wahrscheinlich ein totaler Missbrauch der Art und Weise, wie VirtualFiles auch verwendet werden sollen ;-)

Sie erhalten:

privates statisches IDictionary resourceVirtualFile;

wobei die Zeichenfolge virtuelle Pfade sind.

Der folgende Code macht einige Annahmen über den Namespace der ASPX-Dateien, funktioniert jedoch in einfachen Fällen. Das Schöne daran ist, dass Sie keine komplizierten Ansichtspfade erstellen müssen, die aus dem Ressourcennamen erstellt werden.

class ResourceVirtualFile : VirtualFile
{
    string path;
    string assemblyName;
    string resourceName;

    public ResourceVirtualFile(
        string virtualPath,
        string AssemblyName,
        string ResourceName)
        : base(virtualPath)
    {
        path = VirtualPathUtility.ToAppRelative(virtualPath);
        assemblyName = AssemblyName;
        resourceName = ResourceName;
    }

    public override Stream Open()
    {
        assemblyName = Path.Combine(HttpRuntime.BinDirectory, assemblyName + ".dll");

        Assembly assembly = Assembly.ReflectionOnlyLoadFrom(assemblyName);
        if (assembly != null)
        {
            Stream resourceStream = assembly.GetManifestResourceStream(resourceName);
            if (resourceStream == null)
                throw new ArgumentException("Cannot find resource: " + resourceName);
            return resourceStream;
        }
        throw new ArgumentException("Cannot find assembly: " + assemblyName);
    }

    //todo: Neaten this up
    private static string CreateVirtualPath(string AssemblyName, string ResourceName)
    {
        string path = ResourceName.Substring(AssemblyName.Length);
        path = path.Replace(".aspx", "").Replace(".", "/");
        return string.Format("~{0}.aspx", path);
    }

    public static IDictionary<string, VirtualFile> FindAllResources()
    {
        Dictionary<string, VirtualFile> files = new Dictionary<string, VirtualFile>();

        //list all of the bin files
        string[] assemblyFilePaths = Directory.GetFiles(HttpRuntime.BinDirectory, "*.dll");
        foreach (string assemblyFilePath in assemblyFilePaths)
        {
            string assemblyName = Path.GetFileNameWithoutExtension(assemblyFilePath);
            Assembly assembly = Assembly.ReflectionOnlyLoadFrom(assemblyFilePath);  

            //go through each one and get all of the resources that end in aspx
            string[] resourceNames = assembly.GetManifestResourceNames();

            foreach (string resourceName in resourceNames)
            {
                if (resourceName.EndsWith(".aspx"))
                {
                    string virtualPath = CreateVirtualPath(assemblyName, resourceName);
                    files.Add(virtualPath, new ResourceVirtualFile(virtualPath, assemblyName, resourceName));
                }
            }
        }

        return files;
    }
}

Im erweiterten VirtualPathProvider können Sie dann Folgendes tun:

    private bool IsExtended(string virtualPath)
    {
        String checkPath = VirtualPathUtility.ToAppRelative(virtualPath);
        return resourceVirtualFile.ContainsKey(checkPath);
    }

    public override bool FileExists(string virtualPath)
    {
        return (IsExtended(virtualPath) || base.FileExists(virtualPath));
    }

    public override VirtualFile GetFile(string virtualPath)
    {
        string withTilda = string.Format("~{0}", virtualPath);

        if (resourceVirtualFile.ContainsKey(withTilda))
            return resourceVirtualFile[withTilda];

        return base.GetFile(virtualPath);
    }
Simon Farrow
quelle
3

Ich denke, es ist möglich, Ihre Ansichten in den Plug-In-Projekten zu belassen.

Das ist meine Idee: Sie benötigen eine ViewEngine, die das Plugin (wahrscheinlich über eine Schnittstelle) aufruft und die Ansicht anfordert (IView). Das Plugin instanziiert die Ansicht dann nicht über seine URL (wie es eine normale ViewEngine tut - /Views/Shared/View.asp), sondern über den Namen der Ansicht), beispielsweise über Reflection oder DI / IoC-Container.

Die Rückgabe der Ansicht im Plugin könnte mich sogar fest codieren (einfaches Beispiel folgt):

public IView GetView(string viewName)
{
    switch (viewName)
    {
        case "Namespace.View1":
            return new View1();
        case "Namespace.View2":
            return new View2();
        ...
    }
}

... das war nur eine Idee, aber ich hoffe, es könnte funktionieren oder nur eine gute Inspiration sein.

gius
quelle
0

[als Antwort posten, weil ich keinen Kommentar abgeben kann]

Tolle Lösung - Ich habe den Ansatz von J Wynia verwendet und ihn dazu gebracht, eine Ansicht von einer separaten Baugruppe zu rendern. Dieser Ansatz scheint jedoch nur die Ansicht zu rendern. Controller innerhalb des Plugins scheinen nicht unterstützt zu werden, richtig? Zum Beispiel, wenn eine Ansicht von einem Plugin einen Beitrag zurück tat, dass der Controller die Ansichten innerhalb des Plugins wird nicht aufgerufen werden . Stattdessen wird es an einen Controller in der Root-MVC-Anwendung weitergeleitet . Verstehe ich das richtig oder gibt es eine Problemumgehung für dieses Problem?

tbehunin
quelle
Sie können die Routen lokal im Plugin registrieren. Sie müssen sie nur auf irgendeine Weise konfigurieren. Sie können wahrscheinlich StructureMap verwenden, um zu vermeiden, dass Sie viel mit Reflexion herumspielen.
Simon Farrow
Ignoriere meinen Kommentar / meine Antwort. Ich habe es falsch gemacht, aber es hat so funktioniert, wie es beabsichtigt war. Funktioniert super! Entschuldigung für den Lärm!
Tbehunin