Creating multitenant web applications with ASP.NET Core and Angular

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.

Source Code

Enjoy!

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

CRUD Blazor applications with Radzen

We are excited to share with you that experimental Blazor support has landed in Radzen v2.8.0! For now we support only CRUD page generation but other features will follow.
Read more