c# asp.net-mvc-4 mef

c# - MVC4 Complementos cargados dinĂ¡micamente basados en MEF



asp.net-mvc-4 (2)

actualizado: lea a continuación en este post para una solución mínima

Tengo algunas preguntas para principiantes sobre una solución MVC4 con complementos. Busqué en Google un poco y encontré algunas cosas buenas, pero no se ajustan exactamente a mis necesidades, así que estoy pidiendo algunos consejos.

Parece que la mejor solución para los complementos similares a widgets en MVC son las áreas portátiles (en el paquete MvcContrib). He encontrado la guía básica aquí:

http://lostechies.com/erichexter/2009/11/01/asp-net-mvc-portable-areas-via-mvccontrib/

y algunos consejos útiles aquí:

http://geekswithblogs.net/michelotti/archive/2010/04/05/mvc-portable-areas-ndash-web-application-projects.aspx

Más cosas en este post:

¿Cómo crear el área de ASP.NET MVC como un DLL de complemento?

Eso es genial, pero lamentablemente mis requisitos son un poco diferentes:

  1. desafortunadamente, necesito un sistema donde los complementos se agreguen y descubran dinámicamente, y este no es el caso de las áreas portátiles, a las que debe hacer referencia el proyecto del sitio MVC principal. Me gustaría simplemente cargar algo en el sitio y hacer que descubra y use nuevos componentes, así que usaré MEF para esto.

  2. afortunadamente, mis complementos no serán como widgets, que pueden ser muy complejos y heterogéneos; más bien, son componentes que deben seguir un patrón común y compartido. Piense en ellos como editores especializados: para cada tipo de datos ofreceré un componente con funciones de edición: nuevo, editar, eliminar. Así que estaba pensando en controladores de complementos que implementan una interfaz común y proporcionan acciones como Nuevo, Editar, Eliminar y similares.

  3. Debo usar MVC4 y en el futuro tendré que agregar la localización y las personalizaciones móviles.

  4. Debo evitar las dependencias de marcos complejos y mantener el código lo más simple posible.

Por lo tanto, cada vez que quiera agregar un nuevo tipo de datos para editar en este sitio web, me gustaría colocar una DLL en su carpeta de complementos para la lógica (controlador, etc.) y algunas vistas en las ubicaciones correctas, para obtener el sitio. Descubre y utiliza el nuevo editor.

