ASP.NET Core Multi-tenancy: Tenant lifetime

In my previous post I covered the basics of building multi-tenant applications in ASP.NET Core with SaasKit.

SaasKit uses a tenant resolver to find tenants based on information available in the current request (hostname, user, header etc.). By default, if you implement ITenantResolver directly, SaasKit will attempt to resolve the tenant on every request.

There are a few reasons why you may not want to do this:

  1. You're loading tenant information from an external data source (e.g. a database) which could be an expensive operation.
  2. You need to maintain state for your tenants across requests, for example storing singleton-per-tenant scoped objects (I'll cover this in a later article).

SaasKit has built-in support for caching tenant context instances. Rather than implementing ITenantResolver you can instead derive from MemoryCacheTenantResolver<TTenant> which uses an in-memory cache to persist the current tenant context across requests. Here's our updated resolver:

public class CachingAppTenantResolver : MemoryCacheTenantResolver<AppTenant>
{
    private readonly IEnumerable<AppTenant> tenants;

    public CachingAppTenantResolver(IMemoryCache cache, ILoggerFactory loggerFactory, IOptions<MultitenancyOptions> options)
        : base(cache, loggerFactory)
    {
        this.tenants = options.Value.Tenants;
    }

    protected override string GetContextIdentifier(HttpContext context)
    {
        return context.Request.Host.Value.ToLower();
    }

    protected override IEnumerable<string> GetTenantIdentifiers(TenantContext<AppTenant> context)
    {
        return context.Tenant.Hostnames;
    }

    protected override 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)
        {
            tenantContext = new TenantContext<AppTenant>(tenant);
        }

        return Task.FromResult(tenantContext);
    }
}

The base class performs the cache lookup for you. It requires that you override the following methods:

  1. ResolveAsync - Resolve a tenant context from the current request. This will only be executed on cache misses.
  2. GetContextIdentifier - Determines what information in the current request should be used to do a cache lookup e.g. the hostname.
  3. GetTenantIdentifiers - Determines the identifiers (keys) used to cache the tenant context. In our example tenants can have multiple domains, so we return each of the hostnames as identifiers.

Overriding Tenant Lifetime

By default, tenant contexts are cached for an hour but you can control this by overriding CreateCacheEntryOptions:

protected override MemoryCacheEntryOptions CreateCacheEntryOptions()
{
    return new MemoryCacheEntryOptions()
        .SetAbsoluteExpiration(new TimeSpan(0, 30, 0)); // Cache for 30 minutes
}

Here we set an absolute expiration of 30 minutes.

Testing

If you want to test the cache is working as expected, set your minimum logging level to debug or if you're loading log settings from a JSON file set the following:

"Logging": {
  "IncludeScopes": false,
  "LogLevel": {
    "Default": "Information",
    "SaasKit": "Debug"
  }
},

Now when you run the application (dnx web) you should see the following in the console:

First Request

info: Microsoft.AspNet.Hosting.Internal.HostingEngine[1]
      Request starting HTTP/1.1 GET http://localhost:6000/
dbug: SaasKit.Multitenancy.Internal.TenantResolutionMiddleware<AspNetMvcSample.AppTenant>[0]
      Resolving TenantContext using CachingAppTenantResolver.
dbug: SaasKit.Multitenancy.MemoryCacheTenantResolver<AspNetMvcSample.AppTenant>[0]
      TenantContext not present in cache with key "localhost:6000". Attempting to resolve.
dbug: SaasKit.Multitenancy.MemoryCacheTenantResolver<AspNetMvcSample.AppTenant>[0]
      TenantContext resolved. Caching with keys "localhost:6000, localhost:6001".
verb: SaasKit.Multitenancy.Internal.TenantResolutionMiddleware<AspNetMvcSample.AppTenant>[0]
      TenantContext Resolved. Adding to HttpContext.

Subsequent Requests

dbug: SaasKit.Multitenancy.Internal.TenantResolutionMiddleware<AspNetMvcSample.AppTenant>[0]
      Resolving TenantContext using CachingAppTenantResolver.
dbug: SaasKit.Multitenancy.MemoryCacheTenantResolver<AspNetMvcSample.AppTenant>[0]
      TenantContext retrieved from cache with key "localhost:6000".
verb: SaasKit.Multitenancy.Internal.TenantResolutionMiddleware<AspNetMvcSample.AppTenant>[0]
      TenantContext Resolved. Adding to HttpContext.

Wrapping up

If you need to control the lifetime of your tenant context instances or resolving a tenant is an expensive operation, derive your tenant resolver from MemoryCacheTenantResolver.

Source for this example.

Questions?

Join the SaasKit chat room on Gitter.


More content like this?

If you don't have anything to contribute but are interested in where SaasKit is heading, please subscribe to the mailing list below. Emails won't be frequent, only when we have something to show you.


Ben Foster

About Me

I'm a software engineer and aspiring entrepreneur with 12+ years experience in the tech industry and have worked with startups and SMB’s in areas such as healthcare, recruitment and e-commerce (I even worked in enterprise, once). I founded my first startup Fabrik in 2011.

I now head up the engineering team at Checkout.com. If you're interested in working in an exciting fin-tech company, drop me a message on Twitter.

Creative Commons Licence