Multitenant Blazor applications

In this post I’m going to show you how to enable multitenancy with data isolation for Blazor application in few steps. The end result will be an application that uses a separate database for every tenant. The current tenant will be determined from the application URL.

1. Create new Blazor application, add new MSSQL data-source connected to our Sample database and auto-generate pages.

2. Run the application from Radzen to generate everything needed and add the following file to application ignore list to tell Radzen to ignore it during subsequent code generation.

server\Program.cs

3. Add the following Multitenancy section in server\appsettings.json with information about tenants, their hostnames and connection strings. We specify two hostnames per tenant - one for debug mode (so you can test while developing the application) and one for production (which your users will browse).

server\appsettings.json

{
  ...
  "ConnectionStrings": {
    "SampleConnection": "Server=.;Initial Catalog=Sample-Tenant1;Persist Security Info=False;User ID=sa;Password=passw0rdMSSQL;MultipleActiveResultSets=False;Encrypt=false;TrustServerCertificate=true;Connection Timeout=30"
  },
  "Multitenancy": {
    "Tenants": [
      {
        "Name": "Tenant1",
        "Hostnames": [
          "localhost:5001",
          "tenant1.radzen-rocks.com"
        ],
        "ConnectionString": "Server=.;Initial Catalog=Sample-Tenant1;Persist Security Info=False;User ID=sa;Password=yourpassword;MultipleActiveResultSets=False;Encrypt=false;TrustServerCertificate=true;Connection Timeout=30"
      },
      {
        "Name": "Tenant2",
        "Hostnames": [
          "localhost:5002",
          "tenant2.radzen-rocks.com"
        ],
        "ConnectionString": "Server=.;Initial Catalog=Sample-Tenant2;Persist Security Info=False;User ID=sa;Password=yourpassword;MultipleActiveResultSets=False;Encrypt=false;TrustServerCertificate=true;Connection Timeout=30"
      }
    ]
  }
}

4. Add partial method Startup.OnConfigureServices to read multitenancy from server\appsettings.json and register it for dependency injection together with HttpContextAccessor.

using BlazorMultiTenant.Data;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using System.Collections.ObjectModel;

namespace BlazorMultiTenant
{
    public partial class Startup
    {
        partial void OnConfigureServices(IServiceCollection services)
        {
            services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
            services.AddSingleton(Configuration.GetSection("Multitenancy").Get<Multitenancy>());
        }

        partial void OnConfigure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            using (var scope = app.ApplicationServices.GetRequiredService<IServiceScopeFactory>().CreateScope())
            {
                using (var context = scope.ServiceProvider.GetService<SampleContext>())
                {
                    var created = context.Database.EnsureCreated();
                    if (created)
                    {
                        var databaseCreator = (RelationalDatabaseCreator)context.Database.GetService<IDatabaseCreator>();
                        databaseCreator.CreateTables();
                    }
                }

                using (var context = scope.ServiceProvider.GetService<ApplicationIdentityDbContext>())
                {
                    context.Database.Migrate();
                }
            }
        }
    }

    public class Tenant
    {
        public string Name { get; set; }
        public string[] Hostnames { get; set; }
        public string ConnectionString { get; set; }
    }

    public class Multitenancy
    {
        public Collection<Tenant> Tenants { get; set; }
    }
}

5. Enable default security for the application and auto-generate pages for user and role management.

6. Add server\Data\SampleContext.Custom.cs and server\Data\ApplicationIdentityDbContext.Custom.cs with the following code to read the tenants.

SampleContext.Custom.cs

public partial class SampleContext
{
    private readonly HttpContext context;
    private readonly Multitenancy multitenancy;

    public SampleContext(DbContextOptions<SampleContext> options, ApplicationIdentityDbContext identityDbContext, IHttpContextAccessor httpContextAccessor, Multitenancy mt) : base(options)
    {
        context = httpContextAccessor.HttpContext;
        multitenancy = mt;
    }

    public SampleContext(IHttpContextAccessor httpContextAccessor, Multitenancy mt, ApplicationIdentityDbContext identityDbContext)
    {
        context = httpContextAccessor.HttpContext;
        multitenancy = mt;
    }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        if (multitenancy != null && context != null)
        {
            var tenant = multitenancy.Tenants
                    .Where(t => t.Hostnames.Contains(context.Request.Host.Value)).FirstOrDefault();

            if (tenant != null)
            {
                optionsBuilder.UseSqlServer(tenant.ConnectionString);
            }
        }
    }
}

ApplicationIdentityDbContext.Custom.cs

