Azure Ad Multi Tenancy + Multi Db scenario - How to have a Distributed Token Cache per tenant

164 Views Asked by At

Introduction

I'm working on migrating a single-tenant, single-db ASP.NET Core Web API project that uses Azure AD with a DistributedSqlServerCache to support a multi-tenant, multi-db approach.

Our application will support Azure AD logins from multiple tenants, each tenant having their own database for better isolation.

This is documented well for Entity Framework Core, with code samples as well, but there isn't any clear information about having a multi-tenant, multi-db approach for the token cache. The official Multi tenant Azure AD documentation does mention the token caches, but it doesn't talk about a multi-db approach.

The problem

When you set up a token cache, you have to specify a connectionstring:

services.AddDistributedSqlServerCache(options =>
{
    options.ConnectionString = connectionString;
    options.TableName = "DistributedCache";
    options.SchemaName = "cache";
});

This code will be executed once, so I can't change the connectionstring to the tenant of the user who is executing the current request.

Does anyone know of a way to point the application to a different cache, depending on the current request/current user? As long as I would have access to the current IServiceProvider, I could grab the current HttpContext and find the Tenant and base my connectionstring on that, for example.

1

There are 1 best solutions below

6
Stuart Dobson On

Given the comments about the core code creating a singleton of SqlServerCache, I've taken a new approach where we register it ourselves.

Now for every request we are setting up the cache. I have tested and this works but I'm not sure about performance implications of registering the service and building the ServiceProvider on every request. It's up to you how to implement the connection string decision, but this will enable you to make it.

First, create a delegate of type IDistributedCache:

public delegate IDistributedCache CacheResolver(string key);

Then in Startup add this delegate as a Transient service and set up the action to set the options and add the SqlServerCache as Transient:

services.AddTransient<CacheResolver>(_ => connectionString =>
{
    Action<SqlServerCacheOptions> cacheConfigOptions = options =>
    {
        options.ConnectionString = connectionString;
        options.SchemaName = "cache";
        options.TableName = "DistributedCache";
    };
    services.Configure(cacheConfigOptions);

    services.AddTransient<SqlServerCache>();      

    return services.BuildServiceProvider().GetService<SqlServerCache>();
});

Create a Cache provider class with an IDistributedCache which is resolved by our CacheResolver:

public class CustomCacheProvider : ICustomCacheProvider
{
    private readonly IDistributedCache _cache;
    public CustomCacheProvider(CacheResolver resolver)
    {
            _cache = resolver("<connection string from somewhere, depending on your logic, maybe from another injected service>")
    }

    // Caching logic, get, set etc, using _cache
}

Register the provider:

services.AddTransient<ICustomCacheProvider, CustomCacheProvider>();

Then simply inject ICustomCacheProvider where you need it.

Note that the discard in the callback is the serviceProvider parameter. Don't use this to get the SqlServerCache if you happened to register this outside the callback, for some reason it doesn't pick up the cacheConfigOptions set up in the callback.