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

.NET 4.0 ASP.NET MVC 3 plug-in architecture with embedded views

0.00/5 (No votes)
8 Jul 2013 3  
MVC 3 plugin architecture with embedded razor views: steb-by-step description and demo application.

Introduction 

This article demonstrates how to quickly build a plug-in architecture with ASP.NET MVC 3 and how to build new plugins for your application. It also shows how to create your views as embedded resources into the plugins and how .NET 4.0 features can help with discovering the new plug-ins from the host application.

Second Part shows how to add server side-logic inside plugins. 

Background  

I had to design a new ASP.NET MVC enterprise application that was marketed as software-as-a-service, basically being distributed to multiple customers by features. The customer can select which services he wants activate and pay only for those. Extra security of the code was also needed, all assemblies had to be signed and views have to be embedded for protection, so the best approach was to create a plug-in able architecture that could help accomplish all of this requirements and deliver extra features with ease. This also adds extra flexibility if custom requirements are needed from individual customers and allow them to create their own modules it they want.

Steps to create a plug-in system with embedded views 

Setup the plugin infrastructure

The host application has to identify all the plug-ins through a public library. A common interface is necessary for all plugins, that will be implemented by a class in the root of any plug-in. This interface must be located in a public assembly that will be referenced by all plug-ins and will contain common interfaces that are used as a bridge between a plug-in and the host application.

Let's call that interface the IModule interface:

public interface IModule
{
    /// <summary>
    /// Title of the plugin, can be used as a property to display on the user interface
    /// </summary>
    string Title { get; }

    /// <summary>
    /// Name of the plugin, should be an unique name
    /// </summary>
    string Name { get; }

    /// <summary>
    /// Version of the loaded plugin
    /// </summary>
    Version Version { get; }

    /// <summary>
    /// Entry controller name
    /// </summary>
    string EntryControllerName { get; }
}

The class that will implement this interface will carry all the information about name, version and default access to the plugin.

Plugin discovery

ASP.NET 4 introduces a few new extensibility APIs that are very useful. One of them is a new assembly attribute called PreApplicationStartMethodAttribute.

This new attribute allows you to have code run way early in the ASP.NET pipeline as the application starts up, even before Application_Start. This happens to be also before the code in your App_Code folder has been compiled. To use this attribute, create a class library and add this attribute as an assembly level attribute. Example:

[assembly: PreApplicationStartMethod(typeof(PluginTest.PluginManager.PreApplicationInit),"InitializePlugins")]

As seen above, a type and a string was specified. The string represents a method that needs to be a public static void method with no arguments. Now, any ASP.NET website that references this assembly will call the InitializePlugins method when the application is about to start, giving this method a chance to perform some early initialization.

public class PreApplicationInit
{
    /// <summary>
    /// Initialize method 
    /// </summary>
    public static void InitializePlugins()
    { ... } 
}

Now the real usage of this attribute is that you can add on run-time build providers or new assembly references, which in the previous versions of Visual Studio could be done only via web.confing. We will use this to add a reference to all the plug-ins before the application starts. Example:

System.Web.Compilation.BuildManager.AddReferencedAssembly(assembly);

The plug-ins should be copied in a directory inside the web application, but not referenced directly, instead a copy of them should be made and reference the copied plug-ins. All of this steps have to be made in the PreApplicationInit class static constructor.

public class PreApplicationInit
{
    static PreApplicationInit()
    {
        string pluginsPath = HostingEnvironment.MapPath("~/plugins");
        string pluginsTempPath = HostingEnvironment.MapPath("~/plugins/temp");

        if (pluginsPath == null || pluginsTempPath == null)
            throw new DirectoryNotFoundException("plugins");

        PluginFolder = new DirectoryInfo(pluginsPath);
        TempPluginFolder = new DirectoryInfo(pluginsTempPath);
    }
    /// <summary>
    /// The source plugin folder from which to copy from
    /// </summary>
    private static readonly DirectoryInfo PluginFolder;
    /// <summary>
    /// The folder to copy the plugin DLLs to use for running the application
    /// </summary>
    private static readonly DirectoryInfo TempPluginFolder;

    /// <summary>
    /// Initialize method that registers all plugins
    /// </summary>
    public static void InitializePlugins()
    { ... }
}

When the InitializePlugins method is called which will refresh the temp directory and reference all the plugin assemblies. When referencing, a check is required that validates the plugin by verifying if they contain a class that implements the IModule interface.

