Implementing tenant providers on ASP.NET Core

In my previous post about global query filters in Entity Framework 2.0 I proposed an idea how to automatically apply query filters to all domain entities when model is built so enitites are always coming from the same tenant. This post digs deeper to possible solutions for detecting current tenant in ASP.NET Core we applications and proposes some tenant providers to use as starting point for multi-tenancy support in real-life applications.

NB! Please read my previous post Global query filters in Entity Framework Core 2.0 as this post is continuation for it and expects that reader is familiar with solution I provided for multi-tenancy. Also the mehtods for applying multi-tenancy rules to all domain entities are taken from my previous global query filters post and not duplicated here.

How to detect current tenant?

Here’s the situation. Data context is built when request comes in and during building a model global query filters are applied. One of these filters is about current tenant. Tenant ID is needed in code the model is not ready yet. Same time tenant ID may be available only in database. What to do?

Some ideas:

  • Use database connection in data context and make direct query to tenants table
  • Use separate data context for tenant information and operations
  • Keep tenants information available on blob storage
  • Use hash of domain name as tenant ID

NB! I expect in this post that tenants are detected by host header in web application.

Tenants table I’m using in this post is given on the following image.

ef-core-tenants-table

NB! Depending on solution tenant ID can be also something else and not int like shown on image above.

Using database connection of data context

This is perhaps the most lightweight solution to problem as there is no need to add additional classes and there’s also no need for tenant provider anymore. It is easy to get current host header using IHttpContextAccessor.


public class PlaylistContext : DbContext
{
    private int _tenantId;
    private string _tenantHost;

    public DbSet<Playlist> Playlists { get; set; }
    public DbSet<Song> Songs { get; set; }

    public PlaylistContext(DbContextOptions<PlaylistContext> options,
                           IHttpContextAccessor accessor)
        : base(options)
    {
        _tenantHost = accessor.HttpContext.Request.Host.Value;
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        var connection = Database.GetDbConnection();
        using (var command = connection.CreateCommand())
        {
            connection.Open();

            command.CommandText = "select ID from Tenants where Host=@Host";
            command.CommandType = CommandType.Text;

            var param = command.CreateParameter();
            param.ParameterName = "@Host";
            param.Value = _tenantHost;

            command.Parameters.Add(param);
            _tenantId = (int)command.ExecuteScalar();
            connection.Close();
        }

        foreach (var type in GetEntityTypes())
        {
            var method = SetGlobalQueryMethod.MakeGenericMethod(type);
            method.Invoke(this, new object[] { modelBuilder });
        }

        base.OnModelCreating(modelBuilder);
    }

    // Other methods follow
}

The code above creates command based on database connection held by data context and runs SQL-command against database to get tenant ID by host header.

This solution is smallest when considering code but it pollutes data context with internal details of host name detection.

Using separate data context for tenants

Second approach is to use separate web application specific tenants context. It’s possible to write tenant provider (refer back to my EF Core global query filters post) and inject it to main data context.

Let’s start with tenant class based on table given in the beginning of this post.


public class Tenant
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Host { get; set; }
}

Now let’s build tenants data context. This context has no dependencies to other dependencies to custom interfaces and classes. It only uses Tenant model. Notice that Tenants set is private and other classes can only query tenant ID by host header.


public class TenantsContext : DbContext
{
    private DbSet<Tenant> Tenants { get; set; }

    public TenantsContext(DbContextOptions<TenantsContext> options)
        : base(options)
    {
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Tenant>().HasKey(e => e.Id);
    }

    public int GetTenantId(string host)
    {
        var tenant = Tenants.FirstOrDefault(t => t.Host == host);
        if(tenant == null)
        {
            return 0;
        }

        return tenant.Id;
    }
}

Now it’s time to bring back in ITenantProvider and write implementation that uses tenants data context. This provider holds all the logic about detecting host header and getting tenant ID. In practice it will be more complex but here I go with simple version of it.


public class WebTenantProvider : ITenantProvider
{
    private int _tenantId;

    public WebTenantProvider(IHttpContextAccessor accessor,
                                TenantsContext context)
    {
        var host = accessor.HttpContext.Request.Host.Value;

        _tenantId = context.GetTenantId(host);
    }

    public int GetTenantId()
    {
        return _tenantId;
    }
}

Now there’s everything needed to detect tenant and find it’s ID. It’s time to rewrite main data context so it uses new tenant provider.


public class PlaylistContext : DbContext
{
    private int _tenantId;

    public DbSet<Playlist> Playlists { get; set; }
    public DbSet<Song> Songs { get; set; }

    public PlaylistContext(DbContextOptions<PlaylistContext> options,
                           ITenantProvider tenantProvider)
        : base(options)
    {
        _tenantId = tenantProvider.GetTenantId();
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        foreach (var type in GetEntityTypes())
        {
            var method = SetGlobalQueryMethod.MakeGenericMethod(type);
            method.Invoke(this, new object[] { modelBuilder });
        }

        base.OnModelCreating(modelBuilder);
    }

