Architecture de plug-in pour ASP.NET MVC

J’ai passé un peu de temps à lire l’article de Phil Haack sur les contrôleurs de regroupement très intéressant.

En ce moment, j’essaie de savoir 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.

Donc, ma question est la suivante: est-il possible que l’article Areas in Phil soit divisé en plusieurs projets?

Je peux voir que les espaces de noms vont fonctionner eux-mêmes, mais je suis préoccupé par le fait que les vues se retrouvent au bon endroit. Est-ce quelque chose qui peut être réglé avec les règles de construction?

En supposant que ce qui précède soit possible avec plusieurs projets dans une seule solution, est-ce que quelqu’un a des idées sur la meilleure façon de le rendre possible avec une solution distincte et un codage à 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 les masses, donc tout conseil dans ce domaine serait utile.

J’ai fait une preuve de concept il y a quelques semaines où j’ai mis une stack complète de composants: une classe de modèle, une classe de contrôleur et leurs vues associées dans une DLL, ajouté / modifié l’ un des exemples des classes VirtualPathProvider qui récupèrent les vues ils adresseraient ceux de la DLL de manière appropriée.

En fin de compte, je viens de déposer la DLL dans une application MVC correctement configurée et cela fonctionnait comme si elle avait fait partie de l’application MVC dès le début. Je l’ai poussé un peu plus loin et cela a fonctionné avec 5 de ces petits plugins mini-MVC. De toute évidence, vous devez surveiller vos références et vos dépendances de configuration 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 de contrôleurs et de vues de base, auxquels s’ajoutent des options supplémentaires dans chaque instance du site. Nous allons faire ces bits optionnels dans ces plugins DLL modulaires. Jusqu’ici tout va bien.

J’ai rédigé un aperçu de mon prototype et un exemple de solution pour les plug-ins ASP.NET MVC sur mon site.

EDIT: 4 ans après, je fais pas mal d’applications ASP.NET MVC avec des plugins et je n’utilise plus la méthode que je décris plus haut. À ce stade, je lance tous mes plugins via MEF et ne place pas de contrôleurs dans les plugins. Au lieu de cela, je crée des contrôleurs génériques qui utilisent les informations de routage pour sélectionner les plug-ins MEF et transférer le travail au plug-in, etc.

Je travaille actuellement sur un framework d’extensibilité à utiliser sur 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 satisfaire 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 à chaque client sans apporter de modifications au site Web principal.

Je me suis inspiré de l’article sur la multipropriété écrit par Ayende Rahien: http://ayende.com/Blog/archive/2008/08/16/Multi-Tenancy–Approaches-and-Applicability.aspx Une autre source d’inspiration était la livre d’Eric Evans sur Domain Driven Design. 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’objects de domaine. Les contrôleurs, référentiels ou objects de domaine sont liés à l’exécution par ExtensionFactory.

Un plug-in est simplement un outil qui contient des contrôleurs ou des référentiels ou des objects de domaine qui respectent une convention de dénomination spécifique. La convention de dénomination est simple, chaque classe doit être préfixée par le customerID, par exemple: AdventureworksHomeController.

