Pracowałem nad projektem, który miał podobną architekturę wtykową, jak ta, którą opisałeś i wykorzystywał te same technologie ASP.NET MVC i MEF. Mieliśmy hostową aplikację ASP.NET MVC, która obsługiwała uwierzytelnianie, autoryzację i wszystkie żądania. Nasze wtyczki (moduły) zostały skopiowane do jego podfolderu. Wtyczki były również aplikacjami ASP.NET MVC, które miały własne modele, kontrolery, widoki, pliki css i js. Oto kroki, które wykonaliśmy, aby to zadziałało:
Konfigurowanie MEF
Stworzyliśmy silnik oparty na MEF, który wykrywa wszystkie komponenty komponowalne na starcie aplikacji i tworzy katalog tych części. Jest to zadanie wykonywane tylko raz podczas uruchamiania aplikacji. Silnik musi wykryć wszystkie podłączane części, które w naszym przypadku znajdowały się w bin
folderze aplikacji hosta lub w Modules(Plugins)
folderze.
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;
}
}
To jest przykładowy kod klasy, która wykonuje odnajdywanie wszystkich części MEF. Compose
Metoda klasy jest wywoływana z Application_Start
metody w Global.asax.cs
pliku. Kod jest zredukowany ze względu na prostotę.
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));
}
}
Zakłada się, że wszystkie wtyczki są kopiowane do oddzielnego podfolderu Modules
folderu znajdującego się w katalogu głównym aplikacji hosta. Każdy podfolder wtyczek zawiera podfolder Views
i bibliotekę DLL z każdej wtyczki. W Application_Start
powyższej metodzie są również zainicjowane niestandardowa fabryka kontrolerów i niestandardowy silnik widoku, który zdefiniuję poniżej.
Tworzę fabrykę kontrolerów, która czyta z MEF
Oto kod definiujący fabrykę niestandardowych kontrolerów, który wykryje kontroler, który musi obsłużyć żądanie:
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();
}
}
}
Dodatkowo każdy kontroler musi być oznaczony Export
atrybutem:
[Export("Plugin1", typeof(IController))]
[PartCreationPolicy(CreationPolicy.NonShared)]
public class Plugin1Controller : Controller
{
public ActionResult Index()
{
return View();
}
}
Pierwszy parametr Export
konstruktora atrybutów musi być unikatowy, ponieważ określa nazwę kontraktu i jednoznacznie identyfikuje każdy kontroler. PartCreationPolicy
Musi być ustawiony na NonShared bo kontrolerzy nie mogą być ponownie wykorzystane dla wielu żądań.
Tworzenie silnika widoku, który wie, jak znajdować widoki z wtyczek
Utworzenie niestandardowego mechanizmu widoku jest potrzebne, ponieważ zgodnie z konwencją silnik widoku szuka widoków tylko w Views
folderze aplikacji hosta. Ponieważ wtyczki znajdują się w osobnym Modules
folderze, musimy powiedzieć silnikowi widoku, aby również tam zajrzał.
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();
}
}
Rozwiąż problem z silnie wpisanymi widokami we wtyczkach
Używając tylko powyższego kodu, nie mogliśmy używać silnie wpisanych widoków w naszych wtyczkach (modułach), ponieważ modele istniały poza bin
folderem. Aby rozwiązać ten problem, skorzystaj z następującego łącza .