public static void InitializePlugins()
{
    Directory.CreateDirectory(TempPluginFolder.FullName);

    //clear out plugins
    foreach (var f in TempPluginFolder.GetFiles("*.dll", SearchOption.AllDirectories))
    {
        f.Delete();
    }

    //copy files
    foreach (var plug in PluginFolder.GetFiles("*.dll", SearchOption.AllDirectories))
    {
        var di = Directory.CreateDirectory(TempPluginFolder.FullName);
        File.Copy(plug.FullName, Path.Combine(di.FullName, plug.Name), true);
    }

    //This will put the plugin assemblies in the 'Load' context
    var assemblies = TempPluginFolder.GetFiles("*.dll", SearchOption.AllDirectories)
            .Select(x => AssemblyName.GetAssemblyName(x.FullName))
            .Select(x => Assembly.Load(x.FullName));

    foreach (var assembly in assemblies)
    {
        Type type = assembly.GetTypes()
                            .Where(t => t.GetInterface(typeof(IModule).Name) != null).FirstOrDefault();
        if (type != null)
        {
            //Add the plugin as a reference to the application
            BuildManager.AddReferencedAssembly(assembly);

            //Add the modules to the PluginManager to manage them later
            var module = (IModule)Activator.CreateInstance(type);
            PluginManager.Current.Modules.Add(module, assembly);
        }
    }
}

For this to work a probing folder must be configured in web.config to tell the AppDomain to also look for Assemblies/Types in the specified folders:

<runtime>
   <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
      <probing privatePath="plugins/temp" />
   </assemblyBinding>
</runtime>  

The singleton, PluginManager, is used to keep a track of all the plugins, for later use in the application.

In the final step, after the plugins are referenced it is necessary to register the embedded views from the plugins. This will be done using BoC.Web.Mvc.PrecompiledViews.ApplicationPartRegistry. I preferred to do it in a Bootstrapper class where I can, for example, also setup my DI container if I want. More information in the next chapter "Create embedded views". Bootstrapper overview:

public static class PluginBootstrapper
{
    public static void Initialize()
    {
        foreach (var asmbl in PluginManager.Current.Modules.Values)
        {
            ApplicationPartRegistry.Register(asmbl);
        }
    }
}

Steps to create a new plug-in project 

  1. Create new Class Library project.
  2. Create folders similar to an MVC application: Models, Views, Controllers. 
  3. Create  a module initializer class that implements the IModule interface: 
public class CalendarModule : IModule
{
    public string Title
    {
        get { return "Calendar"; }
    }
    public string Name
    {
        get { return Assembly.GetAssembly(GetType()).GetName().Name; }
    }
    public Version Version
    {
        get { return new Version(1, 0, 0, 0); }
    }
    public string EntryControllerName
    {
        get { return "Calendar"; }
    }
}

Create embedded views

The full tutorial, that I also followed, on how to add embedded Views to an MVC project can be found here. This chapter will only describe the absolute steps to this case. There are some simple steps to follow:

  1. Download the following Visual Studio Extension and install it from here.   
  2. The views have to be created like on a normal MVC project, in the same structure, in the Views directory. Then the views must be set as Embedded Resource and set the Custom Tool Namespace to MvcRazorClassGenerator.  
  3. After you add the Custom Tool some references will be added to the project, but some extra references are needed, realated to an MVC project. Make sure that all the references are added to the project, like: System.Web.Helpers, System.Web.WebPages, System.Web.MVC. 
  4. Last step is to make sure to register the embedded views: 
BoC.Web.Mvc.PrecompiledViews.ApplicationPartRegistry.Register(asmbl);

Using the code    

All needed assemblies are in the Binaries folder, on the same level with the solution file. You can find the following DLLs: BoC.Web.Mvc.PrecompiledViews.dll, Commons.Web.Mvc.PrecompiledViews.dll, System.Web.Helpers.dll, System.Web.WebPages.dll, System.Web.MVC.dll, System.Web.Razor.dll, System.Web.WebPages.Razor.dll.

All the plugins dlls are compiled in the directory PluginBin on the same root with the solution file.

The plugins are searched in the plugins directory inside the Web Application, the relative path from the solution file is PluginTest.Web/plugins

First make sure that the project is compiled and then copy the contents from PluginBin to PluginTest.Web/plugins

Make sure that the web-server is stopped or restart the web-server and run the application.

Feel free to play with the plugins by deleting one or more and then restart the application and see the results

Conclusion

Plug-in web applications can be easy achieved as described in this article, but web applications rarely needs this kind of added complexity. This is also only a proof of concept example of how this could be achieved and it does not represent a full blown implementation. The purpose of this article was to share a basic implementation that others could reuse and extend if required.

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