For multitenant web applications with security please visit this blog post.
In this post I’m going to show you how to enable multitenancy with data isolation for every Radzen Angular/ASP.NET Core 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 application with .NET server-side project, 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 files to application ignore list to tell Radzen to ignore them during subsequent code generation.
client\src\environments\environment.ts
server\Program.cs
server\Data\SampleContext.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).
{
...
"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.
namespace MultiTenantSample
{
public partial class Startup
{
partial void OnConfigureServices(IServiceCollection services)
{
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
services.AddSingleton(Configuration.GetSection("Multitenancy").Get<Multitenancy>());
}
}
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. Add Multitenancy
and IHttpContextAccessor
to SampleContext
constructors and override SampleContext.OnConfiguring
method to read current tenant connection string from request. Call also EnsureCreated()
to make sure that EF will create the database from your model for each tenant (data isolation).
public partial class SampleContext : Microsoft.EntityFrameworkCore.DbContext
{
private readonly HttpContext context;
private readonly Multitenancy multitenancy;
public SampleContext(IHttpContextAccessor httpContextAccessor, Multitenancy mt)
{
context = httpContextAccessor.HttpContext;
multitenancy = mt;
Database.EnsureCreated();
}
public SampleContext(DbContextOptions<SampleContext> options, IHttpContextAccessor httpContextAccessor, Multitenancy mt) : base(options)
{
context = httpContextAccessor.HttpContext;
multitenancy = mt;
Database.EnsureCreated();
}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
var tenant = multitenancy.Tenants
.Where(t => t.Hostnames.Contains(context.Request.Host.Value)).FirstOrDefault();
if (tenant != null)
{
optionsBuilder.UseSqlServer(tenant.ConnectionString);
}
}
...
6. Add UseUrls()
to server\Program.cs
to specify the urls the web host will listen on for each tenant in debug mode.
public class Program
{
public static void Main(string[] args)
{
var host = BuildWebHost(args);
host.Run();
}
public static IWebHost BuildWebHost(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.UseKestrel()
.UseUrls("http://localhost:5001", "http://localhost:5002")
.UseStartup<Startup>()
.Build();
}
7. Specify Angular application data service url for each tenant for debug (client\src\environments\environment.ts
). In debug mode we will use query strings (http://localhost:8000/orders?tenant=tenant2
) and in production subdomains (http://tenant2.radzen-rocks.com/orders
). For example purposes we will use the hypothetical domain name radzen-rocks.com
.
client\src\environments\environment.ts - for debug
export function dataSourceByTenant(): string {
const tenant = new URLSearchParams(window.location.search).get('tenant');
if (!tenant || tenant == 'tenant1') {
return 'http://localhost:5001/odata/Sample';
} else if (tenant == 'tenant2') {
return 'http://localhost:5002/odata/Sample';
}
}
export const environment = {
sample: dataSourceByTenant(),
production: false,
};
8. Run the application from Radzen and change tenant from query string (tenant1
is default).
9. Create new radzen-rocks.com
web site in your IIS with following bindings.
10. Modify your hosts (%WINDIR%\system32\drivers\etc\hosts
) file to simulate hypothetical domains locally.
...
127.0.0.1 radzen-rocks.com
127.0.0.1 tenant1.radzen-rocks.com
127.0.0.1 tenant2.radzen-rocks.com
...
11. Deploy the application from Radzen (the connection string should be the default tenant connection string) and browse tenants.
Enjoy!