Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

ASP.NET MVC2 Plugin Architecture Part 2: Electric Boogaloo

0.00/5 (No votes)
20 Jan 2015 1  
Implementation of a library to enable a plugin architecture in ASP.NET MVC2 applications, Part II.

Introduction

In part one of this article, I went over the implementation of a plugin architecture for ASP.NET MVC applications and provided code for a solution. Although the initial implementation met the requirements at the time, I noted at the end of the article that there are several outstanding issues that should be addressed.

  • If the plugin does not apply the [PluginActionFilter] attribute, it can bypass the ability to be deactivated.
  • There should be some generic test cases to ensure that the plugin project meets the minimum requirements.
  • The handling of resources, other than views which are embedded, such as images and javascript files.

In the second, and final chapter of this article, I will cover each of these concerns and provide updated solutions to resolve them.

You Shall Not Pass!

The management of the plugins is handled by the PluginManager class. It maintains state information about each plugin, such as the pluginName and activated properties (within a list of PluginStatus objects), and provides methods to query and set said information. In the sample host application, an administration form provides functionality for activating and deactivating plugins, and the ASP.NET MVC override OnActionExecuting uses an attribute called PluginActionFilter on the Index actions to either allow the plugin to function normally or redirect to a DeactivatedPage action. This, however, works on the honor system in that it's up to the plugin developer to add [PluginActionFilter] to the Index action. Now, in the updated version of the PluginManager class, we have the tools to enforce this rule by requring that plugins be considered "valid" only when their Index method has the [PluginActionFilter] attribute.

PluginHelper

Additional methods and updates were made to PluginHelper to query additional information about the plugin assembly.

  • HasPluginActionFilter - this method checks to see if a plugin assembly is implementing the [PluginActionFilter] attribute on the Index action method.
C#
Private static bool HasPluginActionFilter(Assembly plugin)
{
    var types = plugin.GetTypes().Where(t => t.BaseType == typeof(System.Web.Mvc.Controller));

    foreach (var type in types)
    {
        if (type != null)
        {
            var method = type.GetMethod("Index");
            if (method != null)
            {
                var actionFilterAttributes = method.GetCustomAttributes(typeof(PluginActionFilter), false);
                if (actionFilterAttributes.Count() > 0)
                    return true;
            }
        }
    }
    return false;
}
  • InitializePluginsAndLoadViewLocations - this method was updated to call HasPluginActionFilter, and will only register the plugin if it returns "true". If the plugin does not have the PluginActionFilter attribute specified for its Index action method, it will be marked invalid.
C#
public static void InitializePluginsAndLoadViewLocations(bool defaultStatus)
{
    Assembly[] pluginAssemblies = GetPluginAssemblies();

    List<string> viewLocations = new List<string>();

    foreach (Assembly plugin in pluginAssemblies)
    {
        var pluginAttribute = plugin.GetCustomAttributes(typeof(MvcPluginViewLocations), false).FirstOrDefault() as MvcPluginViewLocations;

        if (pluginAttribute != null)
        {
            //Check to see if this plugin has the PluginActionFilter attribute.
            if (HasPluginActionFilter(plugin))
            {
                viewLocations.AddRange(pluginAttribute.viewLocations);

                //Register plugin with PluginManager class
                PluginManager.RegisterPlugin(plugin.ManifestModule.Name,DefaultStatus);

                //The PluginViewEngine is used to locate views in the assemlbies 
                ViewEngines.Engines.Clear();
                ViewEngines.Engines.Add(new PluginViewEngine(viewLocations.ToArray()));
            }
            else
            {
                //Mark plugin invalid
                PluginManager.InvalidatePlugin(plugin.ManifestModule.Name);
            }
        }
    }
}
PluginManager

Here are the changes that were made to the PluginManager class to make use of the updates to PluginHelper.

  • PluginStatus - added the property valid to track the validity of the plugin.
C#
public class PluginStatus
{
    public string pluginName;
    public bool activated;
    public bool valid;
}
  • IsPluginValid - returns "true" if the plugin is valid, otherwise "false".
C#
public static bool IsPluginValid(string name)
{
    var plugin = PluginList.First(p => p.pluginName == name);
    return (plugin == null ? false : plugin.valid);
}
  • SetPluginStatus - this method was updated to check the validity of the plugin before activating it.
C#
public static void SetPluginStatus(string name, bool status)
{
    var plugin = PluginList.First(p => p.pluginName == name);
    if (!plugin.valid && status == true)
        throw new Exception("An invalid plugin cannot be activated.");
    if (plugin != null)
        plugin.activated = status;
}

Testing

In order to make sure a plugin meets the minimum requirements needed to actually be a plugin, we will add some automated unit testing into the mix.

Validity

The requirements for a valid plugin are simple. It has to have an Index action and that Index action has to have a [PluginActionFilter] attribute. Below is the test method.

