Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

MEF and MVC 3 - how to load embedded views dynamically from mef container?

I'm building an MVC 3 application where MEF is used. The main idea is to have plug-in mechanism where models, controllers and views are loaded dynamically during runtime from mef container.

Each plugin/module consists of two assemblies:

  • Module1.Data.dll (contains definitions of models)
  • Module1.Web.dll (contains controllers and views)

and are put in the Plugins directory inside web application bin:

  • WebApp/Bin/Plugins/Module1.Data.dll
  • WebApp/Bin/Plugins/Module1.Web.dll
  • WebApp/Bin/Plugins/Module2.Data.dll
  • WebApp/Bin/Plugins/Module2.Web.dll
  • WebApp/Bin/Plugins/ModuleCore.Data.dll
  • WebApp/Bin/Plugins/ModuleCore.Web.dll
  • etc...

There is also core module that is referenced by all other modules: ModuleCore.Data.dll and respectively ModuleCore.Web.dll.

Then, in Global.asax, container is build in the following way:

AggregateCatalog catalog = new AggregateCatalog();
var binCatalog = new DirectoryCatalog(HttpRuntime.BinDirectory, "Module*.dll");
var pluginsCatalot = new DirectoryCatalog(Path.Combine(HttpRuntime.BinDirectory, "Plugins"), "Module*.dll");
catalog.Catalogs.Add(binCatalog);
catalog.Catalogs.Add(pluginsCatalot);
CompositionContainer container = new CompositionContainer(catalog);
container.ComposeParts(this);
AppDomain.CurrentDomain.AppendPrivatePath(Path.Combine(HttpRuntime.BinDirectory, "Plugins"));

CustomViewEngine is created and registered and used for finding views in module assembly:

ViewEngines.Engines.Clear();
ViewEngines.Engines.Add(new CustomViewEngine());

controller factory for loading controllers from container:

ControllerBuilder.Current.SetControllerFactory(new MefControllerFactory(_container));

and also custom virtual path provider for getting assemblies from container:

HostingEnvironment.RegisterVirtualPathProvider(new ModuleVirtualPathProvider());

Ok, so the whole infrastructure for handling pluggable models, controllers and views are ready. Now everything works... except one thing - strongly typed views.

To ilustrate the problem in more details, let's prepare the scene:

  • UserDTO model is located in Module1.Data.dll
  • ShowUserController.cs is located in Module1.Web.dll/Controllers/
  • Index.cshtml is located in Module1.Web.dll/Views/ShowUser (with declared @model Module1.Data.UserDto)

Now we do the following:

  1. Run the application and go to HOST/ShowUser/Index (action method Index is executed on ShowUserController and view Index.cshtml is fetched)
  2. After the view Index.cshtml is fetched - compilation starts (by RazorBuildProvider)
  3. Exceptions is thrown: "cannot find Data type in namespace Module1", in other words UserDTO couldn't be found during building the view dynamically

So it seems that compiler/builder didnt look through bin/Plugins folder for Module1.Data.dll, because when I copied this file into bin folder - it worded fine.

Question/problem: why builder didn't look into bin/Plugins folder even though this directory was added by AppDomain.CurrentDomain.AppendPrivatePath method? How to add private paths for assembly builder once so that plugins folder will be taken into consideration??

I have managed to do some work around by creating CustomRazorBuildProvider that overrides standard one:

public class CustomRazorBuildProvider : RazorBuildProvider
{
  public override void GenerateCode(System.Web.Compilation.AssemblyBuilder assemblyBuilder)
  {
    Assembly a = Assembly.LoadFrom(Path.Combine(HttpRuntime.BinDirectory, "Plugins", "Module1.Data.dll"));
    assemblyBuilder.AddAssemblyReference(a);      
    base.GenerateCode(assemblyBuilder);
  }
} 

but the drawback of this solution is that everytime the view is compiled, references to all assemblies in Plugins folder need to be added, which can cause performance issues later on when lots of plugins will be used.

Any nicer solutions?

like image 227
untoldex Avatar asked Nov 27 '11 12:11

untoldex


1 Answers

Here is a thought.

If you follow the View Model Pattern then instead of sending the DTO's straight to the view use a ViewModel that is would be located in the same assembly as the View.

So Instead of:

UserDTO model is located in Module1.Data.dll ShowUserController.cs is located in Module1.Web.dll/Controllers/ Index.cshtml is located in Module1.Web.dll/Views/ShowUser (with declared @model Module1.Data.UserDto)

You would have:

UserDTO model is located in Module1.Data.dll ShowUserController.cs is located in Module1.Web.dll/Controllers/ UserVM located in Module1.Web.dll/ViewModels Index.cshtml is located in Module1.Web.dll/Views/ShowUser (with declared @model Module1.Web.ViewModels.UserVM)

Have the Controller Map your DTO's to ViewModels

See AutoMapper to help with the Mapping

like image 181
SimonGates Avatar answered Oct 23 '22 06:10

SimonGates