web-dev-qa-db-fra.com

Architecture de plug-in pour ASP.NET MVC

J'ai passé un peu de temps à lire l'article de Phil Haack sur Grouping Controllers des trucs très intéressants.

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

Ma question est donc la suivante: est-il possible d'avoir l'article Zones de Phil divisé en plusieurs projets?

Je peux voir que les espaces de noms fonctionneront d'eux-mêmes, mais je suis préoccupé par les vues qui se retrouvent au bon endroit. Est-ce quelque chose qui peut être réglé 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 distincte et le codage sur un ensemble prédéfini d'interfaces? Passer d'une zone à un plug-in.

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

70
Simon Farrow

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

En fin de compte, je viens de déposer le DLL dans une application MVC correctement configurée et cela a fonctionné comme s'il faisait partie de l'application MVC depuis le début. Je l'ai poussé un peu plus loin et a bien fonctionné avec 5 de ces petits plugins mini-MVC. Évidemment, vous devez surveiller vos références et configurer vos dépendances lorsque vous les mélangez, mais cela a fonctionné.

L'exercice visait la fonctionnalité de plug-in pour une plate-forme basée sur MVC que je construis pour un client. Il existe un ensemble principal de contrôleurs et de vues qui sont augmentés par des contrôleurs plus facultatifs dans chaque instance du site. Nous allons faire ces bits optionnels dans ces plugins modulaires DLL. Jusqu'ici tout va bien.

J'ai rédigé un aperçu de mon prototype et un exemple de solution pour les plugins ASP.NET MVC sur mon site.

EDIT: 4 ans plus tard, j'ai fait pas mal d'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 ne place pas du tout les contrôleurs dans des plugins. Au lieu de cela, je crée des contrôleurs génériques qui utilisent les informations de routage pour sélectionner les plugins MEF et confier le travail au plugin, etc. Je pensais simplement ajouter car cette réponse est assez bonne.

51
J Wynia

Je travaille actuellement sur une infrastructure d'extensibilité à utiliser par-dessus 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éez une application qui devrait avoir des fonctionnalités de base qui peuvent être étendues pour chaque client (= multi-tenancy). 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 Web principal.

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

Un plug-in est simplement un assemblage qui contient des contrôleurs ou des référentiels ou des objets de domaine qui respecte une convention de dénomination spécifique. La convention de dénomination est simple, chaque classe doit être préfixée par l'ID client, par exemple: AdventureworksHomeController.

Pour étendre une application, vous copiez un plug-in Assembly 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 y a un plug- pour ce client particulier et instancier les classes de plug-in personnalisées, sinon il charge la valeur par défaut une fois. Les classes personnalisées peuvent être des contrôleurs - référentiels 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éférentiels.

Lorsque vous souhaitez étendre certaines fonctionnalités existantes, vous créez un plug-in un assembly qui contient des sous-classes de l'application principale. Lorsque vous devez créer des fonctionnalités totalement nouvelles, vous ajoutez de nouveaux contrôleurs dans le plug-in. Ces contrôleurs seront chargés par le framework MVC lorsque l'url correspondante sera demandée. Si vous souhaitez étendre l'interface utilisateur, vous pouvez créer une nouvelle vue à l'intérieur du dossier d'extension et référencer la vue par un contrôleur nouveau ou sous-classé. Pour modifier le comportement existant, vous pouvez créer de nouveaux référentiels ou objets de domaine ou sous-classer les anciens. La responsabilité du framework est de déterminer quel contrôleur/référentiel/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 surtout aux fonctionnalités DSL du Registre 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éférentiels de plug-in ou les objets de domaine:

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

J'utilise également une ExtensionFactory héritée de System.Web.MVC. DefaultControllerFactory. Cette usine est chargée de charger les objets d'extension (contrôleurs/registres ou objets de domaine). Vous pouvez brancher vos propres usines en les enregistrant au démarrage dans le fichier Global.asax:

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

Ce cadre en tant que site d'exemple entièrement opérationnel peut être trouvé sur: http://code.google.com/p/multimvc /

14
Geo

J'ai donc eu un petit jeu avec l'exemple de J Wynia ci-dessus. Merci beaucoup pour ce btw.

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érentes DLL du système. C'est laborieux mais seulement nous ne le faisons qu'une fois.

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

vous vous retrouvez avec un:

ressource IDictionary statique privéeVirtualFile;

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 des cas simples. Cette 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("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;
    }
}

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);
    }
4
Simon Farrow

Je suppose qu'il est possible de laisser votre point de vue dans les projets de plug-in.

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

Le retour de la vue dans le plugin 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();
        ...
    }
}

... c'était juste une idée mais j'espère que ça pourrait marcher ou simplement être une bonne inspiration.

3
gius

Ce message peut être un peu en retard, mais j'ai joué avec ASP.NET MVC2 et j'ai mis au point un prototype utilisant la fonctionnalité "Zones".

Voici le lien pour toute personne intéressée: http://www.veebsbraindump.com/2010/06/asp-net-mvc2-plugins-using-areas/

3
Veebs

[poster comme réponse parce que je ne peux pas commenter]

Excellente solution - j'ai utilisé l'approche de J Wynia et l'ai obtenue pour rendre une vue à partir d'un assemblage séparé. Cependant, cette approche semble uniquement rendre la vue. Les contrôleurs du plugin ne semblent pas être pris en charge, n'est-ce pas? Par exemple, si une vue d'un plugin a fait un post, le contrôleur de cette vue dans le plugin pas sera appelé . Au lieu de cela, il sera routé vers un contrôleur dans l'application MVC racine . Suis-je bien comprendre ou existe-t-il une solution de contournement pour ce problème?

0
tbehunin