Ответ 1
Я работал над проектом, который имел аналогичную архитектуру, подобную той, которую вы описали, и использовал те же технологии 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
также инициализируется пользовательский контроллер factory и настраиваемый механизм просмотра, который я буду определять ниже.
Создание контроллера factory, который читает из MEF
Вот код для определения пользовательского контроллера factory, который обнаружит контроллер, который должен обрабатывать запрос:
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
{
//
// GET: /Plugin1/
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
. Чтобы решить эту проблему, выполните следующую ссылку .