Multi-tenancy in ASP.NET MVC - Controller Actions (Part I)

Published on 2010-2-14

Previous entries in the series

 In the last entry, we covered how and why we might want to replace existing views and partial views from the core application with our own from modules. We also covered that with this ability it was possible to add entirely new views and partial views.

However, views need actions and actions come from controllers. If we add a new view to the application and the core application does not support that path with an action, that view cannot be loaded.

It stands to reason therefore that our modules need the ability to add new controller actions (and indeed replace existing actions) at run-time on a per-request basis - again assuming we're going for full on multi-tenancy.

Actions come from controllers, and by default controllers come from the main web assembly. Now obviously our modules should be as self contained as possible and therefore probably each have their own assemblies so they can be developed separately and added to the project ad-hoc.

Once again, the ASP.NET MVC team have given us an extensibility point with which to override this default behaviour with the ability to implement our own controller factories.

Resolving actions
As with the last topic, I will assume the presence of a configuration provider that can tell us which modules are loaded.
For purposes of simplicity, the Module class now contains a reference to an Assembly that we'll assume was loaded in when the configuration was last scanned.

    public class Module
    {
        public string Id
        {
            get;
            set;
        }

        public Assembly Assembly
        {
            get;
            set;
        }
    }

The job of the ControllerFactory is another well documented concept; when a controller is required, the factory is invoked with the name of the controller being requested and the current request data. It is expected to return an instance of the controller (which is used for that single request), and then just like the ViewEngine is given that controller to dispose of at the end of the request.

The simplest solution is clearly going to be that we look at the context we have access to and then work out which controller to return based on that context.
The context in this case being the name of the controller, the action being requested and the collection of modules which are currently active for this request.

Each module can therefore hold their own controllers with their own actions, and the controller factory can select which controller to return when a specific action is being invoked.

I implement my controller factory from the base interface, which is System.Web.Mvc.IControllerFactory: 

    public class ModuleControllerFactory : IControllerFactory
    {
        public IController CreateController(System.Web.Routing.RequestContext requestContext, string controllerName)
        {
            throw new NotImplementedException();
        }

        public void ReleaseController(IController controller)
        {
            throw new NotImplementedException();
        }
    }

ReleaseController can just check for IDisposable and dispose if necessary, so we'll take that as read and focus on what we need to do in order to create the controller.

The Goal

These are the modules exposed by the application, the controllers they provide and the actions those controllers have on them.

  • CoreModule
    • HomeController
    • ActionResult Index();
  • ModuleOne
    • HomeController
      • ActionResult Extra();
  • ModuleTwo
    • HomeController
      • ActionResult Index();
      • ActionResult Extra();
    • OtherController
      • ActionResult Index();

 WIth the set-up, the following behaviour is desired:

Core Module loaded:

/Home/Index requested => CoreModule Index Invoked
/Home/Extra requested =>  Action Not found

Core + ModuleOne Loaded

/Home/Index requested => CoreModule Index Invoked
/Home/Extra requested =>  ModuleOne Extra Invoked

Core + ModuleOne + ModuleTwo Loaded (in that order)

/Home/Index requested => ModuleTwo Index Invoked
/Home/Extra requested => ModuleTwo Extra Invoked
/Other/Index requested => ModuleTwo Index Invoked

A Solution

This is actually quite tricky, as the solution is going to involve not only scanning for the controllers, but scanning for methods on those controllers that match the actions being requested.

There are a lot of rules involved already in MVC selecting the right method to call from a controller, and we don't want to go down the route of duplicating this, so this is where we set a convention and say that if *any* action is found with the name being requested, that we'll use that controller and assume that all the necessary permutations of that action will be provided too. (A post action vs Get action for example).

There are two parts to solving this problem, finding the type we want to create, and creating the controller from that type.

The process will be similar to that of the ViewEngine example:

1) Reverse the module list order so we have the most recently loaded first
2) Scan all the types in the assembly for that module
3) Find a type with the name we're looking for ( <Name>Controller )
4) Scan the methods on that type to find the action we're looking for
5) If found, return this type
6) Else Continue

Obviously reflecting on all these types is a slow process, and we should cache the type once found by configuration id, controller name and action name.

Here is some code which loosely achieves the above:

private Type FindControllerType(String controllerName, RequestContext requestContext, Configuration currentConfiguration)
        {
            // Generate the type name we're looking for
            String controllerTypeName = string.Format("{0}Controller", controllerName);

            // Get the action and therefore method name we're looking for
            String actionName = (string)requestContext.RouteData.Values["action"];

            // TODO: Check Cache here

            // Get modules in reverse order
            var searchModules = currentConfiguration
                .Modules
                .Reverse();

            foreach (var module in searchModules)
            {
                // Get all the types in the assembly
                Type[] controllerTypes = module.Assembly.GetTypes()
                    .Where(
                        t =>
                            // Where the type name is the one we're looking for
                            t.Name == controllerTypeName &&

                            // Where it can be cast to a controller
                            typeof(IController).IsAssignableFrom(t) &&

                            // And there is a public instance method with the name we're looking for on that type
                            t.GetMethods(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance)
                            .Where(m => m.Name == actionName).Count() > 0

                        ).ToArray();

                // Skip to the next module if no types found
                if (controllerTypes.Length == 0) { continue; }

                // Else, simply return the first one found
                return controllerTypes[0];
            }

            // Fail
            return null;
        }


