views:

4679

answers:

6

I've been spending some time looking at Phil Haack's article on Grouping Controllers very interesting stuff.

At the moment I'm trying to figure out if it would be possible to use the same ideas to create a plug-in/modular architecture for a project I'm working on.

So my question is: Is it possible to have the Areas in Phil's article split across multiple projects?

I can see that the name spaces will work themselves out, but I'm concerned about the views ending up in the right place. Is it something that can be sorted out with build rules?

Assuming that the above is possible with multiple projects in a single solution, does anyone have any ideas about the best way to make it possible with a separate solution and coding to a predefined set of interfaces? Moving from an Area to a plug-in.

I have some experiences with plug-in architecture but not masses so any guidance in this area would be useful.

+1  A: 

I guess it is possible to leave your views in the plug-in projects.

That's my idea: you need a ViewEngine that would call the plugin (probably through an interface) and request the view (IView). The plugin would then instantiate the view not through its url (as an ordinary ViewEngine does - /Views/Shared/View.asp) but through its name of the view )for example via reflection or DI/IoC container).

The returning of the view in the plugin might me even hardcoded (simple example follows):

public IView GetView(string viewName)
{
    switch (viewName)
    {
        case "Namespace.View1":
            return new View1();
        case "Namespace.View2":
            return new View2();
        ...
    }
}

...this was just an idea but I hope it could work or just be a good inspiration.

gius
+16  A: 

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.

J Wynia
That sounds like exactly what I'm after, I'll look forward to the post. Could you add a link when it is done.
Simon Farrow
Yep. That was my plan. I admit that it's a little kludgy, but it does work.
J Wynia
link doesn't seem to work anymore
Peter M
http://www.wynia.org/wordpress/2008/12/aspnet-mvc-plugins/
Mr Grieves
I fixed the link.
J Wynia
+2  A: 

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);
    }
Simon Farrow
+2  A: 

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/

Geo
This is really interesting stuff, I like the idea of overloading functionality for different tenants. Ayende's article was interesting.
Simon Farrow
A: 

[posting as an answer because I can't comment]

Great solution - I used the approach by J Wynia and got it to render a view from a separate assembly. However, this approach appears to only render the view. Controllers within the plugin do not appear to be supported, correct? For instance, if a view from a plugin did a post back, that views' controller within the plugin will not be called. Instead, it will be routed to a controller within the root MVC application. Am I understanding this correctly or is there a workaround for this problem?

tbehunin
You can register the routes locally to the plugin, just need some way of configuring them. You can probably use StructureMap to avoid a lot of messing around with reflection.
Simon Farrow
Disregard my comment/answer. I was doing it wrong, but got it to work the way it was intended. Works awesome! Sorry for the noise!
tbehunin
A: 

This post may be a little late but I've been playing with ASP.NET MVC2 and have come up with a prototype using the "Areas" feature.

Here's the link for anyone who is interested: http://www.veebsbraindump.com/2010/06/asp-net-mvc2-plugins-using-areas/

Veebs