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.
These are the modules exposed by the application, the controllers they provide and the actions those controllers have on them.
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
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).
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.
2020 © Rob Ashton. ALL Rights Reserved.