Trabajé en un proyecto que tenía una arquitectura conectable similar a la que describiste y usaba las mismas tecnologías ASP.NET MVC y MEF. Teníamos una aplicación ASP.NET MVC que manejaba la autenticación, autorización y todas las solicitudes. Nuestros complementos (módulos) se copiaron en una subcarpeta del mismo. Los complementos también eran aplicaciones ASP.NET MVC que tenían sus propios modelos, controladores, vistas, archivos css y js. Estos son los pasos que seguimos para que funcione:
Configuración de MEF
Creamos un motor basado en MEF que descubre todas las partes componibles al inicio de la aplicación y crea un catálogo de las partes componibles. Esta es una tarea que se realiza solo una vez al inicio de la aplicación. El motor necesita descubrir todas las partes conectables, que en nuestro caso estaban ubicadas en la bincarpeta de la aplicación host o en la Modules(Plugins)carpeta.
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;
}
}
Este es el código de muestra de la clase que realiza el descubrimiento de todas las partes de MEF. El Composemétodo de la clase se llama desde el Application_Startmétodo del Global.asax.csarchivo. El código se reduce en aras de la simplicidad.
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));
}
}
Se asume que todos los complementos se copian en una subcarpeta separada de la Modulescarpeta que se encuentra en la raíz de la aplicación host. Cada subcarpeta de complemento contiene Viewsuna subcarpeta y la DLL de cada complemento. En el Application_Startmétodo anterior también se inicializan la fábrica de controladores personalizados y el motor de vista personalizada que definiré a continuación.
Creando una fábrica de controladores que lee de MEF
Aquí está el código para definir la fábrica de controladores personalizados que descubrirá el controlador que necesita manejar la solicitud:
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();
}
}
}
Además, cada controlador debe estar marcado con el Exportatributo:
[Export("Plugin1", typeof(IController))]
[PartCreationPolicy(CreationPolicy.NonShared)]
public class Plugin1Controller : Controller
{
public ActionResult Index()
{
return View();
}
}
El primer parámetro del Exportconstructor de atributos debe ser único porque especifica el nombre del contrato e identifica de forma única a cada controlador. El PartCreationPolicydebe establecerse en NonShared porque los controladores no pueden ser reutilizados para múltiples peticiones.
Creando View Engine que sabe encontrar las vistas de los complementos
Es necesaria la creación de un motor de vista personalizado porque el motor de vista por convención busca vistas solo en la Viewscarpeta de la aplicación host. Dado que los complementos están ubicados en una Modulescarpeta separada , debemos decirle al motor de visualización que busque allí también.
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();
}
}
Resuelva el problema con vistas fuertemente tipadas en los complementos
Al usar solo el código anterior, no pudimos usar vistas fuertemente tipadas en nuestros complementos (módulos), porque los modelos existían fuera de la bincarpeta. Para solucionar este problema siga el siguiente enlace .