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!