Click here to Skip to main content
15,887,812 members
Articles / Web Development / HTML

ASP.NET MVC2 Plugin Architecture Part 1

Rate me:
Please Sign up or sign in to vote.
4.67/5 (5 votes)
3 Nov 2014CPOL9 min read 19.4K   530   15  
Implementation of a library to enable a plugin architecture in ASP.NET MVC2 applications.

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 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 its 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 - Creates an instance of the view engine and adds 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:

MVC Solution

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 sync.

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:

Embedded Resource

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.

C#
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
....
assembly: MvcPluginLib.MvcPluginViewLocations(
            new string[]
            { "~/Plugins/PluginExample.dll/PluginExample.Views.{1}.{0}.aspx" },//View
                                                                //imbedded as a resource
            true,//Add to "Dynamic Links" for plugin
            action = "Index",//Action that will be called
            controller = "SamplePlugin", //Controller that will be called
            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.

C#
protected void Application_Start()
    {
        AreaRegistration.RegisterAllAreas();

        //NOTE: Use the AssemblyResourceProvider of the MvcPluginLib library.
        HostingEnvironment.RegisterVirtualPathProvider(new AssemblyResourceProvider());

        //NOTE: Set the view locations for the loaded plugin assemblies.
        // Initialize all plugins to have an active status of false.
        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:

C#
routes.MapRoute(
            "Default", // Route name
            "{controller}/{action}/{id}", // URL with parameters
            new { controller = "Home", action = "Index",
                id = UrlParameter.Optional } // Parameter defaults
            );

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 route definition like so...

C#
routes.MapRoute(
            "Default", // Route name
            "{controller}/{action}/{id}", // URL with parameters
            new { controller = "Home", action = "Index",
                id = UrlParameter.Optional }, // Parameter defaults
            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.

C#
<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.

This has all been addressed in ASP.NET MVC2 Plugin Architecture Part 2: Electric Boogaloo.

History

  • Initial publish date: November 3, 2014.
  • Link to Part II of article and a correction: January 20, 2015.

Corrections

The version of the code in this article has an error. In the ExamplePlugin project, the Application_Start method in Global.asax.cs does not make the following call, therefore it will not run as a standalone application.

C#
PluginHelper.InitializePluginsAndLoadViewLocations(true)

This was corrected in Part II of the article.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Team Leader
United States United States
I am a Senior Analyst Engineer/Team Lead for a billion dollar, multinational company specializing in clinical trials. I firmly believe that continual improvements in processes and technologies are the key to high performance.

Comments and Discussions

 
-- There are no messages in this forum --