C#
[TestMethod]
public void IndexHasPluginActionFilter()
{
    var indexMethod = pluginControllerInstance.GetType().GetMethod("Index");
    Assert.IsNotNull(indexMethod, "Index action method does not exist in plugin.");

    var actionFilterAttributes = indexMethod.GetCustomAttributes(typeof(PluginActionFilter), false);
    Assert.IsTrue(actionFilterAttributes.Count() > 0, "Index action method does not have [PluginActionFilter] attribute.");
}

To try this out, simply remove the [PluginActionFilter] attribute from the Index method, recompile and execute the test case. Here is what you will get.

Image 1

This test method also checks to make sure you have an Index action method defined, and will fail if not.

Embedded Views

Now this one is interesting. Any views provided by the plugin have to be embedded, but there is nothing stopping a plugin from using a view that is provided by the hosting application. The other question is, "How do we determine what view is being requested within an action?"

The short answer is, "I don't think we can do that."

The one thing we can do is assume that there should be at least one embedded view, and if there is not issue some sort of a warning. In this case, the warning will be an Inconclusive assertion.

C#
[TestMethod]
public void HasEmbeddedViews()
 {
    int numViews = pluginControllerInstance.GetType().Assembly.GetCustomAttributes(typeof(MvcPluginViewLocations), false).Count();
    if (numViews == 0)
        Assert.Inconclusive("Warning: There are no embedded views.");
}

Here is what you will see if there are no embedded views in your plugin.

Image 2

Additional Resources

In order to access the views, which in a normal MVC application are separate files, the plugin must internalize them as embedded resources. Well, what about other files, such as images and javascript files? For the scope of this article we will not worry about files that are expected to be part of the hosting application, and will focus on files that are assumed to be specific to the plugin. Since we already have some of the plumbing to reference and use embedded resource files, let's take a look at leveraging it to access other files.

Referencing the Resource

We already know how to embed these resources, so what we need to do now is find out how to reference and use them. The plugin library already expects embedded views to have URLs that start with ~/Plugins. The library also references the plugin in the URL. So, let's take a look at doing the same. After some poking around, here is what I came up with.

The example plugin now has an image. Since it is embedded, we need to compose the URL in such a way that the plugin library can know it's embedded, know which plugin assembly it's embedded in, and be able to retrieve it when the request is made by the browser. Here is the URL.

HTML
<img src="/Plugins/PluginExample.dll/Resources.Santa_hat_smiley_T.png" />
  • /Plugins identify that this resource is embedded.
  • PluginExample.dll identifies the assembly.
  • Resources.Santa_hat_smiley_T.png is the embedded resource.

Image 3 Note that the embedded resource in the URL is different than when viewed in the dll when decompiled, and that the URL does not have the ~. This is because the resource is prepended with the assembly name, and IsAppResourcePath method of the AssemblyResourceProvider class prepends the tilde.

Handling the Resource Request

In order to handle requests to embedded resources we need to intercept them. Since we are already doing this for the views, all we need to do is make updates to the existing code to handle other files. We start by making sure we disable routing for the files we are interested in (.css, .js, .gif, .jpg, .png), by adding the following to the RegisterRoutes method in Global.ascx.cs.

C#
routes.IgnoreRoute("{*staticfile}", new { staticfile = @".*\.(css|js|gif|jpg|png)(/.*)?" });

Now, we have to update the AssemblyResourceProvider class to check for embedded resources. Note that we have to take into account the namespace since it is added to the resource in the assmbly. This is the code that gets the name from the assembly:

C#
string assemblyNamespace = assembly.GetName().Name;

This information is used in the updated FileExists method of the provider, so that it will also check the assembly for the existence of the requested file.

C#
bool found = Array.Exists(resourceList, 
         delegate(string r) { return r.Equals(resourceName); })
          || Array.Exists(resourceList, 
         delegate(string r) { return r.Equals(assemblyNamespace + "." + resourceName); });

Once the AssemblyResourceProvider methods indicate that the request is for an embedded resource, we need to make sure we update the AssemblyResourceVirtualFile class to retrieve the file from the assembly. So we update the Open method to check to see if the resource exists with the namespace prepended, and if it is use that when calling GetManifestResourceStream. Here is the code snippet:

C#
...
string assemblyNamespace = assembly.GetName().Name;

if (assembly != null)
{
    //Check to see if this exists with the namespace. If so
    //then prepend the namespace.
    if (assembly.GetManifestResourceInfo(resourceName) == null)
    {
        resourceName = assemblyNamespace + "." + resourceName;
    } 
    return assembly.GetManifestResourceStream(resourceName);
}
...

Conclusion

Now we have a more complete solution for the implementation of a plugin architecture for ASP.NET MVC2 applications.

References

Ignoring routes for specific file types.

History

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here