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
{
string Title { get; }
string Name { get; }
Version Version { get; }
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
{
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);
}
private static readonly DirectoryInfo PluginFolder;
private static readonly DirectoryInfo TempPluginFolder;
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);
foreach (var f in TempPluginFolder.GetFiles("*.dll", SearchOption.AllDirectories))
{
f.Delete();
}
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);
}
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)
{
BuildManager.AddReferencedAssembly(assembly);
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
- Create
new Class Library project.
- Create
folders similar to an MVC application: Models, Views, Controllers.
- 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:
- Download the following Visual Studio Extension and
install it from
here.
- 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
.
- 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.
- 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.