Sunday, 20 August 2017

ASP.NET MVC Multitenant routing with OWIN

Recently I have been migrating one of my multitenant ASP.NET MVC application's to the OWIN middleware. This has presented me with an opportunity to change my initial implementation. In this blog post I am going to explore a possible solution.

Requirement

Provide a simple to understand, use and maintain multitenant OWIN middleware.

Solution

This solution was inspired by the OpenIDConnect middleware. Here is how it’s going to look like when we are done:
public class Startup
{
    public void Configuration(IAppBuilder app)
    {
      //...
      app.UseMultitenancy(
          //under what conditions multitenancy middleware should be used
          new WhenMiddleware((context) =>
          {
              HttpContextBase httpContext = (HttpContextBase)context.Environment["System.Web.HttpContextBase"];
              RouteData routeData = RouteTable.Routes.GetRouteData(httpContext);
              return Task.FromResult(routeData != null && routeData.DataTokens.ContainsValue("multi"));
          }),
          new MultitenancyNotifications
          {
              TenantNameCouldNotBeFound = context =>
              {
                  //Do what you need to do... redirect, throw exception, etc.
                  throw new HttpException(400, "Tenant name must be provided");
              },
              TenantCouldNotBeResolved = context =>
              {
                  //Do what you need to do... redirect, throw exception, etc.
                  context.Response.Redirect("https://your_url_goes_here");
                  return Task.FromResult(0);
              },
              TenantResolved = (context, tenant) =>
              {
                  //Do what you need to do... update DI object, redirect, etc. 
                  ServiceLocator.Resolve<Tenant>(new {id = tenant.ID, name = tenant.Name});
                  return Task.FromResult(0);
              }
          }
      );
    }
}

Theory

When it comes to multitenancy we need to do 3 things:
  • Conditional Check - Check the context, make sure that conditions are right for the multitenancy execution.
  • Extraction - Extract tenant name from the payload, this payload can be anything, Http Request, Queue Message, etc.
  • Resolution - Resolve tenant settings by using tenant name. These setting properties can include tenant connection string (if you are creating database per tenant), authentication secrets, etc.

Implementation

To make my app work I have decided to use MVC routing with data tokens for tenant discremintation:

RouteConfig.cs
   
public class RouteConfig
{
    public static void RegisterRoutes(RouteCollection routes)
    {
        //...
        routes.MapRoute(
          name: "multi",
          url: "{tenant}/{controller}/{action}/{id}",
          defaults: new { controller = "Login", action = "Index", id = UrlParameter.Optional }
        ).DataTokens.Add("name", "multi"); 
    }
}
You don’t have to use MVC routing with data tokens in your application you can use subdomains, headers, etc.

Conditional Check

OWIN pipeline supports MapWhen, MapWhen creates pipeline branching. I have decided to avoid that unnecessary complexity at this stage. Instead I have chosen to use linear conditional approach, if conditions are correct execute the middleware otherwise just skip it. To make this work I have decided to create my own middleware:

WhenMiddleware.cs
   
public class WhenMiddleware
{
    private Func<IOwinContext, Task<bool>> condition;

    public WhenMiddleware(Func<IOwinContext, Task<Boolean>> condition)
    {
        this.condition = condition;
    }

    public async Task Invoke(IOwinContext context, Func<IOwinContext, Func<Task>, Task> conditionalNext, Func<Task> next)
    {
        if (condition(context).Result)
        {
            await conditionalNext(context, next);
        }
        else
        {
            await next();
        }
    }
}
It will be used like this later on:
   
Func<IOwinContext, Func<Task>, Task> conditionalNext = (context, next) =>
    new SomeMiddleware.Invoke(context, next);

app.Use((context, next) => when.Invoke(context,  conditionalNext, next));

Extraction

There are lots of different payloads that you might want to extract data out of. As such I have decided to introduce this interface:

ITenantNameExtractor.cs
   
public interface ITenantNameExtractor
{
    string GetName(IOwinContext context);
}

In my example I am going to extract tenant name from the MVC route using data tokens, here is the concrete implementation:

RouteDataTokensTenantNameExtractor.cs
   
public class RouteDataTokensTenantNameExtractor : ITenantNameExtractor
{
    public string GetName(IOwinContext context)
    {
        HttpContextBase httpContext = (HttpContextBase) context.Environment["System.Web.HttpContextBase"];
        RouteData routeData = RouteTable.Routes.GetRouteData(httpContext);
        if (routeData != null && routeData.DataTokens.ContainsValue("multi"))
            return routeData.GetRequiredString("tenant");

        return null;
    }
}

If I wanted to extract tenant name from the header then I would have implemented something like this:

HeaderTenantNameExtractor.cs
   
