Multi-tenant ASP.NET MVC – Inversion of Control

Part I – Introduction

Part II – Foundation

Part III – Controllers

Part IV – Views

Source Code

One of the most important aspects of my Multi-tenant ASP.NET MVC implementation is Inversion of Control containers. These containers are essential to wiring up and finding the proper controllers. In my last iteration of code that followed the Views post, my setup for the container was a simple association of types added to the PluginGraph. I really do not want to have to do this manually. Therefore, I want to utilize StructureMap the best I can. This post will show you how I wire up controllers by convention rather than by configuration.

 

The Convention

The main convention I will be targeting is this: the controller that will be wired must have the same name as the root (or a unique name) and must inherit from a controller of the same name or “Controller”. For example, this first class would be in the “Host” project (for this example, the controller is “AccountController”). If a tenant project DID NOT override the AccountController, the host’s AccountController would be added to the container. If a tenant DID override AccountController, the AccountController in the tenant would be used as the AccountController. Here’s the example in code:

 

// AccountController in the Host project
namespace MyApp.HostWeb
{
    public class AccountController : Controller
    {
        ....
    }
}
// AccountController in the Tenant project
namespace MyApp.FirstTenant
{
    public class AccountController : MyApp.HostWeb.AccountController
    {
        ....
    }
}

The obvious downside to this implementation is that the fully qualified controller name must be used after the “:” to denote inheritance. You could implement this a bit differently, but I’ve chosen this way for simplicity.

 

Wiring Up Controllers

The question that arises is how to inject this convention into container configuration using StructureMap. The way I’ve chosen to go is by using assembly scanner conventions. An assembly scanner will blindly go through all the types in a specified assembly and run the types against a convention. There are some conventions already in place, but we can also write our own conventions.

In my implementation, the host and tenant project assemblies will both be scanned for controllers. Here’s how the assembly scanning works inside the configuration of a container:

 

var container = new Container();
container.Configure(config =>
{
    config.Scan(scanner =>
    {
        // add the conventions
        scanner.Convention<ControllerConvention>();
        // specify assemblies to scan (just examples here, simpler in practice)
        scanner.TheCallingAssembly();
        scanner.Assembly(typeof(Something).Assembly);
        scanner.Assembly("MyAssembly");
        scanner.AssemblyContainingType<Foo>();
    });
});

 

Writing the Convention

The next step is to write ControllerConvention so that the controllers will be added correctly to the container. I’m using a little black magic to intercept types and initialize them properly using an interceptor. However, this is frankly the only way I’ve found that works. But anyways, here’s the code.

public class ControllerConvention : IRegistrationConvention
{
    public void Process(Type type, Registry registry)
    {
        if (registry == null || !IsValidController(type))
            return;
        var baseClass = type.BaseType;
        if (!IsValidController(baseClass) || !baseClass.Name.Equals(type.Name))
            registry.AddType(typeof(IController), type);
        else
        {
            registry.AddType(typeof(IController), baseClass);
            registry.RegisterInterceptor(new TypeReplacementInterceptor(baseClass, type));
        }
    }
    private static bool IsValidController(Type type)
    {
        return type != null && !type.IsAbstract && typeof(IController).IsAssignableFrom(type) &&
               type.Name.EndsWith("Controller") && type.IsPublic;
    }
    private class TypeReplacementInterceptor : TypeInterceptor
    {
        private readonly Type typeToReplace;
        private readonly Type replacementType;
        public TypeReplacementInterceptor(Type typeToReplace, Type replacementType)
        {
            this.typeToReplace = typeToReplace;
            this.replacementType = replacementType;
        }
        public bool MatchesType(Type type)
        {
            return type != null && type.Equals(this.typeToReplace);
        }
        public object Process(object target, IContext context)
        {
            // Sanity check: If the context is null, we can't do anything about it!
            if (context == null)
                return target;
            return context.GetInstance(this.replacementType);
        }
    }
}

Going back to the AccountController example, suppose that the controller is overridden by the tenant. The way the convention will look at it is that the names match and the base type is a controller. Therefore, the BASE TYPE is added to the PluginGraph and will be INTERCEPTED once requested by the TypeInterceptor.

 

Another Way to Implement

Another way to implement this behavior is through a type scanner (ITypeScanner). The process is the same as the convention, but you are given a PluginGraph directly rather than using a registry. There is a subtle difference in the types, but the idea is the same. In fact, you can copy and paste the body of the “Process” code right into a type scanner and only have to change “registry.RegisterInterceptor(…)” to “graph.InterceptorLibrary.AddInterceptor(…);”.

 

Caveat

There’s a bit of a wonkiness in the type interceptor. If you look at the “Process” method of the interceptor, you’ll notice you’re given a target. If you’re overriding a controller, target will be the base controller. For example, going back to the AccountController example, target will be an instance of the AccountController from the Host. While this is not so problematic, there is still no need for this instance since it will be intercepted and not used. This is the price you pay for having such a structure in StructureMap. If someone has a better way than I’ve described, I’m all ears :)

 

Up Next

I have 3 things on my list that I need to talk about. The first is routing with multi-tenancy, second is content resolution and the third is wiring this all up in IIS7. This might be the order of the post or it might not be. We will see. But until next time, keep the questions and discussions going. Email me, @-reply or DM me on Twitter(Source code link)

1 Comment

Comments have been disabled for this content.