Pour étendre une application, vous copiez un assembly 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/%5BcustomerID%5D/%5Bcontroller%5D/%5Baction%5D, le framework vérifie s’il existe un plug-in pour ce client particulier et instancie les classes de plug-in personnalisées sinon il charge une fois la valeur par défaut. Les classes personnalisées peuvent être des contrôleurs – des référentiels ou des objects de domaine. Cette approche permet d’étendre une application à tous les niveaux, de la firebase database à 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 dans 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 au sein du 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 dans le dossier d’extension et référencer la vue par un contrôleur nouveau ou sous-classé. Pour modifier un comportement existant, vous pouvez créer de nouveaux référentiels, objects de domaine ou sous-classes existants. La responsabilité du cadre consiste à déterminer quel object contrôleur / référentiel / domaine doit être chargé pour un client spécifique.
Je conseille de consulter structuremap ( http://structuremap.sourceforge.net/Default.htm ) et en particulier les fonctionnalités de registre DSL http://structuremap.sourceforge.net/RegistryDSL.htm .

C’est le code que j’utilise au démarrage de l’application pour enregistrer tous les contrôleurs / référentiels ou objects de domaine:

protected void ScanControllersAndRepositoriesFromPath(ssortingng 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 extension qui hérite de System.Web.MVC. DefaultControllerFactory. Cette fabrique est chargée de charger les objects d’extension (contrôleurs / registres ou objects de domaine). Vous pouvez twigr 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 pleinement opérationnel se trouve sur: http://code.google.com/p/multimvc/

J’ai donc un peu joué avec l’exemple de J Wynia ci-dessus. Merci beaucoup pour cette 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 nous ne le faisons qu’une fois.

C’est probablement un abus total de la façon dont VirtualFiles est censé être utilisé aussi 😉

vous vous retrouvez avec un:

IDictionary statique privé resourceVirtualFile;

avec la chaîne étant des chemins virtuels.

Le code ci-dessous fait des 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 { ssortingng path; ssortingng assemblyName; ssortingng resourceName; public ResourceVirtualFile( ssortingng virtualPath, ssortingng AssemblyName, ssortingng 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 ssortingng CreateVirtualPath(ssortingng AssemblyName, ssortingng ResourceName) { ssortingng path = ResourceName.Subssortingng(AssemblyName.Length); path = path.Replace(".aspx", "").Replace(".", "/"); return ssortingng.Format("~{0}.aspx", path); } public static IDictionary FindAllResources() { Dictionary files = new Dictionary(); //list all of the bin files ssortingng[] assemblyFilePaths = Directory.GetFiles(HttpRuntime.BinDirectory, "*.dll"); foreach (ssortingng assemblyFilePath in assemblyFilePaths) { ssortingng assemblyName = Path.GetFileNameWithoutExtension(assemblyFilePath); Assembly assembly = Assembly.ReflectionOnlyLoadFrom(assemblyFilePath); //go through each one and get all of the resources that end in aspx ssortingng[] resourceNames = assembly.GetManifestResourceNames(); foreach (ssortingng resourceName in resourceNames) { if (resourceName.EndsWith(".aspx")) { ssortingng virtualPath = CreateVirtualPath(assemblyName, resourceName); files.Add(virtualPath, new ResourceVirtualFile(virtualPath, assemblyName, resourceName)); } } } return files; } } 

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

  private bool IsExtended(ssortingng virtualPath) { Ssortingng checkPath = VirtualPathUtility.ToAppRelative(virtualPath); return resourceVirtualFile.ContainsKey(checkPath); } public override bool FileExists(ssortingng virtualPath) { return (IsExtended(virtualPath) || base.FileExists(virtualPath)); } public override VirtualFile GetFile(ssortingng virtualPath) { ssortingng withTilda = ssortingng.Format("~{0}", virtualPath); if (resourceVirtualFile.ContainsKey(withTilda)) return resourceVirtualFile[withTilda]; return base.GetFile(virtualPath); } 

Je suppose qu’il est possible de laisser vos vues dans les projets de plug-in.

C’est mon idée: vous avez besoin d’un ViewEngine qui appelle le plugin (probablement via une interface) et demande la vue (IView). Le plug-in instancierait alors la vue non pas via son URL (comme le fait un ViewEngine ordinaire – /Views/Shared/View.asp) mais par son nom de vue), par exemple via un conteneur de reflection ou 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(ssortingng 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 juste être une bonne source d’inspiration.

Cet article est peut-être un peu en retard, mais je joue avec ASP.NET MVC2 et j’ai mis au point un prototype en utilisant la fonctionnalité “Areas”.

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

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

Excellente solution – J’ai utilisé l’approche de J Wynia pour obtenir une vue d’un assemblage distinct. Cependant, cette approche semble ne rendre que la vue. Les contrôleurs du plug-in ne semblent pas être pris en charge, n’est-ce pas? Par exemple, si une vue d’un plug-in faisait un post, ce contrôleur de vues dans le plug-in ne sera pas appelé . Au lieu de cela, il sera acheminé vers un contrôleur dans l’application MVC racine . Est-ce que je comprends bien ou existe-t-il une solution à ce problème?