web-dev-qa-db-fra.com

MEF avec MVC 4 ou 5 - Architecture enfichable (2014)

J'essaie de construire une application MVC4/MVC5 avec une architecture enfichable comme Orchard CMS. J'ai donc une application MVC qui sera le projet de démarrage et se chargera de l'authentification, de la navigation, etc. Ensuite, il y aura plusieurs modules construits séparément en tant que bibliothèques de classes asp.net ou en projets mvc dépouillés, avec des contrôleurs, des vues, des dépôts de données, etc.

J'ai passé toute la journée à parcourir des didacticiels sur le Web, à télécharger des échantillons, etc. et à constater que Kenny possède le meilleur exemple - http://kennytordeur.blogspot.in/2012/08/mef-in-aspnet-mvc -4-and-webapi.html

Je peux importer les contrôleurs à partir des modules (DLL séparées) si j'ajoute une référence à ces DLL. Mais l'utilisation de MEF tient au fait que des modules peuvent être ajoutés au moment de l'exécution. Je veux que les DLL avec les vues soient copiées dans un répertoire ~/Modules // dans le projet de démarrage (j'ai réussi à le faire) et MEF les récupèrerait simplement. Se battre pour que MEF charge ces bibliothèques.

Il existe également MefContrib, comme expliqué dans cette réponse Contrôleurs ASP.NET MVC 4.0 et MEF, comment réunir ces deux éléments? , ce qui est la prochaine chose que je vais essayer. Mais je suis surpris que MEF ne fonctionne pas avec MVC.

Quelqu'un a-t-il une architecture similaire fonctionnant (avec ou sans MefContrib)? Au départ, j'ai même pensé à dépouiller Orchard CMS et à l'utiliser comme cadre, mais c'est trop complexe. Ce serait également agréable de développer l'application dans MVC5 pour tirer parti de WebAPI2.

79
Yashvit

J'ai travaillé sur un projet ayant une architecture enfichable similaire à celle que vous avez décrite et utilisant les mêmes technologies ASP.NET MVC Et MEF. Nous avions une application Host ASP.NET MVC qui gérait l'authentification, l'autorisation et toutes les demandes. Nos plugins (modules) ont été copiés dans un sous-dossier de celui-ci. Les plugins étaient également des applications ASP.NET MVC Qui avaient leurs propres modèles, contrôleurs, vues, fichiers css et js. Voici les étapes que nous avons suivies pour que cela fonctionne:

Configuration de MEF

Nous avons créé un moteur basé sur MEF qui détecte toutes les pièces composables au démarrage de l'application et crée un catalogue des pièces composables. Il s’agit d’une tâche qui n’est exécutée qu’une fois au démarrage de l’application. Le moteur doit détecter toutes les pièces enfichables, qui dans notre cas se trouvaient soit dans le dossier bin de l'application hôte, soit dans le dossier Modules(Plugins).

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

Il s'agit de l'exemple de code de la classe qui effectue la découverte de toutes les parties MEF. La méthode Compose de la classe est appelée à partir de la méthode Application_Start Du fichier Global.asax.cs. Le code est réduit pour des raisons de simplicité.

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

Il est supposé que tous les plugins sont copiés dans un sous-dossier distinct du dossier Modules situé à la racine de l'application hôte. Chaque sous-dossier du plugin contient le sous-dossier Views et le dll de chaque plugin. Dans la méthode Application_Start Ci-dessus, sont également initialisées la fabrique de contrôleurs personnalisés et le moteur de visualisation personnalisé que je définirai ci-dessous.

Création d'une fabrique d'automate qui lit à partir de MEF

Voici le code permettant de définir une fabrique de contrôleurs personnalisés qui découvrira le contrôleur devant gérer la demande:

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

De plus, chaque contrôleur doit être marqué avec l'attribut Export:

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

Le premier paramètre du constructeur d'attribut Export doit être unique car il spécifie le nom du contrat et identifie de manière unique chaque contrôleur. PartCreationPolicy doit être défini sur NonShared car les contrôleurs ne peuvent pas être réutilisés pour plusieurs demandes.

Création d'un moteur de visualisation capable de rechercher les vues à partir des plugins

La création d'un moteur de vue personnalisé est nécessaire car, par convention, le moteur de vue recherche les vues uniquement dans le dossier Views de l'application hôte. Les plugins étant situés dans un dossier séparé Modules, nous devons en informer le moteur de visualisation.

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

Résoudre le problème des vues fortement typées dans les plugins

En utilisant uniquement le code ci-dessus, nous ne pouvions pas utiliser de vues fortement typées dans nos plugins (modules), car les modèles existaient en dehors du dossier bin. Pour résoudre ce problème, suivez les instructions suivantes lien .

104
Ilija Dimov

Sachez simplement que le conteneur de MEF possède une "fonctionnalité intéressante" qui conserve les références à tout objet identifiable qu'il crée, ce qui entraînera une fuite de mémoire considérable. La fuite de mémoire peut apparemment être traitée avec ce nuget - http://nuget.org/packages/NCode.Composition.DisposableParts.Signed

5
Aleksandar

Il existe des projets qui implémentent une architecture de plugin. Vous voudrez peut-être en utiliser un ou consulter leur code source pour voir comment ils accomplissent ces tâches:

En outre, 404 sur les contrôleurs d'assemblages externes adopte une approche intéressante. J'ai beaucoup appris en lisant simplement la question.

3
Dejan