70 votes

Architecture de plug-in pour ASP.NET MVC

Je passe du temps à examiner l'article de Phil Haack sur Grouping Controllers - des choses très intéressantes.

En ce moment, j'essaie de comprendre s'il serait possible d'utiliser les mêmes idées pour créer une architecture de plug-ins/modulaire pour un projet sur lequel je travaille.

Ma question est donc la suivante : Est-il possible d'avoir les zones décrites dans l'article de Phil réparties sur plusieurs projets?

Je vois que les espaces de noms se régleront d'eux-mêmes, mais je suis préoccupé par le fait que les vues finissent dans le bon emplacement. Est-ce quelque chose qui peut être résolu avec des règles de construction?

En supposant que ce qui précède est possible avec plusieurs projets dans une seule solution, quelqu'un a-t-il des idées sur la meilleure façon de le rendre possible avec une solution séparée et en codant selon un ensemble d'interfaces prédéfini? Passer d'une zone à un plug-in.

J'ai quelques expériences avec l'architecture de plug-ins mais pas énormément, donc tout conseil dans ce domaine serait utile.

52voto

J Wynia Points 4679

J'ai réalisé une preuve de concept il y a quelques semaines où j'ai mis un ensemble complet de composants : une classe modèle, une classe contrôleur et leurs vues associées dans un DLL, ajouté/modifié un des exemples des classes VirtualPathProvider qui récupèrent les vues pour qu'elles s'adressent à celles du DLL de manière appropriée.

En fin de compte, j'ai simplement déposé le DLL dans une application MVC configurée de manière appropriée et cela a fonctionné comme s'il avait fait partie de l'application MVC dès le départ. J'ai poussé un peu plus loin et cela a fonctionné avec 5 de ces petits mini-plugins MVC sans problème. Bien sûr, il faut faire attention à vos références et dépendances de configuration lorsque vous mélangez tout cela, mais ça a fonctionné.

L'exercice visait la fonctionnalité des plugins pour une plateforme basée sur MVC que je construis pour un client. Il y a un ensemble central de contrôleurs et vues qui sont augmentés par d'autres en option dans chaque instance du site. Nous allons transformer ces éléments optionnels en ces plugins DLL modulaires. Jusque-là, tout va bien.

J'ai rédigé un résumé de mon prototype et une solution d'exemple pour les plugins ASP.NET MVC sur mon site.

EDIT: 4 ans plus tard, j'ai réalisé plusieurs applications ASP.NET MVC avec des plugins et n'utilise plus la méthode que je décris ci-dessus. À ce stade, j'exécute tous mes plugins via MEF et n'insère pas du tout de contrôleurs dans les plugins. Plutôt, je crée des contrôleurs génériques qui utilisent les informations de routage pour sélectionner les plugins MEF et transmettre le travail au plugin, etc. Je voulais juste ajouter cela car cette réponse est souvent consultée.

2 votes

Vos liens ne fonctionnent pas, j'espérais voir ce que vous avez construit. J'ai des problèmes similaires avec mon projet où je veux créer un projet plug-and-play pour pouvoir ajouter/supprimer des fonctionnalités à la demande, de la même manière que les gens le font dans WordPress.

14voto

Geo Points 524

Je travaille actuellement sur un framework d'extensibilité à utiliser avec ASP.NET MVC. Mon framework d'extensibilité est basé sur le célèbre conteneur Ioc : Structuremap.

Le cas d'utilisation que j'essaie de remplir est simple : créer une application qui devrait avoir des fonctionnalités de base pouvant être étendues pour chaque client (= multi-locataire). Il ne devrait y avoir qu'une seule instance de l'application hébergée, mais cette instance peut être adaptée pour chaque client sans apporter de modifications au site principal.

J'ai été inspiré par l'article sur le multi-locataire écrit par Ayende Rahien : http://ayende.com/Blog/archive/2008/08/16/Multi-Tenancy--Approaches-and-Applicability.aspx. Une autre source d'inspiration a été le livre d'Eric Evans sur la conception pilotée par le domaine. Mon framework d'extensibilité est basé sur le modèle de répertoire et le concept d'agrégats racines. Pour pouvoir utiliser le framework, l'application hébergée doit être construite autour de répertoires et d'objets de domaine. Les contrôleurs, les répertoires ou les objets de domaine sont liés à l'exécution par l'ExtensionFactory.

Un plug-in est simplement une assemblée qui contient des contrôleurs, des répertoires ou des objets de domaine respectant une convention de nommage spécifique. La convention de nommage est simple, chaque classe doit être préfixée par l'ID client, par exemple : AdventureworksHomeController.