Eventualmente, podría incluir las vistas en la propia DLL (encontré esto: http://razorgenerator.codeplex.com , y este tutorial: http://www.chrisvandesteeg.nl/2010/11/22/embedding-pre-compiled-razor-views-in-your-dll/ , que supongo que podría usar con el generador de código codeplex razorgenerator ya que el código al que se refiere no es compatible con VS2012), pero es mejor que sea mejor mantenerlos separados (también debido a la requisitos de localización y sensibilización móvil); Estaba pensando en agregar un mecanismo de carga en el área de administración de mi sitio, donde puede cargar un solo archivo zip con la DLL con controladores y carpetas con vistas, y luego dejar que el servidor descomprima y almacene los archivos donde sea necesario. Esto me permitiría modificar fácilmente las vistas sin tener que volver a implementar todo el complemento.

Así que empecé a buscar MEF y MVC, pero la mayoría de las publicaciones se refieren a MVC2 y no son compatibles. Tuve más suerte con esto, que se centra principalmente en la API web, pero parece bastante prometedor y simple:

http://kennytordeur.blogspot.it/2012/08/mef-in-aspnet-mvc-4-and-webapi.html

Esto esencialmente agrega una fábrica de controladores y resolución de dependencias basada en MEF a la aplicación MVC "estándar". De todos modos, el ejemplo en la publicación se refiere a una solución de ensamblaje único, mientras que necesito implementar varios complementos diferentes. Así que modifiqué ligeramente el código para usar un MEF DirectoryCatalog (en lugar de un AssemblyCatalog) que apunta a mi carpeta de complementos y luego creé una solución de prueba MVC, con un solo complemento en una biblioteca de clases.

De todos modos, cuando intento cargar el controlador del complemento, el marco llama a mi fábrica GetControllerInstance con un tipo nulo, por lo que, por supuesto, MEF no puede proceder a la composición. Probablemente me esté perdiendo algo obvio, pero soy nuevo en MVC 4 y cualquier sugerencia o enlace útil (compatible con MVC4) son bienvenidos. ¡Gracias!

Aquí está el código esencial:

public static class MefConfig { public static void RegisterMef() { CompositionContainer container = ConfigureContainer(); ControllerBuilder.Current.SetControllerFactory(new MefControllerFactory(container)); System.Web.Http.GlobalConfiguration.Configuration.DependencyResolver = new MefDependencyResolver(container); } private static CompositionContainer ConfigureContainer() { //AssemblyCatalog assemblyCatalog = new AssemblyCatalog(Assembly.GetExecutingAssembly()); DirectoryCatalog catalog = new DirectoryCatalog( HostingEnvironment.MapPath("~/Plugins")); CompositionContainer container = new CompositionContainer(catalog); return container; } } public class MefDependencyResolver : IDependencyResolver { private readonly CompositionContainer _container; public MefDependencyResolver(CompositionContainer container) { _container = container; } public IDependencyScope BeginScope() { return this; } public object GetService(Type serviceType) { var export = _container.GetExports(serviceType, null, null).SingleOrDefault(); return (export != null ? export.Value : null); } public IEnumerable GetServices(Type serviceType) { var exports = _container.GetExports(serviceType, null, null); List createdObjects = new List(); if (exports.Any()) createdObjects.AddRange(exports.Select(export => export.Value)); return createdObjects; } public void Dispose() { } } public class MefControllerFactory : DefaultControllerFactory { private readonly CompositionContainer _compositionContainer; public MefControllerFactory(CompositionContainer compositionContainer) { _compositionContainer = compositionContainer; } protected override IController GetControllerInstance( System.Web.Routing.RequestContext requestContext, Type controllerType) { if (controllerType == null) throw new ArgumentNullException("controllerType"); var export = _compositionContainer.GetExports(controllerType, null, null).SingleOrDefault(); IController result; if (null != export) result = export.Value as IController; else { result = base.GetControllerInstance(requestContext, controllerType); _compositionContainer.ComposeParts(result); } //eelse return result; } }

Puede descargar la solución de prueba completa desde aquí:

http://www.filedropper.com/mvcplugins

Edición: una primera solución mínima de trabajo

Aquí están mis conclusiones, espero que puedan ser útiles para otros novatos que comienzan con estas cosas: no logré ejecutar con éxito el marco citado en la respuesta anterior, supongo que debe haber algo actualizado para VS2012 y MVC4. De todos modos, miré el código y busqué un poco más en Google:

1) en primer lugar, una fuente de confusión para mí fueron las 2 interfaces diferentes con el mismo nombre: IDependencyResolver. Si entiendo bien, uno (System.Web.Http.Dependencies.IDependencyResolver) se utiliza para webapi, y otro (System.Web.Mvc.IDependencyResolver) para DI genérico. Este post me ayudó aquí: http://lucid-nonsense.co.uk/dependency-injection-web-api-and-mvc-4-rc/ .

2) también, un tercer componente es la fábrica de controladores derivados de DefaultControllerFactory, que es crucial para esta publicación porque es la fábrica utilizada para los controladores alojados en complementos.

Aquí están mis implementaciones para todo esto, ligeramente modificadas de varias muestras: primero el resolvedor HTTP:

public sealed class MefHttpDependencyResolver : IDependencyResolver { private readonly CompositionContainer _container; public MefHttpDependencyResolver(CompositionContainer container) { if (container == null) throw new ArgumentNullException("container"); _container = container; } public object GetService(Type serviceType) { if (serviceType == null) throw new ArgumentNullException("serviceType"); string name = AttributedModelServices.GetContractName(serviceType); try { return _container.GetExportedValue(name); } catch { return null; } } public IEnumerable GetServices(Type serviceType) { if (serviceType == null) throw new ArgumentNullException("serviceType"); string name = AttributedModelServices.GetContractName(serviceType); try { return _container.GetExportedValues(name); } catch { return null; } } public IDependencyScope BeginScope() { return this; } public void Dispose() { } }

Luego, el resolvedor MVC, que es muy similar, incluso si no es estrictamente necesario para la muestra ficticia en este escenario:

public class MefDependencyResolver : IDependencyResolver { private readonly CompositionContainer _container; public MefDependencyResolver(CompositionContainer container) { if (container == null) throw new ArgumentNullException("container"); _container = container; } public object GetService(Type type) { if (type == null) throw new ArgumentNullException("type"); string name = AttributedModelServices.GetContractName(type); try { return _container.GetExportedValue(name); } catch { return null; } } public IEnumerable GetServices(Type type) { if (type == null) throw new ArgumentNullException("type"); string name = AttributedModelServices.GetContractName(type); try { return _container.GetExportedValues(name); } catch { return null; } } }

Y finalmente la fábrica de controladores:

[Export(typeof(IControllerFactory))] public class MefControllerFactory : DefaultControllerFactory { private readonly CompositionContainer _container; [ImportingConstructor] public MefControllerFactory(CompositionContainer container) { if (container == null) throw new ArgumentNullException("container"); _container = container; } public override IController CreateController(RequestContext requestContext, string controllerName) { var controller = _container .GetExports() .Where(c => c.Metadata.Name.Equals(controllerName, StringComparison.OrdinalIgnoreCase)) .Select(c => c.Value) .FirstOrDefault(); return controller ?? base.CreateController(requestContext, controllerName); } }

En cuanto al controlador de muestra, lo creé en un proyecto de biblioteca de clases:

[Export(typeof(IController))] [PartCreationPolicy(CreationPolicy.NonShared)] [ExportMetadata("Name", "Alpha")] public sealed class AlphaController : Controller { public ActionResult Index() { ViewBag.Message = "Hello, this is the PLUGIN controller!"; return View(); } }

En el proyecto principal (el sitio MVC) tengo una carpeta de complementos donde copio esta DLL, más un conjunto de vistas "estándar" en sus carpetas para las vistas de este controlador.

Este es el escenario más simple posible, y probablemente haya mucho más para descubrir y refinar, pero para empezar necesitaba ser simple. De todos modos, cualquier sugerencia es bienvenida.


Actualmente estoy trabajando en el mismo tema. He encontrado esta solución:

Básicamente, carga ensamblajes desde una ubicación específica y con algún patrón de nombre en el inicio de la aplicación web:

AssemblyInfo.cs:

[assembly: PreApplicationStartMethod( typeof(PluginAreaBootstrapper), "Init")]

PluginAreaBootstrapper.cs:

public class PluginAreaBootstrapper { public static readonly List<Assembly> PluginAssemblies = new List<Assembly>(); public static List<string> PluginNames() { return PluginAssemblies.Select( pluginAssembly => pluginAssembly.GetName().Name) .ToList(); } public static void Init() { var fullPluginPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Areas"); foreach (var file in Directory.EnumerateFiles(fullPluginPath, "*Plugin*.dll", SearchOption.AllDirectories)) PluginAssemblies.Add(Assembly.LoadFile(file)); PluginAssemblies.ForEach(BuildManager.AddReferencedAssembly); // Add assembly handler for strongly-typed view models AppDomain.CurrentDomain.AssemblyResolve += AssemblyResolve; } private static Assembly AssemblyResolve(object sender, ResolveEventArgs resolveArgs) { var currentAssemblies = AppDomain.CurrentDomain.GetAssemblies(); // Check we don''t already have the assembly loaded foreach (var assembly in currentAssemblies) { if (assembly.FullName == resolveArgs.Name || assembly.GetName().Name == resolveArgs.Name) { return assembly; } } return null; } }

Pero creo que puede crear un observador de directorio que puede cargar ensamblajes dinámicamente, por lo que ni siquiera necesita reiniciar su aplicación web.

En mi opinión satisface tus necesidades 1, 2 y 4. Es muy simple, no requiere ningún marco, tiene una configuración mínima, permite la carga dinámica de los complementos y funciona con MVC 4.

Esta solución conecta los ensamblajes en el directorio del Área, pero creo que puede sintonizarlo fácilmente para jugar como quiera usando enrutamiento.


Puede encontrar más soluciones completas, here , y más antecedentes here . Nuestros requisitos también fueron un DI, así que this también ayudará un poco, aunque no es necesario (los primeros enlaces también ofrecen una solución para DI. Buena suerte con esto, no es difícil. Tenga en cuenta que estos son para MVC3 pero se pueden convertir / migrar fácilmente a MVC 4