April 20, 2016

Handling unresolved tenants in SaasKit

The core component of SaasKit’s multi-tenancy library is tenant-resolution middleware that attempts to identity a tenant based on information available in the current request, for example, the hostname or current user.

When a tenant cannot be found the middleware does nothing. A call to HttpContext.GetTenantContext<TTenant> or HttpContext.GetTenant<TTenant> will return null. This means that if any of your controllers/classes have a dependency on your tenant type, you’ll get an exception:

Unable to resolve service for type ‘AspNetMvcSample.AppTenant’ while attempting to activate ‘AspNetMvcSample.Controllers.HomeController’.

This is because the built in DI cannot resolve an instance of your tenant class. Internally it’s just trying to pull the tenant out of HttpContext which of course returns null.

It’s down to you to decide how you want to handle unresolved tenants, and you have a few options.

Provide a default instance

If a tenant cannot be resolved for the current request you can return a default instance. You can do this in your tenant resolver:

public Task<TenantContext<AppTenant>> ResolveAsync(HttpContext context)
{
    TenantContext<AppTenant> tenantContext = null;

    var tenant = tenants.FirstOrDefault(t => 
        t.Hostnames.Any(h => h.Equals(context.Request.Host.Value.ToLower())));

    if (tenant == null)
    {
        tenant = new AppTenant { Name = "Default Tenant", Hostnames = new string[0] };
    }

    tenantContext = new TenantContext<AppTenant>(tenant);

    return Task.FromResult(tenantContext);
}

With this in place, the middleware will always return a tenant preventing your application from blowing up. Of course you’ll need to ensure that your default tenant has all the necessary information your application needs to run (default connection strings etc.).

Redirecting to another site

Alternatively you may wish to redirect the user somewhere else if a tenant can not be resolved such as a specific landing or on-boarding page on your marketing site.

SaasKit has built-in middleware for doing exactly this. Just specify the redirect URL and whether the redirect should be permanent (301):

app.UseMultitenancy<AppTenant>();
app.UseMiddleware<TenantUnresolvedRedirectMiddleware<AppTenant>>("http://saaskit.net", false);

Currently there isn’t an extension method for registering the middleware so you’ll have to use the UseMiddleware method and provide the arguments as above.

Now if I browse to my application and no matching tenant is found I’ll be redirected to the SaasKit home page.

Redirecting to a page in the same site

If you want to redirect to a page in the same site the easiest approach is to redirect to a static HTML page. Create a standard HTML page and make sure you place it in your wwwroot folder. Then add the redirect middleware:

app.UseMiddleware<TenantUnresolvedRedirectMiddleware<AppTenant>>("/tenantnotfound.html", false);

Providing you’re using the Static Files middleware and that it is registered before SaasKit, it will serve the specified HTML page and short-circuit the request.

If you want to redirect to dynamic page (one served by ASP.NET) then you can’t use the provided redirect middleware as you’ll end up in a redirect loop, since the middleware redirect is invoked on every request.

A solution to this is to have your tenant resolver return a default tenant (see above) then add some middleware that checks if the current tenant instance is the default tenant then do a redirect:

app.Map(
    new PathString("/onboarding"),
    branch => branch.Run(async ctx =>
    {
        await ctx.Response.WriteAsync("Onboarding");
    })
);

app.UseMultitenancy<AppTenant>();

app.Use(async (ctx, next) =>
{
    if (ctx.GetTenant<AppTenant>().Name == "Default")
    {
        ctx.Response.Redirect("/onboarding");
    } 
    else
    {
        await next();
    }
});

app.UseMiddleware<LogTenantMiddleware>();

The above example is quite primitive but illustrates this behaviour. First I created an “Onboarding” page at /onboarding using a simple middleware delegate. If you’re redirecting to an MVC controller you don’t need to do this.

Then (after the tenant resolution middleware has been registered) I add another middleware delegate that checks the current tenant and if it’s the default, redirects them to my on-boarding page.

Doing something else

If you want to do something else when a tenant cannot be resolved just create a middleware component and obtain the current tenant context. I chose not to build this into the framework because it’s really this simple:

public class CustomTenantMiddleware
{
    RequestDelegate next;

    public CustomTenantMiddleware(RequestDelegate next)
    {
        this.next = next;
    }

    public async Task Invoke(HttpContext context)
    {
        var tenantContext = context.GetTenantContext<AppTenant>();

        if (tenantContext == null)
        {
            // do whatever you want
        }
    }
}

I’ve updated the samples on GitHub.

© 2022 Ben Foster