public class HeaderTenantNameExtractor : ITenantNameExtractor
{
    public string GetName(IOwinContext context)
    {
        string tenantName = context.Request.Headers["tenantname"];
        //...
    }
}

Resolution

Resolution is where you call some service or repository and you obtain all data that is required to hydrate tenant object. You might get a connection string, authentication client id and secret etc.

Tenant.cs
   
public class Tenant
{
    public  Guid ID { get; private set; }
    public string Name { get; private set; }
    //.. add your properties

    public Tenant(Guid id, string name, //..add your properties)
    {
        this.ID = id;
        this.Name = name;
        //.. add your properties 
    }
}

ITenantResolver.cs
   
public interface ITenantResolver
{
    Tenant GetTenant(string tenantName);
}

TenantResolver.cs
   
public class TenantResolver : ITenantResolver
{
    private readonly ITenantService tenantService;

    public TenantResolver(ITenantService tenantService)
    {
        this.tenantService = tenantService;
    }

    public Tenant GetTenant(string tenantName)
    {
        TenantDto tenantDto = this.tenantService.Get(tenantName).Value;
        if (tenantDto != null)
        {
            return new Tenant(tenantDto.Id, tenantDto.Name, ...);
        }
        return null;
    }
}

It’s highly likely that you will want to cache tenant resolution, to do this you can use decorator pattern and cache your tenant data:

CacheTenantResolver.cs
   
public class CacheTenantResolver : ITenantResolver
{
    private TenantResolver tenantResolver;

    public CacheTenantResolver(TenantResolver tenantResolver)
    {
        this.tenantResolver = tenantResolver;
    }

    public Tenant GetTenant(string tenantName)
    {
        string cacheKey = $"tenantName:{tenantName}";

        Tenant tenant = (Tenant)MemoryCache.Default[cacheKey];

        if (tenant != null)
            return tenant;

        tenant = this.tenantResolver.GetTenant(tenantName);
        if (tenant == null)
            return null;

        MemoryCache.Default.Set(cacheKey, tenant, new CacheItemPolicy
        {
            AbsoluteExpiration = DateTimeOffset.Now.AddMinutes(10)
        });
    
        return tenant;
    }
}

Bringing it all together

Now that we have all key components we need to bring it all together.

MultitenancyMiddleware.cs
   
public class MultitenancyMiddleware
{
    ITenantNameExtractor tenantNameExtractor;
    ITenantResolver tenantResolver;
    MultitenancyNotifications notifications;

    public MultitenancyMiddleware(ITenantNameExtractor tenantNameExtractor, 
        ITenantResolver tenantResolver, MultitenancyNotifications notifications)
    {
        this.tenantNameExtractor = tenantNameExtractor;
        this.tenantResolver = tenantResolver;
        this.notifications = notifications;
    }

    public async Task Invoke(IOwinContext context, Func<Task> next)
    {
        string name = this.tenantNameExtractor.GetName(context);

        if (string.IsNullOrEmpty(name))
        {
            await this.notifications.TenantNameCouldNotBeFound(context);
        }
        else
        {
            Tenant tenant = this.tenantResolver.GetTenant(name);
            if (tenant == null)
            {
                await this.notifications.TenantCouldNotBeResolved(context);
            }
            else
            {
                await this.notifications.TenantResolved(context, tenant);
                await next();
            }
        }

    }
}

MultitenancyNotifications.cs
   
public class MultitenancyNotifications
{
    public Func<IOwinContext, Task> TenantCouldNotBeResolved { get; set; }
    public Func<IOwinContext, Task> TenantNameCouldNotBeFound { get; set; }
    public Func<IOwinContext, Tenant, Task> TenantResolved { get; set; }

    public MultitenancyNotifications()
    {
        this.TenantCouldNotBeResolved = context => Task.FromResult(0);
        this.TenantNameCouldNotBeFound = context => Task.FromResult(0);
        this.TenantResolved = (context, tenant) => Task.FromResult(0);
    }
}

AppBuilderExtensions.cs
   
public static IAppBuilder UseMultitenancy(this IAppBuilder app, WhenMiddleware whenMiddleware, MultitenancyNotifications notifications)
{
    Func<IOwinContext, Func<Task>, Task> conditionalNext = (context, next) =>
        ServiceLocator.Resolve<MultitenancyMiddleware>(new {notifications = notifications})
            .Invoke(context, next);

    app.Use((context, next) => whenMiddleware.Invoke(context,  conditionalNext, next));

    return app;
}
Service locator is using Castle Windsor DI. Castle Windsor allows you to resolve services with anonymous types:
 
((IWindsorContainer)container).Resolve<T>(argumentsAsAnonymousType);

*Note: Code in this article is not production ready and is used for prototyping purposes only. If you have suggestions or feedback please do comment.

No comments:

Post a Comment