Introduction
One of the applications I support is a huge, monolithic web app that is mission critical to the company I work for. At times we have had more than one team developing for it, and had to go through the branching/merging process to manage it all. It was not easy to do because of the sheer amount of code, projects and other artifacts that make up the application. One day when I had time to think, I began to wonder...
"Wouldn't it be great if this application was like a CMS where functionality could be added by developing separate modules!"
Thus began my journey into the mysteries of adding a plugin architecture to an ASP.NET MVC application.
Background
The idea of a plugin architecture is nothing new, and there are technologies that help support this such as MEF. The challenge I had in my situation was that I had to stick with the existing, and admittedly dated, technologies. In fact, here is a list of the requirements and constraints for this implementation of a plugin architecture.
-
Current platform is .NET 3.5, ASP.NET MVC2
-
Simple to develop and test
-
Can have separate Visual Studio solutions/projects
-
Can exist as standalone applications
-
Loaded without restarting/redeploying the main application
-
Provide methods for passing information to the hosting application
-
Can support being activated/deactivated via an admin interface
After doing some googling, I ran into an article written by Justin Slattery at FzySqr where he describes step by step how he created a plugin library which was the perfect basis for what I needed to do. You can go here for an excellent overview of the details, as I will highlight the main points in the rest of this article.
Provided with this article are three visual studio 2010 solutions which are based on ASP.NET 2.0, MVC2 and .NET 3.5. Unzip the solutions in the same location in order to use them properly.
The Code
MvcPluginLib
MvcPluginLib does the plugin magic. It is used by the "host" application and the "plugin" to communicate and work with each other. The key classes and methods are:
AssemblyResourceProvider
This class inherits from System.Web.Hosting.VirtualPathProvider
and allows plugins to request resources provided by their assemblies instead of the normal methods used by the base VirtualPathProvider
.
IsAppResourcePath
- a private method used by the class to determine whether or not a resource being requested is provided by the plugin or the hosting application. If Virtual Path contains "~/Plugins/
" that means the resource belongs to a plugin.
FileExists
- an override method that allows for checking the existence of plugin resources.
GetFile
- an override method that allows for the retrieval of plugin resources.
GetCacheDependency
- an override method that exempts plugin resources from the caching mechanisms of the base VirtualPathProvider
class.
AssemblyResourceVirtualFile
This class inherits from System.Web.Hosting.VirtualFile
and enables plugins to extract resources (files) from their respective assemblies.
AssemblyResourceVirtualFile
- instance method that stores the requested resource's path into a member variable for later use.
Open
- override method used to return a stream to the requested resource that is embedded within the plugin's assembly.
IPlugin
This interface defines the action methods required for plugins.
Index
- a default action named Index will is required of all plugins.
DeactivatedPage
- a page to show that the plugin is deactivated is required of all plugins.
PluginActionFilter
This class will intercept MVC calls for executing actions, that way the plugin can check its status before executing.
OnActionExecuting
- this override will check the status of the plugin before executing its Index action. If the plugin is activated, the Index action will execute. If the plugin is not activated, it will redirect to the plugin's DeactivatedPage action.
[NOTE: In order for this to work, the plugin will have to add the [PluginActionFilter]
attribute to its Index action method.]
MvcPluginViewLocations
This class (defined in the PluginAttribute.cs file) is used to store custom attribute information from plugins. More will be discussed on this in the PluginExample section below.
PluginHelper
This class is used to pull attribute information from plugin assemblies.
InitializePluginsAndLoadViewLocations
- this method loops through the loaded plugin assemblies, extracts the view information from them and provides it to the plugin view engine class (PluginViewEngine
). It also Registers the plugin with the PluginManager
and sets its default status to the value that is passed in as a parameter.
GetPluginActions
- retrieves the action/link information from the plugin assemblies.
GetPluginAssemblies
- returns a list of loaded assemblies that are plugins. They are determined by checking to see if they are typeof(MvcPluginViewLocations)
.
PluginAction
This is a class used to store plugin action information. More will be discussed on this in the PluginExample section below.
PluginManager
This static class contains methods for registering and managing the status of plugins.
PluginStatus
- a class that holds plugin status information.
PluginList
- a property that maintains a list of registered plugins and their statuses.
RegisterPlugin
- registers a plugin by adding it to the PluginList
and setting it's activated member variable.
GetPluginStatus
- returns the activated status of a specified plugin.
SetPluginStatus
- sets the activated status of a specified plugin.
PluginViewEngine
This class inherits from System.Web.Mvc.WebFormViewEngine
and intercepts calls made to the base WebFormViewEngine
class to enable plugin functionality.
PluginViewEngine
- create an instance of the view engine and add the passed in additional view locations.
IsAppResourcePath
- same functionality as the one defined above in AssemblyResourceProvider.
FileExists
- an override method that allows for checking the existence of plugin resources. This overrides the method from WebFormViewEngine
.
PluginExample
The PluginExample project shows how to develop a plugin. Here are the key points to look at:
Standalone MVC app
The plugin should be developed as a standalone MVC2 application. It can have controllers, models and views. One thing to note is that any additional items such as images, javascript files, referenced libraries, etc. must already exist on the hosting application. Your best bet is to avoid them altogether, but if you cannot you will have to make sure both the plugin and hosting application are in synch.
What about the views?
Ah, yes! The views are separate files. Do we copy them to the hosting application?
No, we don't. Not in the way you think, anyway.
Above in the section detailing the classes, there are methods defined for handling resources requested by plugins. Views were among them. This means that views have to be setup as "Embedded Resource" like so:
Plugin Assembly Attributes
The way a plugin assembly is identified is by using custom attributes. Below are the custom attributes defined in the AssemblyInfo.cs file.
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
....
assembly: MvcPluginLib.MvcPluginViewLocations(
new string[] { "~/Plugins/PluginExample.dll/PluginExample.Views.{1}.{0}.aspx" }, true, action = "Index", controller = "SamplePlugin", name = "SamplePlugin")]
MvcPluginViewLocations
- As mentioned above, this class holds the custom attributes. This information lets the host application know that this is a plugin how to link to it. The properties are defined as follows:
viewLocations
- this is an array of the view locations required by the plugin.
addLink
- specify whether or not this link should be added to the host application.
controller
- the controller name for the plugin.
action
- the controller action for the plugin.
name
- the name to use for constructing the link for this plugin.
PluginHostExample
The PluginHostExample project demonstrates how a host application will access and manage plugins.
Yes, it uses frames don't judge!
Loading Plugins
To deploy your plugin, simply copy the .dll file from the plugin's project (in this case PluginExample.dll) to the bin folder of the host application. Plugins are loaded in the Application_Start
event by registering the AssemblyResourceProvider
and invoking the PluginHelper's InitializePluginsAndLoadViewLocations
method.
protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();
HostingEnvironment.RegisterVirtualPathProvider(new AssemblyResourceProvider());
PluginHelper.InitializePluginsAndLoadViewLocations(false);
RegisterRoutes(RouteTable.Routes);
}
Controller Conflicts
Since a plugin is designed to run as an independent MVC application, it will not play nice with the hosting application unless you make one minor change. The default routes for MVC apps typically are setup as follows:
routes.MapRoute(
"Default", "{controller}/{action}/{id}", new { controller = "Home", action = "Index", id = UrlParameter.Optional } );
Having your plugin assembly being available along with the hosting application's assemblies means that MVC cannot determine which one to use to handle the route, so you will get:
Multiple types were found that match the controller named ‘Home’.
To resolve this issue, specify the hosting application's namespace in the rout definition like so...
routes.MapRoute(
"Default", "{controller}/{action}/{id}", new { controller = "Home", action = "Index", id = UrlParameter.Optional }, new string[] { "PluginHostExample.Controllers" }
);
Web.config'ism
Another issue I ran into with the hosting application is that the typical MVC project puts a web.config in the Views folder that has important information regarding the processing of .aspx views that are strongly typed. Since plugin views come from resources, they only have access to the application's main web.config, so unless you add some additional information to it you will receive the following error:
Could not load type 'System.Web.Mvc.ViewPage<PluginExample.Models.SamplePluginModel>'.
To resolve this issue, add the following attributes to the <pages> tag in the hosting application's main web.config file.
<pages
validateRequest="false"
pageParserFilterType="System.Web.Mvc.ViewTypeParserFilter, System.Web.Mvc, Version=2.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"
pageBaseType="System.Web.Mvc.ViewPage, System.Web.Mvc, Version=2.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"
userControlBaseType="System.Web.Mvc.ViewUserControl, System.Web.Mvc, Version=2.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35">
References
Musings
This is a good start, but there are a few things bothering me such as...
- The IPlugin interface makes you create both Index and DeactivatedPage action methods, but unless you add the
[PluginActionFilter]
attribute, the plugin can bypass the ability to be deactivated. Hmmm.... we'll have to do something about that.
- A plugin requires a lot of things to be done to it in order to be properly recognized as a plugin. What if you forget something? Maybe there should be some generic test cases to ensure that the plugin project meets the minimum standards. Yeah, let's look into that.
- The only resources plugins have that are embedded within them are the views. Is there something else we can do with a plugin to handle other resources such as javascript files and/or images? Not sure, but maybe we should investigate further.
I am considering creating a second part to this article, titled ASP.NET MVC2 Plugin Architecture Part 2: Electric Boogaloo. Stay tuned for more updates!
History
Initial publish date: November 3, 2014.