public partial class ApplicationIdentityDbContext
{
    private readonly HttpContext context;
    private readonly Multitenancy multitenancy;

    public ApplicationIdentityDbContext(DbContextOptions<ApplicationIdentityDbContext> options, IHttpContextAccessor httpContextAccessor, Multitenancy mt) : base(options)
    {
        context = httpContextAccessor.HttpContext;
        multitenancy = mt;
    }

    public ApplicationIdentityDbContext(IHttpContextAccessor httpContextAccessor, Multitenancy mt)
    {
        context = httpContextAccessor.HttpContext;
        multitenancy = mt;
    }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        if (multitenancy != null && context != null)
        {
            var tenant = multitenancy.Tenants
                    .Where(t => t.Hostnames.Contains(context.Request.Host.Value)).FirstOrDefault();

            if (tenant != null)
            {
                optionsBuilder.UseSqlServer(tenant.ConnectionString);
            }
        }
    }
}

7. Add UseUrls() to server\Program.cs to specify the urls the web host will listen on for each tenant in debug mode. Modify the connection string in .env and appsettings.json and set default tenant to Tenant1.

public class Program
{
    public static void Main(string[] args)
    {
        CreateHostBuilder(args).Build().Run();
    }

    public static IHostBuilder CreateHostBuilder(string[] args) =>
        Host.CreateDefaultBuilder(args)
            .ConfigureWebHostDefaults(webBuilder =>
            {
                webBuilder.UseKestrel();
                webBuilder.UseUrls("https://localhost:5001", "https://localhost:5002");
                webBuilder.UseStartup<Startup>();
            });
}

.env

ConnectionStrings__SampleConnection="Server=.;Initial Catalog=Sample-Tenant1;Persist Security Info=False;User ID=sa;Password=passw0rdMSSQL;MultipleActiveResultSets=False;Encrypt=false;TrustServerCertificate=true;Connection Timeout=30"

8. Run the application, go to http://localhost:5001 and login using admin/admin to access Tenant1 database. You can now create roles and users for Tenant1.

9. Modify the connection string in .env and appsettings.json and set default tenant to Tenant2.

.env

ConnectionStrings__SampleConnection="Server=.;Initial Catalog=Sample-Tenant2;Persist Security Info=False;User ID=sa;Password=passw0rdMSSQL;MultipleActiveResultSets=False;Encrypt=false;TrustServerCertificate=true;Connection Timeout=30"

server\appsettings.json

{
  ...
  "ConnectionStrings": {
    "SampleConnection": "Server=.;Initial Catalog=Sample-Tenant2;Persist Security Info=False;User ID=sa;Password=passw0rdMSSQL;MultipleActiveResultSets=False;Encrypt=false;TrustServerCertificate=true;Connection Timeout=30"
  },
  "Multitenancy": {
    "Tenants": [
      {
        "Name": "Tenant1",
        "Hostnames": [
          "localhost:5001",
          "tenant1.radzen-rocks.com"
        ],
        "ConnectionString": "Server=.;Initial Catalog=Sample-Tenant1;Persist Security Info=False;User ID=sa;Password=yourpassword;MultipleActiveResultSets=False;Encrypt=false;TrustServerCertificate=true;Connection Timeout=30"
      },
      {
        "Name": "Tenant2",
        "Hostnames": [
          "localhost:5002",
          "tenant2.radzen-rocks.com"
        ],
        "ConnectionString": "Server=.;Initial Catalog=Sample-Tenant2;Persist Security Info=False;User ID=sa;Password=yourpassword;MultipleActiveResultSets=False;Encrypt=false;TrustServerCertificate=true;Connection Timeout=30"
      }
    ]
  }
}

10. Run the application, go to http://localhost:5002 and login using admin/admin to access Tenant2 database and add roles and users for Tenant2.

You have now separate security for each tenant!

Both ApplicationIdentityDbContext and SampleDataContext will retrieve runtime the tenant connection string depending on the application host:

Enjoy!

Source Code

Leverage Radzen on LinkedIn

Yes, we are on LinkedIn and you should follow us!

Now, you have the opportunity to showcase your expertise by adding Radzen Blazor Studio and Radzen Blazor Components as skills to your LinkedIn profile. Present your skills to employers, collaborators, and clients.

All you have to do is go to our Products page on LinkedIn and click the + Add as skill button.

by Vladimir Enchev

Blazor DataGrid InLine editing, column totals and more

We’ve been busy over the past few months, adding more and more new components and features to our FREE Blazor component suite. For the official release of Microsoft .NET Core 3.0 in September at .NET Conf 2019, 8 new components (AutoComplete, CheckBoxList, ...
Read more