Mulit-tenant ASP.NET MVC – Controllers

Part I – Introduction

Part II – Foundation

 

The time has come to talk about controllers in a multi-tenant ASP.NET MVC architecture. This is actually the most critical design decision you will make when dealing with multi-tenancy with MVC. In my design, I took into account the design goals I mentioned in the introduction about inversion of control and what a tenant is to my design. Be aware that this is only one way to achieve multi-tenant controllers.

 

The Premise

MvcEx (which is a sample written by Rob Ashton) utilizes dynamic controllers. Essentially a controller is “dynamic” in that multiple action results can be placed in different “controllers” with the same name. This approach is a bit too complicated for my design. I wanted to stick with plain old inheritance when dealing with controllers. The basic premise of my controller design is that my main host defines a set of universal controllers. It is the responsibility of the tenant to decide if the tenant would like to utilize these core controllers. This can be done either by straight usage of the controller or inheritance for extension of the functionality defined by the controller. The controller is resolved by a StructureMap container that is attached to the tenant, as discussed in Part II.

 

Controller Resolution

I have been thinking about two different ways to resolve controllers with StructureMap. One way is to use named instances. This is a really easy way to simply pull the controller right out of the container without a lot of fuss. I ultimately chose not to use this approach. The reason for this decision is to ensure that the controllers are named properly. If a controller has a different named instance that the controller type, then the resolution has a significant disconnect and there are no guarantees. The final approach, the one utilized by the sample, is to simply pull all controller types and correlate the type with a controller name. This has a bit of a application start performance disadvantage, but is significantly more approachable for maintainability. For example, if I wanted to go back and add a “ControllerName” attribute, I would just have to change the ControllerFactory to suit my needs.

 

The Code

The container factory that I have built is actually pretty simple. That’s really all we need. The most significant method is the GetControllersFor method. This method makes the model from the Container and determines all the concrete types for IController. 

The thing you might notice is that this doesn’t depend on tenants, but rather containers. You could easily use this controller factory for an application that doesn’t utilize multi-tenancy.

public class ContainerControllerFactory : IControllerFactory
{
    private readonly ThreadSafeDictionary<IContainer, IDictionary<string, Type>> typeCache;

    public ContainerControllerFactory(IContainerResolver resolver)
    {
        Ensure.Argument.NotNull(resolver, "resolver");
        this.ContainerResolver = resolver;
        this.typeCache = new ThreadSafeDictionary<IContainer, IDictionary<string, Type>>();
    }

    public IContainerResolver ContainerResolver { get; private set; }

    public virtual IController CreateController(RequestContext requestContext, string controllerName)
    {
        var controllerType = this.GetControllerType(requestContext, controllerName);

        if (controllerType == null)
            return null;

        var controller = this.ContainerResolver.Resolve(requestContext).GetInstance(controllerType) as IController;

        // ensure the action invoker is a ContainerControllerActionInvoker
        if (controller != null && controller is Controller && !((controller as Controller).ActionInvoker is ContainerControllerActionInvoker))
            (controller as Controller).ActionInvoker = new ContainerControllerActionInvoker(this.ContainerResolver);

        return controller;
    }

    public void ReleaseController(IController controller)
    {
        if (controller != null && controller is IDisposable)
            ((IDisposable)controller).Dispose();
    }

    internal static IEnumerable<Type> GetControllersFor(IContainer container)
    {
        Ensure.Argument.NotNull(container);
        return container.Model.InstancesOf<IController>().Select(x => x.ConcreteType).Distinct();
    }

    protected virtual Type GetControllerType(RequestContext requestContext, string controllerName)
    {
        Ensure.Argument.NotNull(requestContext, "requestContext");
        Ensure.Argument.NotNullOrEmpty(controllerName, "controllerName");

        var container = this.ContainerResolver.Resolve(requestContext);

        var typeDictionary = this.typeCache.GetOrAdd(container, () => GetControllersFor(container).ToDictionary(x => ControllerFriendlyName(x.Name)));

        Type found = null;
        if (typeDictionary.TryGetValue(ControllerFriendlyName(controllerName), out found))
            return found;
        return null;
    }

    private static string ControllerFriendlyName(string value)
    {
        return (value ?? string.Empty).ToLowerInvariant().Without("controller");
    }
}

One thing to note about my implementation is that we do not use namespaces that can be utilized in the default ASP.NET MVC controller factory. This is something that I don’t use and have no desire to implement and test. The reason I am not using namespaces in this situation is because each tenant has its own namespaces and the routing would not make sense in this case.

 

Because we are using IoC, dependencies are automatically injected into the constructor. For example, a tenant container could implement it’s own IRepository and a controller could be defined in the “main” project. The IRepository from the tenant would be injected into the main project’s controller. This is quite a useful feature.

 

Again, the source code is on GitHub here.

 

Up Next

Up next is the view resolution. This is a complicated issue, so be prepared. I hope that you have found this series useful. If you have any questions about my implementation so far, send me an email or DM me on Twitter. I have had a lot of great conversations about multi-tenancy so far and I greatly appreciate the feedback!


kick it on DotNetKicks.com

3 Comments

  • My only comment here would be that with this kind of approach, it would be difficult to override single actions, as you'd have to override entire controllers.

    This is why (not in MvcEx because that is too complicated) in my blog series I looked at the actions as well as the controllers themselves, so an application could have multiple controllers with the same name but only one of them might be valid for a certain request because of the actions it has.

    This might sound like overkill, but in practise it is very useful because it means we can override common actions on a per-customer basis (Account has Logon as well as half a dozen other actions, but we only override the Logon screen because we capture and display wildly different information per customer)

    This is similar to an approach in FubuMVC where you don't worry about the controllers themselves, you worry about the actions - and actions don't have to be situated on controllers because they can come from anywhere.

    Controllers are just a convenient container for actions.

  • Rob,

    What's the problem with overriding a whole controller? By separating controllers into separate types, you're adding a lot of unnecessary complexity. Instead of simply pulling a controller type from a container, you you have decide where the action resides in various different types. To me, the benefits do not outweigh the complexity of the solution.

  • What complexity? We're not talking the crazy refleciton.emit stuff here - just selecting a controller based on not only the controller currently being requested, but the action as well.

    I use our IOC container for this.

    That's still overriding the entire controller, but you're basing your decision on whether it has the action you want or not.

    It's about 2 lines of extra code and gives the flexibility we *need* to make things work if we're to use MS MVC over something like Fubu

Comments have been disabled for this content.