Plug-in architecture for ASP.NET MVC
Solution 1:
I did a proof of concept a few weeks ago where I put a complete stack of components: a model class, a controller class and their associated views into a DLL, added/tweaked one of the examples of the VirtualPathProvider classes that retrieve the views so they'd address those in the DLL appropriately.
In the end, I just dropped the DLL into an appropriately configured MVC app and it worked just like if it had been part of the MVC app from the start. I pushed it a bit further and it worked with 5 of these little mini-MVC plugins just fine. Obviously, you have to watch your references and config dependencies when shuffling it all around, but it did work.
The exercise was aimed at plugin functionality for an MVC-based platform I'm building for a client. There are a core set of controllers and views that are augmented by more optional ones in each instance of the site. We're going to be making those optional bits into these modular DLL plugins. So far so good.
I wrote up an overview of my prototype and a sample solution for ASP.NET MVC plugins on my site.
EDIT: 4 years on, I've been doing quite a few ASP.NET MVC apps with plugins and no longer use the method I describe above. At this point, I run all of my plugins through MEF and don't put controllers into plugins at all. Rather, I make generic controllers that use the routing information to select MEF plugins and hand the work off to the plugin, etc. Just thought I'd add since this answer gets hit a fair bit.
Solution 2:
I’m actually working on an extensibility framework to use on top of ASP.NET MVC. My extensibility framework is based on the famous Ioc container: Structuremap .
The use case I’m trying to fulfill is simple: create an application that should have some basic functionality that can be extended for every customer (=multi-tenancy). There should only be one instance of the application hosted but this instance can be adapted for every customer without making any changes to the core website.
I was inspired by the article on multi tenacy written by Ayende Rahien: http://ayende.com/Blog/archive/2008/08/16/Multi-Tenancy--Approaches-and-Applicability.aspx Another source of inspiration was the book of Eric Evans on Domain Driven Design. My Extensibility framework is based on the repository pattern and the concept of root aggregates. To be able to use the framework the hosting application should be build around repositories and domain objects. The controllers, repositories or domain objects are bind at runtime by the ExtensionFactory.
A plug-in is simply an asselmbly that contains Controllers or Repositories or Domain Objects that respects a specific naming convention. The naming convention is simple, every class should be prefixed by the customerID e.g.: AdventureworksHomeController.
To extend an application you copy a plug-in assembly in the extension folder of the application. When a user request a page under the customer root folder e.g: http://multitenant-site.com/[customerID]/[controller]/[action] the framework check if there is a plug-in for that particular customer and instantiate the custom plug-in classes otherwise it loads the default once. The custom classes can be Controllers – Repositories or Domain Objects. This approach enables to extend an application at all levels, from the database to the UI, through the domain model, repositories.
When you want to extend some existing features you create a plug-in an assembly that contains subclasses of the core application. When you’ve to create totally new functionalities you add new controllers inside the plug-in. These controllers will be loaded by the MVC framework when the corresponding url is requested. If you want to extend the UI you can create a new view inside the extension folder and reference the view by a new or subclassed controller .To modify existing behavior you can create new repositories or domain objects or sub classing exiting ones. The framework responsibility is to determine which controller/ repository / domain object should be loaded for a specific customer.
I advise to have a look at structuremap (http://structuremap.sourceforge.net/Default.htm) and especially at the Registry DSL features http://structuremap.sourceforge.net/RegistryDSL.htm .
This is the code I use at the startup of the application to register all plug-in controllers/repositories or domain objects:
protected void ScanControllersAndRepositoriesFromPath(string path)
{
this.Scan(o =>
{
o.AssembliesFromPath(path);
o.AddAllTypesOf<SaasController>().NameBy(type => type.Name.Replace("Controller", ""));
o.AddAllTypesOf<IRepository>().NameBy(type => type.Name.Replace("Repository", ""));
o.AddAllTypesOf<IDomainFactory>().NameBy(type => type.Name.Replace("DomainFactory", ""));
});
}
I also use an ExtensionFactory inheriting from the System.Web.MVC. DefaultControllerFactory. This factory is responsible to load the extension objects (controllers/registries or domain objects). You can plugin your own factories by registering them at startup in the Global.asax file:
protected void Application_Start()
{
ControllerBuilder.Current.SetControllerFactory(
new ExtensionControllerFactory()
);
}
This framework as a fully operational sample site can be found on: http://code.google.com/p/multimvc/
Solution 3:
So I had a little play around with the example from J Wynia above. Many thanks for that btw.
I changed things so that the extension of the VirtualPathProvider used a static constructor to create a list of all of the available resources ending with .aspx in the various dll's in the system. It's laborious but only we're only doing it once.
It's probably a total abuse of the way that VirtualFiles are supposed to be used as well ;-)
you end up with a:
private static IDictionary resourceVirtualFile;
with the string being virtual paths.
the code below makes some assumptions about the namespace of the .aspx files but it will work in simple cases. This nice thing being that you don't have to create complicated view paths they are created from the resource name.
class ResourceVirtualFile : VirtualFile
{
string path;
string assemblyName;
string resourceName;
public ResourceVirtualFile(
string virtualPath,
string AssemblyName,
string ResourceName)
: base(virtualPath)
{
path = VirtualPathUtility.ToAppRelative(virtualPath);
assemblyName = AssemblyName;
resourceName = ResourceName;
}
public override Stream Open()
{
assemblyName = Path.Combine(HttpRuntime.BinDirectory, assemblyName + ".dll");
Assembly assembly = Assembly.ReflectionOnlyLoadFrom(assemblyName);
if (assembly != null)
{
Stream resourceStream = assembly.GetManifestResourceStream(resourceName);
if (resourceStream == null)
throw new ArgumentException("Cannot find resource: " + resourceName);
return resourceStream;
}
throw new ArgumentException("Cannot find assembly: " + assemblyName);
}
//todo: Neaten this up
private static string CreateVirtualPath(string AssemblyName, string ResourceName)
{
string path = ResourceName.Substring(AssemblyName.Length);
path = path.Replace(".aspx", "").Replace(".", "/");
return string.Format("~{0}.aspx", path);
}
public static IDictionary<string, VirtualFile> FindAllResources()
{
Dictionary<string, VirtualFile> files = new Dictionary<string, VirtualFile>();
//list all of the bin files
string[] assemblyFilePaths = Directory.GetFiles(HttpRuntime.BinDirectory, "*.dll");
foreach (string assemblyFilePath in assemblyFilePaths)
{
string assemblyName = Path.GetFileNameWithoutExtension(assemblyFilePath);
Assembly assembly = Assembly.ReflectionOnlyLoadFrom(assemblyFilePath);
//go through each one and get all of the resources that end in aspx
string[] resourceNames = assembly.GetManifestResourceNames();
foreach (string resourceName in resourceNames)
{
if (resourceName.EndsWith(".aspx"))
{
string virtualPath = CreateVirtualPath(assemblyName, resourceName);
files.Add(virtualPath, new ResourceVirtualFile(virtualPath, assemblyName, resourceName));
}
}
}
return files;
}
}
You can then do something like this in the extended VirtualPathProvider:
private bool IsExtended(string virtualPath)
{
String checkPath = VirtualPathUtility.ToAppRelative(virtualPath);
return resourceVirtualFile.ContainsKey(checkPath);
}
public override bool FileExists(string virtualPath)
{
return (IsExtended(virtualPath) || base.FileExists(virtualPath));
}
public override VirtualFile GetFile(string virtualPath)
{
string withTilda = string.Format("~{0}", virtualPath);
if (resourceVirtualFile.ContainsKey(withTilda))
return resourceVirtualFile[withTilda];
return base.GetFile(virtualPath);
}