ASP.NET MVC Plugins

Originally published: 12/2008 by J Wynia


One of the last things I posted here was how I was toying with flexible content and template solutions in ASP.NET MVC. For that project, we're going forward with the solution that emerged from that experimentation and I'm excited to see how it turns out. 

Along the way, an interesting question about how to create what are essentially plugins for the MVC framework. Basically, if what you're building is an MVC app that will be deployed in dozens, hundreds or thousands of independent implementations, it becomes obvious pretty quickly that you need most of the pieces of the application to be modular so they can added/removed/overridden at least to a minimal level.

I've come up with what I think is a workable solution, which is pretty much just gathering up some of the other ideas already out there into a quick-n-dirty working prototype. The basic idea is this:

Put an encapsulated set of model classes (or none if you depend on central model classes), controller classes and a set of views into its own DLL. The views (using the default WebForms view engine, they're aspx files) go in as resources.

Pull that DLL into your MVC application and the MVC stack inside your "plugin" will work just like if those bits had been a "natural" part of the base MVC application. In fact actually creating the pieces from inside the main app and then moving them over to their own project is the quickest way I found to actually create the bits for the plugins.

When the question came up, I vaguely remembered seeing a question on StackOverflow about how to put views into an assembly/DLL. That question along with one on VirtualPathProviders provided most of the kernel of what I needed.

So, on to how to actually do this.

Start with a standard ASP.NET MVC project (in beta as of the time of this writing). Add whatever model, controller and view elements you would if this bit of functionality wasn't a plugin (this also makes this approach far easier to work if you decide later to pull something out of the core and into a plugin).

Add a Class Library project that's compatible with MVC (adding System.Web.MVC, etc) for your plugin and cut/paste the components into your new project. The views need to be changed to be embedded resources via the properties pane.

Next, you'll need to tweak your controller methods just a bit if you're using the standard "return View();" return approach. Change that return line to:


ViewResult RenderedView = View("~/Plugin/YOURDLL.dll/FULLNAME_YOUR_VIEW.aspx");
return RenderedView;

Getting that path right can be a bit tricky at first. The "~/Plugin/" part is pretty much like a route. It's probably possible/better to actually make that a route, but I haven't looked into it. Then you've got the filename of your DLL. It's the actual view name that isn't straightforward. It's not the file name as you saw it in Visual Studio. Instead, it's the fully-qualified name of the view class, followed by .aspx.

I finally figured that out by loading my DLL in Reflector and looking at the resources folder that way. If you have trouble getting the name right, that's a good thing to try.

Once that's done, make sure the class library builds and add a reference in your MVC app to either the project or the output DLL. Either way, the DLL will end up in the "bin" directory of your MVC app.

Now you've got to modify your MVC app to actually look at the plugin(s) for the views at that long path. The models and controller classes are handled by setting the right namespaces and imports.

This is where the VirtualPathProvider comes in. I created a "Lib" directory in the MVC app and added a class called AssemblyResourceProvider. In there, you set that "~/Plugin" path and handle the digging into the assembly for the view. If you're OK with that path, you can just use my class wholesale.

Lastly, you need to register that path provider in the global.asax:


System.Web.Hosting.HostingEnvironment.RegisterVirtualPathProvider(new AssemblyResourceProvider());

The most straightforward way to "get" it is just to take a look at my project. You can download the solution (VStudio2008) and dig through it. The included plugin is available at this URL:


http://server:PORT/BasicExample/Display/Whatever

Where To Go From Here


One of the bits that this doesn't address, but that will be necessary at least in my implementation of this concept is the registering/unregistering of these plugins with the application. The way it is right now, it's entirely held together by convention. You make links to the plugin controller actions and if the plugin is there, it works. Otherwise, it breaks.

The most obvious solutions to this to me are to add to the Assembly information or other meta-data/manifest in it explaining what's in the plugin, which controllers and methods, etc. The hosting app would then watch the "plugin" path (as defined in the VirtualPathProvider) and take over from there as to whether they're automatically available or need additional "activation", etc.

I'd love to see/hear improvements or if there's an entirely better way to do it that means I should scratch this one. Let me know.

Comments

Jamison on 2/13/2009
I've had good luck using StringTemplates for a ViewEngine solution. The clean separation of UI and BL keeps things more modular for deployment and testing (thus addressing your problem). Here's the OpenSource ViewEngine I wrote, and some tutorials/walkthroughs if you're interested.
Marcel on 7/31/2009
Offcourse, I just do this when I cannot use strongly typed Model. and from there on just use etc... hope this helps
Marcel on 7/31/2009
Ok for some reason it rejected the markup. just do "var model = (Person)Model;" in markup and use "model.Name" from there on...
Simone on 1/22/2009
May I know why you deleted my comment? I though you said that feedback is always welcome.
Charles on 12/20/2008
thanks! Hashing out a plug-in architecture now... Consider Structure Map? Winthusiasm had an interesting method for embedding files, I'm going to have to revisit their source (http://www.codeplex.com/HtmlEditor/Release/ProjectReleases.aspx?ReleaseId=15262).
Marcel on 6/27/2009
Joe, you can create an implementation of your Service or Repository that inherits from a base repository somewhere further down and put this in your "Models" folder. Then just export this implementation as typeof(IDataSource) or whatever. In your plugin controller you can use IDataSource as parameter in the constructor and add the attribute "[ImportingConstructor]". This way you can create an instance of you plugin controller and passing a TestDataSource when unit testing and let MEF take care of the real web app exports and imports.
J Wynia on 1/23/2009
Sorry. I didn't delete it, just hadn't gotten to approving comments for a few days because I've been working 7 days a week and WAY too many hours.
J Wynia on 1/23/2009
I like this addition. Also, check out the followup for the question that prompted this post on StackOverflow for additional tweaks.
Serge on 8/23/2009
I guess i'm a little bit confused. What does the "basic example" do? I'm getting the good old default page when i run it.
Simon on 2/10/2009
I posted some of the tweaks that I made back on my original stack overflow question. I've been fiddling about with this a little more. The plugins I've created as code librarys, its seemed the right thing to do. Unfortunately though you end up losing a lot of the intellesense that makes using MVC so nice. Any ideas on how I might get that back?
Joe Future on 6/26/2009
Say my plugins require interfaces to repositories or other core services from my web app. Do you have any thoughts around how to make those available to the plugins?
Marcel on 5/13/2009
I am just playing around with MEF and Plugins for MVC and am still in two minds about storing the Views in the database or assembly. I am leaning towards the db cause I could then set up a simple content editor that can create, update views independently. Tweaking can be done in superuser admin section without having to restart web app + all the various benefits of Linq for free. Dont know.. any suggestions?
tom on 11/19/2009
I've tried reworking sample code to make the views in the plugins strongly typed. I cannot use ViewData["XXX"] in my views. I must be able to use Model.XXX to get the data to render. I've tried implementing this approach in the sample code you provided, but cannot get it to work. Can you provide an example please? Thanks
Gopinath on 7/29/2009
In this method, how do we get Model support? is it possible to send model(data) from action methods and access it in views? I saw you using ViewData, but can we use the MVC style Model object?
Joe Future on 6/30/2009
Very cool - that worked like a charm, thanks! I also implemented the FileSystemWatcher based solution here http://blog.maartenballiauw.be/post/2009/06/17/Revised-ASPNET-MVC-and-the-Managed-Extensibility-Framework-(MEF).aspx.
Simone on 1/21/2009
I didn't play with plugins before, but your post got me interested. What I have been able to notice is that you'll want to swap your plugin, so if you put it in the bin swapping it makes the application restart. In addition to this, the way you load it for reading the embedded resource will lock the assembly. What I've come up with is a folder named, say, Plugin, in the root of the application from where you load the views and that you can monitor using a cache dependency. Actually, nothing useful, just another proof of concept. Specifically, what I had to modify in order to make this work is replacing the following methods body. With this I achieved that the Plugin folder contains the assembly containing the view, and that it is monitored so when it changes it is reloaded. By the way, the same assembly containing the controller still needs to be in the bin, unless the logic to load the controllers is replaced as well. [code="csharp"] public override CacheDependency GetCacheDependency(string virtualPath, IEnumerable virtualPathDependencies, DateTime utcStart) { if (IsAppResourcePath(virtualPath)) { string[] parts = virtualPath.Split('/'); string folderName = parts[1]; string assemblyName = parts[2]; assemblyName = Path.Combine(Path.Combine(HttpRuntime.AppDomainAppPath, folderName), assemblyName); return new CacheDependency(assemblyName); } return base.GetCacheDependency(virtualPath, virtualPathDependencies, utcStart); } public override Stream Open() { string[] parts = path.Split('/'); string folderName = parts[1]; string assemblyName = parts[2]; string resourceName = parts[3]; assemblyName = Path.Combine(Path.Combine(HttpRuntime.AppDomainAppPath, folderName), assemblyName); var assemblyBytes = File.ReadAllBytes(assemblyName); Assembly assembly = Assembly.Load(assemblyBytes); if (assembly != null) return assembly.GetManifestResourceStream(resourceName); return null; } [/code]
Todd on 2/23/2010
Great solution - got it to render a view from an assembly. However, this approach appears to only render the view. Controllers within the plugin do not appear to be supported, correct? For instance, if a view from a plugin did a post back, that views' controller within the plugin will not be called. Instead, it will be routed to a controller within the root MVC application. Is this correct or is there a workaround for this problem?
Damrod on 2/27/2010
Hi, i am trying to embed a master page in an external dll based on your plugin example. But i can't get it running :( "simple" pages can be loaded from the dll but not a masterpage, if the base view resides in the referencing project. "return base.GetCacheDependency(..)" is throwing an exception, because in my master page scenario i have 2 virtualPathDependencies. The system is saying the folder xy (for the master) doesn't exists and the file monitoring can't be started. Any suggestions how I can "fix" this problem? :) Thanks a lot Damrod.
ASP.NET MVC2 Plugins using Areas : : Veeb's Brain Dump on 6/14/2010
[...] excellent information from wynia and [...]
Veebs on 6/14/2010
Thanks for your great work. I did a prototype for Plugins in ASP.NET MVC2 using Areas increase you are interested. See: http://www.veebsbraindump.com/2010/06/asp-net-mvc2-plugins-using-areas/
johnny on 1/1/2010
I have placed plugin assembly in e.g. "Plugins" folder in the root application folder and then load it calling Assembly.LoadFile() method. When I try to refer from view inline code to other classes from plugin assembly (e.g. then exception occured (CS0246 - http://msdn.microsoft.com/en-us/library/w7xf6dxs%28VS.80%29.aspx) during IController.Execute(RequestContext requestContext). But when I copy plugin assembly to application bin folder then it works correctly.
orip on 2/2/2010
Thanks, elegant!
AWright on 3/5/2010
Did you ever fix this to allow it to use strongly typed view user controls? I am getting the error "Could not load type 'System.Web.Mvc.ViewUserControl" when I try to render. Note that I realize the existence of the workaround mentioned - i.e. "just do "var model = (Person)Model;" in markup and use "model.Name" from there on…" but I would like to avoid that if possible.
PerO on 3/30/2010
To use strongly typed views you should virtualize beneath a physical path that has an mvc-view web.config with "<pages pageParserFiltertype = "System.Web.Mvc.ViewTypeParserFilter..." . The easiest way to fix it in the sample "~/Plugin/YOURDLL.dll..." just add the physical folder "Plugin" in the application root and copy a web.config from an working mvc-application views folder.
Marcel on 2/25/2010
Todd, this is possible but you need to create a custom controllerfactory to discover your controllers in other assemblies. I have done this via MEF (Microsoft Extensibility Framework). If you search MEF and MVC in google you would find some help
ASP.NET MVC2 Plugin Architecture Tutorial « fzysqr on 4/26/2010
[...] MVC2 into the mix and all I could really find were some proof-of-concept examples, the best being J Wynia’s example which ended up forming the basis for my solution. I took Wynia’s example (and some of the [...]
Marcel on 3/2/2010
Damrod, this is tricky but I accomplished it by removing the masterpage attribute from the view and using an overload of the ViewActionResult View("masterpage", model); then you need to make sure to give all the discovery paths for masterpages in your custom view engine
J Wynia on 3/10/2010
I ended up not needing plugins of this style for the project that prompted it, so I haven't gone much further with plugin architecture on MVC.
Mike on 4/26/2010
I successfully got your sample code to work in my application by dropping the DLL in manually. Currently I have a web form that 'loads' modules. If I upload the DLL from the web form into the bin directory, I get an error stating that the DLL was expected to have a manifest. Any ideas on this?
blog comments powered by Disqus
Or, browse the archives.
© 2003- 2014 J Wynia. Very Few Rights Reserved. This article is licensed under the terms of the Creative Commons Attribution License. Quoted content or content included from others is not subject to that license and defaults to normal copyright.