Я працював над проектом, який мав подібну підключається архітектуру, подібну до описаної вами, і використовував ті самі технології ASP.NET MVC та MEF. У нас була хост-програма ASP.NET MVC, яка обробляла автентифікацію, авторизацію та всі запити. Наші плагіни (модулі) були скопійовані в його підкаталог. Плагінами також були додатки ASP.NET MVC, які мали власні моделі, контролери, подання, файли css та js. Ось кроки, які ми дотримувались, щоб це працювало:
Налаштування MEF
Ми створили движок на основі MEF, який виявляє всі складові деталі під час запуску програми та створює каталог компонуючих деталей. Це завдання, яке виконується лише один раз під час запуску програми. Двигун повинен виявити всі підключаються деталі, які в нашому випадку знаходились або в bin
папці хост-програми, або в 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;
}
}
Це зразок коду класу, який виконує виявлення всіх частин MEF. Compose
Метод класу викликається з Application_Start
методу в Global.asax.cs
файлі. Код зменшений заради простоти.
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));
}
}
Передбачається, що всі плагіни копіюються в окрему підпапку Modules
папки, яка знаходиться в кореневій частині хост-програми. Кожна підпапка плагіна містить Views
підпапку та DLL кожного плагіна. У Application_Start
наведеному вище методі також ініціалізовані фабрика нестандартних контролерів та спеціальний механізм подання, які я визначу нижче.
Створення заводу контролерів, який читає з MEF
Ось код для визначення фабрики користувацьких контролерів, яка виявить контролер, який повинен обробляти запит:
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();
}
}
}
Крім того, кожен контролер повинен бути позначений Export
атрибутом:
[Export("Plugin1", typeof(IController))]
[PartCreationPolicy(CreationPolicy.NonShared)]
public class Plugin1Controller : Controller
{
public ActionResult Index()
{
return View();
}
}
Перший параметр Export
конструктора атрибутів повинен бути унікальним, оскільки він визначає ім'я контракту та однозначно ідентифікує кожен контролер. PartCreationPolicy
Повинен бути встановлений в NonShared , оскільки контролери не можуть бути повторно використані для декількох запитів.
Створення механізму перегляду, який вміє знаходити подання за допомогою плагінів
Створення власного механізму перегляду необхідне, оскільки механізм перегляду за домовленістю шукає подання лише в Views
папці хост-програми. Оскільки плагіни розташовані в окремій Modules
папці, нам потрібно сказати механізму перегляду, щоб він також шукав там.
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();
}
}
Вирішіть проблему з сильно набраними поданнями у плагінах
Використовуючи лише наведений вище код, ми не могли використовувати сильно набрані подання у наших плагінах (модулях), оскільки моделі існували поза bin
папкою. Щоб вирішити цю проблему, перейдіть за таким посиланням .