Multi-tenant ASP.NET – Foundation

Part I – Introduction

 

In my last post, I talked about some of the goals of multi-tenancy in general and some hints about my implementation. Now it is time to put a little meat on the bones. I’m ready to share my ideas about how to implement a multi-tenant system on ASP.NET MVC and IIS7. I’m excited about some of the improvements in both areas in recent years… without some of the advancements in those technologies, I’m sure my code would have been much more complicated. (Here' is the link to the source code if you’d like to follow along :) )

 

Modeling the tenant

As I described in my first post on multi-tenancy, I’m working with a multi-tenancy architecture because of my job. At eagleenvision.net, we are contracted by clients to create websites, all with similar structure. Because of the similarity in implementation, I felt that it was necessary to eliminate our issue of maintaining different codebases for the same application. This is problematic in that some enhancements applied to some clients and not to others.

Going back to multi-tenancy, a “tenant” to me is simply a client. This client has a few different attributes that are reflected in the model. Here’s the interface I use to describe a tenant.

public interface IApplicationTenant
{
    string ApplicationName { get; }

    IFeatureRegistry EnabledFeatures { get; }

    IEnumerable<string> UrlPaths { get; }

    IContainer DependencyContainer { get; }

    IViewEngine ViewEngine { get; }
}

 

The first thing you might notice is the UrlPaths property. Url paths are root URL paths that are mapped to a tenant. One of my requirements is that multiple bindings can exist for the same tenant. In some applications, this might not be a requirement, in which case you might want to make this a single string.

The next thing you’ll notice is that a tenant has a dependency container. Like I alluded to in my previous post, my implementation uses 1:1 tenant to container. That is, every tenant has it’s own isolated container from the other tenants. This is an important aspect of the tenant and it really sets my implementation apart from the others I have seen. IoC is my friend… I intend to use StructureMap to do a lot of my bidding. This allows for a lot of flexibility and isolation that a multi-tenancy application should have.

 

Features

As you can see from the IApplicationTenant interface, a tenant has a list of features that are enabled by the tenant, called a feature registry. The feature registry is responsible for describing which features are enabled and which features the tenant is allowed to utilize. Here’s some relevant code for features.

 

public interface IFeature
{
    string FeatureName { get; }
}

public interface IComplexFeature : IFeature
{
    IEnumerable<IFeature> SubFeatures { get; }
}

public interface IFeatureRegistry
{
    IEnumerable<IFeature> Features { get; }

    bool IsEnabled(IEnumerable<string> featurePath);
}

 

A feature is a simple string. A complex feature is a feature that has sub features. The feature registry is responsible for the features enabled by the application tenant. Feature path describes the path to the lowest enabled sub feature. For example, if there’s a feature path such as A – B – C, A and B are complex features where B is a sub feature of A and C is a sub feature of B. This enables a lot of power in describing features and sub features. In MvcEx, I believe the concept of Module is similar. The distinction here is that a controller doesn’t have to be a feature, but it can be a feature. Here’s an example of some of the uses of the “Feature” attribute that will check the tenant’s feature registry once a controller has been called for execution.

// explicit feature paths

public class AccountController : Controller
{
    [Feature("Account", "Login")]
    public ActionResult Login()
    {
        return View("Login");
    }

    [Feature("Account", "Logout")]
    public ActionResult Logout()
    {
        return View("Logout");
    }

    [Feature("Account", "Register")]
    public ActionResult Register()
    {
        return View("Register");
    }
}

// implicit feature path from controller name

[Feature]
public class AccountController : Controller
{
    // Account -> Login
    public ActionResult Login()
    {
        return View("Login");
    }

    // Account -> Logout
    public ActionResult Logout()
    {
        return View("Logout");
    }

    // Account -> Register
    public ActionResult Register()
    {
        return View("Register");
    }
}

 

The implementation of this attribute are a bit complicated. Just look at the source for a complete implementation.

I have also included a default implementation for IsEnabled in the Features class of the source code. It’s a pretty simple implementation this is hardcoded for IFeature and IComplexFeature. (This probably isn’t the best design for the API, but it suits my needs. Any suggestions on an API would be wonderful.)

 

Tenant Selection

One of the concepts that is essential to the delegation of requests to the various tenants is tenant selection. As you can see from the code below, there’s an interface that handles tenant selection and querying of the tenants (ITenantSelector). The tenant selector is responsible for getting the tenant for the request or an exception can be thrown if a tenant cannot be found. The default tenant selector queries the tenants based upon the root URL (“BaseUrl” in my implementation) and selects the first tenant with that URL. Obviously you can implement your own tenant selector that works off something else, such as a route value, etc. However, this sort of selection process makes sense for my implementation.

 

public interface ITenantSelector
{
    IEnumerable<IApplicationTenant> Tenants { get; }

    IApplicationTenant Select(RequestContext context);
}

public class DefaultTenantSelector : ITenantSelector
{
    public DefaultTenantSelector(IEnumerable<IApplicationTenant> tenants)
    {
        Ensure.Argument.NotNull(tenants, "tenants");
        this.Tenants = tenants;
    }

    public IEnumerable<IApplicationTenant> Tenants { get; private set; }

    public IApplicationTenant Select(RequestContext context)
    {
        Ensure.Argument.NotNull(context, "context");

        string baseurl = context.HttpContext.BaseUrl().TrimEnd('/');

        var valid = from tenant in this.Tenants
                    from path in tenant.UrlPaths
                    where path.Trim().TrimEnd('/').Equals(baseurl, StringComparison.OrdinalIgnoreCase)
                    select tenant;

        if (!valid.Any())
            throw new TenantNotFoundException();
        return valid.First();
    }
}

 

A related concept is container resolution. Because I have a lot of different containers floating around and I don’t want certain things knowing about tenants, there is an abstraction on container resolution. The default resolver selects a tenant. This code will be available with the source code.

 

Where’s the code?!

I have the source code on my GitHub account. Feel free to fork it or use to your whim. Make sure you keep up with my blog series to get the inside scoop. This post goes along with the f8cf7e5408eff7b659f3 commit. Be aware that the code introduced in this post are subject to change with subsequent commits.

 

Up Next

Next up is controller resolution. You’ll see how controllers are resolved and how my implementation uses controllers. You will be surprised at how simple the implementation is. In contrast, MvcEx uses a lot of Reflection.Emit to achieve “Dynamic Controllers”. While this may work for some applications, I think it’s a bit overkill. You will see what I mean in my next post.

Until next time, leave questions in the comments, DM me on Twitter, or send me an email.


kick it on DotNetKicks.com

4 Comments

  • MvcEx only did reflection.emit as an experiment (and really, *really* please don't tell people to use it, it's only a tech demo :))

    My used implementation just selects a controller and returns it rather than trying to generate proper controllers - it's less pure but it's a lot tidier, simpler and less error prone.

    I assume this is what you're going to do - and it's a much better solution.

    Keep up the posts, avidly following to see if you end up doing anything majorly different to how I approach it at work.

  • In fact, I think I cover that point in the video http://vimeo.com/9217399

    I'd hate for people to think I was actually advocating the use of Reflection.Emit where there were far more simple options available.

  • Just wanted to fire you a note to let you know how much I've enjoyed the first two posts in this series - and I'm really looking forward to reading more.

    Thanks for putting this out there!

  • good post. waiting for next installment.

Comments have been disabled for this content.