    // Other methods follow
}

In startup class of web application all dependencies must be defined for framework-level dependency injection. It is done in ConfigureServices() method.


public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc();

    var connection = Configuration["ConnectionString"];
    services.AddEntityFrameworkSqlServer();
    services.AddDbContext<PlaylistContext>(options => options.UseSqlServer(connection));
    services.AddDbContext<TenantsContext>(options => options.UseSqlServer(connection));
    services.AddScoped<ITenantProvider, WebTenantProvider>();
    services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
}

This solution is more elegant as it moves tenant related functionalities out from main data context. ITenantProvider is the only thing that main data context must know and now it is usable also in other projects that are not necessarily web application ones.

Keeping tenants information on blob storage

One my now say that tenants are not coming and going all the time and instead of tenant provider that queries database it’s possible to cache tenants information somewhere and update it when needed. Considering cloud scenarios it’s good idea to keep tenants information where it is accessible for multiple instances of web application. My choice is blob storage.

Let’s start with simple tenants file in JSON-format and let’s expect it’s the responsibility of some internal application or background task to keep this file up to date. This the sample file I’m using.


[
  {
    "Id": 2,
    "Name": "Local host",
    "Host": "localhost:30172"
  },
  {
    "Id": 3,
    "Name": "Customer X",
    "Host": "localhost:3331"
  },
  {
    "Id": 4,
    "Name": "Customer Y",
    "Host": "localhost:33111"
  }
]

To read files from blob storage application needs to know storage account connection string, container name and blob name. Blob is tenants file. I use ITenantProvider interface again and create a new implementation of it for Azure blob storage. I call it BlobStorageTenantProvider. It is simple and doesn’t take care of many real-life aspects like refreshing of tenants information and taking care of locks.


public class BlobStorageTenantProvider : ITenantProvider
{
    private static IList<Tenant> _tenants;

    private int _tenantId = 0;

    public BlobStorageTenantProvider(IHttpContextAccessor accessor, IConfiguration conf)
    {
        if(_tenants == null)
        {
            LoadTenants(conf["StorageConnectionString"], conf["TenantsContainerName"], conf["TenantsBlobName"]);
        }

        var host = accessor.HttpContext.Request.Host.Value;
        var tenant = _tenants.FirstOrDefault(t => t.Host.ToLower() == host.ToLower());
        if(tenant != null)
        {
            _tenantId = tenant.Id;
        }
    }

    private void LoadTenants(string connStr, string containerName, string blobName)
    {
        var storageAccount = CloudStorageAccount.Parse(connStr);
        var blobClient = storageAccount.CreateCloudBlobClient();
        var container = blobClient.GetContainerReference(containerName);
        var blob = container.GetBlobReference(blobName);

        blob.FetchAttributesAsync().GetAwaiter().GetResult();

        var fileBytes = new byte[blob.Properties.Length];

        using (var stream = blob.OpenReadAsync().GetAwaiter().GetResult())
        using (var textReader = new StreamReader(stream))
        using (var reader = new JsonTextReader(textReader))
        {
            _tenants = JsonSerializer.Create().Deserialize<List<Tenant>>(reader);
        }
    }

    public int GetTenantId()
    {
        return _tenantId;
    }
}

The provider’s code is perhaps not very nice but it performs better than previous ones as there’s no need for additional database call and tenant ID-s are served from memory.

Using host header hash as tenant ID

Third method is simplest one but it means that tenant ID is same as host header or derived from it. I don’t like this idea because if customer wants to change host header then changes are spread all over the database. It’s possible that customer wants to start with custom host name given automatically by service and later use their own subdomain.

Code for tenant ID as host name is here.


public class PlaylistContext : DbContext
{
    private string _tenantId;

    public DbSet<Playlist> Playlists { get; set; }
    public DbSet<Song> Songs { get; set; }

    public PlaylistContext(DbContextOptions<PlaylistContext> options,
                            IHttpContextAccessor accessor)
        : base(options)
    {
        _tenantId = accessor.HttpContext.Request.Host.Value;
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        foreach (var type in GetEntityTypes())
        {
            var method = SetGlobalQueryMethod.MakeGenericMethod(type);
            method.Invoke(this, new object[] { modelBuilder });
        }

        base.OnModelCreating(modelBuilder);
    }

    // Other methods follow
}

It’s possible to use MD5 has instead of host name but it doesn’t change the problem of host as key anyway.

Wrapping up

This post was about making real use of global query filters in Entity Framework Core 2.0. Although code artifacts represented here were simple and not up to needs for live environments they still work as an good examples and starting points when building real solutions. I tried to keep with solutions as close to good architecture principles as possible. I think solutions provided here with my opinions about hteir pros and cons will help readers start with their own multi-tenant applications.



See also

4 thoughts on “Implementing tenant providers on ASP.NET Core

Leave a Reply

Your email address will not be published. Required fields are marked *