Pour étendre une application, vous copiez une assemblée de plug-in dans le dossier d'extension de l'application. Lorsqu'un utilisateur demande une page sous le dossier racine du client par exemple : http://multitenant-site.com/[customerID]/[controller]/[action], le framework vérifie s'il existe un plug-in pour ce client particulier et instancie les classes personnalisées du plug-in sinon il charge celles par défaut. Les classes personnalisées peuvent être des contrôleurs, des répertoires ou des objets de domaine. Cette approche permet d'étendre une application à tous les niveaux, de la base de données à l'interface utilisateur, en passant par le modèle de domaine, les répertoires.

Lorsque vous souhaitez étendre certaines fonctionnalités existantes, créez un plug-in, une assemblée qui contient des sous-classes de l'application principale. Lorsque vous devez créer de toutes nouvelles fonctionnalités, ajoutez de nouveaux contrôleurs dans le plug-in. Ces contrôleurs seront chargés par le framework MVC lorsque l'URL correspondante est demandée. Si vous souhaitez étendre l'interface utilisateur, créez une nouvelle vue dans le dossier d'extension et référencez la vue par un contrôleur nouvel ou hérité. Pour modifier un comportement existant, créez de nouveaux répertoires ou objets de domaine ou sous-classez ceux existants. La responsabilité du framework est de déterminer quel contrôleur/répertoire/objet de domaine doit être chargé pour un client spécifique.
Je conseille de jeter un œil à structuremap (http://structuremap.sourceforge.net/Default.htm) et en particulier aux fonctionnalités du DSL Registry http://structuremap.sourceforge.net/RegistryDSL.htm.

Voici le code que j'utilise au démarrage de l'application pour enregistrer tous les contrôleurs/répertoires ou objets de domaine des plug-ins :

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

J'utilise également une ExtensionFactory héritant de System.Web.MVC.DefaultControllerFactory. Cette factory est responsable de charger les objets d'extension (contrôleurs/répertoires ou objets de domaine). Vous pouvez brancher vos propres factories en les enregistrant au démarrage dans le fichier Global.asax :

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

Ce framework, avec un site d'exemple entièrement opérationnel, peut être trouvé sur : http://code.google.com/p/multimvc/

2 votes

Ceci est vraiment des choses intéressantes, j'aime l'idée de surcharger la fonctionnalité pour différents locataires. L'article d'Ayende était intéressant.

4voto

Simon Farrow Points 898

Alors j'ai un peu joué avec l'exemple de J Wynia ci-dessus. Merci beaucoup pour ça d'ailleurs.

J'ai changé les choses pour que l'extension de VirtualPathProvider utilise un constructeur statique pour créer une liste de toutes les ressources disponibles se terminant par .aspx dans les différents dll du système. C'est laborieux mais on ne le fait qu'une fois.

C'est probablement un abus total de la façon dont les VirtualFiles sont censés être utilisés aussi ;-)

vous finissez par avoir un :

private static IDictionary resourceVirtualFile;

avec la chaîne étant des chemins virtuels.

Le code ci-dessous fait quelques hypothèses sur l'espace de noms des fichiers .aspx mais cela fonctionnera dans les cas simples. La bonne chose étant que vous n'avez pas à créer des chemins de vue compliqués, ils sont créés à partir du nom de la ressource.

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("Impossible de trouver la ressource : " + resourceName);
            return resourceStream;
        }
        throw new ArgumentException("Impossible de trouver l'assembly : " + assemblyName);
    }

    //todo: Rendre ceci plus net
    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 FindAllResources()
    {
        Dictionary files = new Dictionary();

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

Vous pouvez ensuite faire quelque chose comme ceci dans le VirtualPathProvider étendu :

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

3voto

Veebs Points 2106

Ce message peut être un peu tardif mais j'ai joué avec ASP.NET MVC2 et j'ai créé un prototype utilisant la fonctionnalité "Areas".

Voici le lien pour ceux qui sont intéressés: http://www.veebsbraindump.com/2010/06/asp-net-mvc2-plugins-using-areas/

3voto

gius Points 4298

Je pense qu'il est possible de laisser vos avis dans les projets de plug-in.

Voici mon idée : vous avez besoin d'un moteur de vue qui appellerait le plug-in (probablement via une interface) et demanderait la vue (IView). Le plug-in instancierait ensuite la vue non pas via son url (comme le fait un moteur de vue ordinaire - /Views/Shared/View.asp) mais via son nom de vue (par exemple via réflexion ou un conteneur DI/IoC).

Le retour de la vue dans le plug-in pourrait même être codé en dur (un exemple simple suit) :

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

...ce n'était qu'une idée mais j'espère que cela pourrait fonctionner ou juste être une bonne inspiration.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X