A very rudimentary controller factory implementation would therefore look something like this:

 public class ModuleControllerFactory : IControllerFactory
    {
        private IConfigurationProvider mConfigurationProvider;

        public ModuleControllerFactory(IConfigurationProvider configurationProvider)
        {
            mConfigurationProvider = configurationProvider;
        }

        public IController CreateController(System.Web.Routing.RequestContext requestContext, string controllerName)
        {
            Type t = FindControllerType(controllerName, requestContext, mConfigurationProvider.GetActiveConfiguration());
            return (IController)Activator.CreateInstance(t);
        }

        public void ReleaseController(IController controller)
        {
            IDisposable disposable = controller as IDisposable;
            if (disposable != null)
            {
                disposable.Dispose();
            }
        }
// Etc

It is of course probably desirable to instantiate the controller using your favourite IOC container  - so for StructureMap for example instead of using Activator.CreateInstance you would call ObjectFactory.GetInstance(t);

(Actually, you'd probably inject the container as well rather than calling ObjectFactory directly but you get the gist).

Summary

Essentially, we can completely re-wire this part of the ASP.NET MVC framework to do what we want it to do. We can load our controllers from wherever based on whatever context we like - and this gives us a powerful mechanism for pluggability and therefore multi-tenancy.

I did contemplate trying to achieve this through routing - custom routing constraints and handlers - but it's not a tidy solution, it generally means having different names for your controllers, or playing havok with namespaces and configuration and because a lot of that configuration is static it often involves re-compilation.

Re-compilation is something to be avoided, as we ideally want to be able to add new customers by just modifying configuration.

Anyway, there are a number of options and this is just one of them,  I'll be hoping to cover a crazy solution using Reflection.Emit and hopefully delve into MEF before I'm done with this particular part of the multi-tenancy story.

This used to ask if you wanted to hire me

But chances are I'm not available, as I'm busy shipping stuff.

I am available for conference speaking, I like talking and will do so for just T&E (and perhaps a bottle of wine or two).

I have done soft keynotes, and usually entertain/rile people in equal proportion, but prefer to talk tech as that's what I do

Email me :)

blog comments powered by Disqus

jt


Thanks for the series! It's very interesting, can't wait for each new post :D Specially using MEF ;) Shame we can't switch out these modules at runtime due to ASP.NET Routing's limitations :(

Blob


Hi,Do you plan to create a sample application "Multi tenancy in ASP.NET MVC" according to your articles?Bye and thank you,

robashton


Jt =&rt; MEF <-- Me too, it's quite exciting - I prefer the Reflection.Emit version but that's just because I find it entertaining to write :)Blob => YES, DEFINITELY! I have actual plans to create a multi-tenant application over a series of blog entries at the end of this series

Bruce


I'm definitely looking forward to the blog entries in which you put together a simple multi-tenant application.I'm working out a multi-tenant application of my own, but my solutions were not nearly as cool as yours.It seems that this should be a very popular topic, but I haven't found many discussing it online.

Blob


robashton =&rt; Thanks

Sean


Awesome awesome awesome stuff here. Really looking forward to your progress and the resultant application.

Matt


Very interesting and well written posts. Very much looking forward to the upcoming posts as this is something that would be very very useful provided it can be done in a logical manner.

Matt


So I'm playing around with MvcEx in my project and have a few errors I'm trying to work though. Everything seems to be working fine so far, but I have a generic base controller where I override the OnException method so I can do some work before throwing the exception.Any suggestion on how I can still run my custom code in this method when my controller is now inheriting from MvcExActionContainer?

robashton


Again with the disclaimer that this is just a reference library, I can tell you that it is indeed possible to still have a base controller in MvcExmApplication.ControllerFactory.SetBaseControllerType(typeof(BaseController));Where mApplication is the IMvcExApplicationThis comes with the caveat that the base controller type must inherit from MvcExController.I'm thinking of ditching the componentised controllers in our code and replacing them with the above solution (although it's less 'neat')

Matt


So I'm still plugging away at this :). I really like the idea of what you've conceived with this MvcEx and it looks like you've put some work into it.My current problem is that if I set up my BaseController to inherit from MvcExController, then all of my controllers that inherit from BaseController and up being MvcExControllers.But the only way I can get my Home controller to get recognized is to use the MvcExActionContainer attribute on it, as you have in the MvcEx solution.This of course doesn't actually work later on as it tries to build in CreateMethodOnDynamicController as it's looking for ActionContainer methods that my HomeController doesn't have since it's not inherited from MvcExActionContainer.As I write this I realize of course it shouldn't work the way I have it, but I can't seem to get my Home controller recognized any other way. Clearly I'm just not quite yet grasping the bigger picture of how these all work together.Do you have any suggestions on how I should structure my controllers with my BaseController inheriting from MvcExController?

robashton


Change your controllers to inherit from MvcExActionContainer - they'll still get the behaviour from your BaseControllerThe limitation here is that you can only have one base controller - but you can always make a base action container if you need shared functionality.Like I said above I'm not sure this is the best solution for backwards compatability reasons - I've effectively removed the responsibility of generic request handling to a separate class (the base controller), and left the actions able to come from anywhere for a particular controller.IMO this is how it should work anyway, why you've got to have all your actions on a single controller which also has control over the entire request lifecycle is beyond me.So, what we have here (and again I'm not saying it's *right*), isCreate a base controller and tell MvcEx to use it - this can be used to intercept all requests to introduce common logic (action filters etc)Create your action containers inheriting from MvcExActionContainer (Yeah I know, an attribute AND a class? I'm tempted to ditch the requirement for MvcExActionContainer inheritance altogether)

robashton


Oh, and a new blog entry will be incoming once I've met a deadline at work - manic times at the moment :)

Asad Ali Butt


reading your posts makes one feel more comfortable about MVC stuff. Do appriciate

Trent


Hey is there a part 2 to this article and is there a sample app

robashton


Writing it right now!I ran out of steam on this series because of work, so I'll be trying to wrap it up in the next couple of entries, and then start blogging about the other aspects multi-tenancy in ASP.NET MVC that we've found to be a struggle :)