How to build a multi-tenant applications with ASP.NET Core

Tenant application in ASP.NET Core 

First of all, we have to know what a tenant is. Simply tenant is a customer. Meaning each customer is called a tenant.

Multi-tenant application in ASP.NET Core

Multi-tenancy is an architecture in which a single software application instance serves multiple customers.

In this case, a single software will manage multiple customer databases. But you have needed a tenant database for multiple tenants. This process is also called SaaS (Software as a Service).

multi-tenant applications with ASP.NET Core

Moreover, you can also manage the software design style by multi-tenant architecture. In the case of a single tenant, every software-service instance has a separate database. Otherwise, in the case of multi-tenant only one software service instance, but there are multiple databases.

Tenant Database

I have to maintain a Tenant database to manage multiple customer Databases. I am using two tables, Like

CREATE DATABASE [TenantDB]

multi-tenant applications with ASP.NET Core

USE [TenantDB]
GO
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TABLE [dbo].[Tenants](
	[CustomerId] [int] NOT NULL,
	[Customer] [varchar](50) NOT NULL,
	[Host] [varchar](50) NULL,
	[SubDomain] [varchar](50) NOT NULL,
	[Logo] [varchar](50) NULL,
	[ThemeColor] [varchar](50) NULL,
	[ConnectionString] [varchar](max) NOT NULL,
 CONSTRAINT [PK_Customer] PRIMARY KEY CLUSTERED 
(
	[CustomerId] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY]
) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]
GO

multi-tenant applications with ASP.NET Core

USE [TenantDB]
GO
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TABLE [dbo].[TenantUsers](
	[Id] [int] NOT NULL,
	[CustomerId] [int] NOT NULL,
	[Email] [varchar](50) NOT NULL,
 CONSTRAINT [PK_User] PRIMARY KEY CLUSTERED 
(
	[Id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY]
) ON [PRIMARY]
GO
USE [TenantDB]
GO
INSERT [dbo].[Tenants] ([CustomerId], [Customer], [Host], [SubDomain], [Logo], [ThemeColor], [ConnectionString]) VALUES (1, N'Red Customer', N'localhost:5057', N'rc', NULL, N'Red', N'Server=Rohol;Database=App-DB1; user id=sa; password=123456; MultipleActiveResultSets=true')
GO
INSERT [dbo].[Tenants] ([CustomerId], [Customer], [Host], [SubDomain], [Logo], [ThemeColor], [ConnectionString]) VALUES (2, N'Green Customer', N'localhost:5057', N'gc', NULL, N'Green', N'Server=Rohol;Database=App-DB2; user id=sa; password=123456; MultipleActiveResultSets=true')
GO
INSERT [dbo].[TenantUsers] ([Id], [CustomerId], [Email]) VALUES (1, 1, N'[email protected]')
GO
INSERT [dbo].[TenantUsers] ([Id], [CustomerId], [Email]) VALUES (2, 2, N'[email protected]')
GO

Application Database

Now I will add two application databases; these databases will access by a single web portal. Databases are like App-DB1 and App-DB2. Each database has one table, like Users.

multi-tenant applications with ASP.NET Core

CREATE Database [App-DB1] 
GO
USE [App-DB1]
GO
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TABLE [dbo].[Users](
	[UserId] [int] NOT NULL,
	[UserName] [varchar](50) NULL,
	[UserEmail] [varchar](50) NULL,
	[Password] [varchar](50) NULL,
 CONSTRAINT [PK_User] PRIMARY KEY CLUSTERED 
(
	[UserId] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY]
) ON [PRIMARY]
GO

USE [App-DB1]
GO
INSERT [dbo].[Users] ([UserId], [UserName], [UserEmail], [Password]) VALUES (1, N'Red Customer', N'[email protected]', N'123456')
GO
CREATE Database [App-DB2] 
GO
USE [App-DB2]
GO
/****** Object:  Table [dbo].[Users]    Script Date: 04/15/23 5:26:26 PM ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TABLE [dbo].[Users](
	[UserId] [int] NOT NULL,
	[UserName] [varchar](50) NULL,
	[UserEmail] [varchar](50) NULL,
	[Password] [varchar](50) NULL,
 CONSTRAINT [PK_User] PRIMARY KEY CLUSTERED 
(
	[UserId] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY]
) ON [PRIMARY]
GO

USE [App-DB2]
GO
INSERT [dbo].[Users] ([UserId], [UserName], [UserEmail], [Password]) VALUES (1, N'Green Customer', N'[email protected]', N'123456')
GO

Create a Multi-tenant Application

Open visual studio editor 2022 and choose an ASP.Net Core Web App (MVC).

multi-tenant applications with ASP.NET Core

multi-tenant applications with ASP.NET Core

multi-tenant applications with ASP.NET Core

Install SaasKit.Multitenancy

Go to the NuGet Package Manager and install the SaasKit.Multitenancypackage

multi-tenant applications with ASP.NET Coremulti-tenant applications with ASP.NET Core

This SaasKit.Multitenancy package will manage a multi-tenancy strategy.

Now add two classes, Tenants and TenantUsers.

public class Tenants
    {
        [Key]
        public int CustomerId { get; set; }
        public string Customer { get; set; }
        public string Host { get; set; }
        public string SubDomain { get; set; }
        public string Logo { get; set; }
        public string ThemeColor { get; set; }
        public string ConnectionString { get; set; }
    }
 public class TenantUsers
    {
        [Key]
        public int Id { get; set; }
        public int CustomerId { get; set; }
        public string Email { get; set; }
    }

Now add another class like TenantResolver. Here I will use TenantContext, which is come from SaasKit.Multitenancy package.

public interface ITenantResolver
    {
        Task<TenantContext<Tenants>> ResolveAsync(HttpContext context);
    }
public class TenantResolver : ITenantResolver<Tenants>
    {
      public async Task<TenantContext<Tenants>> ResolveAsync(HttpContext context)
        {
         throw new NotImplementedException();
        }
    }

Service Registration

Go to the program file and register the Tenant class with TenantResolver class

// Multitenancy
builder.Services.AddMultitenancy<Tenants, TenantResolver>();

Middleware Setup

app.UseMuttitenancy<Tenant>();

This Tenant middleware will call with every HTTP request.

Install Entity Framework Core

I will useEFCore ORM to access SQL Database. So, we need to install the required packages. Like

multi-tenant applications with ASP.NET Core

After installing these three packages, I will add Two database contexts. One context is for Tenant-Database and another for App-Database. Like

multi-tenant applications with ASP.NET Core

TenantDBConnection

{
  "ConnectionStrings": {
    "TenantConnection": "Server=Rohol;Database=TenantDB; user id=sa; password=123456; MultipleActiveResultSets=true"
  },
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*"
}
// Sql Server TenantDb Connection
builder.Services.AddDbContextPool<TenantDbContext>(options => options.
        UseSqlServer(builder.Configuration.GetConnectionString("TenantConnection")));

Now add Signin and Users class in the Models folder. Like

public class Signin
    {
        [Required(ErrorMessage ="email address is required")]
        [EmailAddress]
        public string Email { get; set; }
        [DataType(DataType.Password)]
        public string? Password { get; set; }
    }
public class Users
    {
        [System.ComponentModel.DataAnnotations.Key]
        public int UserId { get; set; }
        [Required]
        public string UserName { get; set; }
        [Required]
        public string UserEmail { get; set; }
        [Required]
        public string Password { get; set; }

    }

DB context Implementation

Now time to implement Db contexts. Like

public class TenantDbContext : DbContext
    {
        public TenantDbContext(DbContextOptions<TenantDbContext> option) : base(option)
        {
        }

        public DbSet<Tenants> Tenants { get; set; }
        public DbSet<TenantUsers> TenantUsers { get; set; }

    }
public class AppDbContext : DbContext
    {
        private readonly Tenants tenant;

        public AppDbContext(DbContextOptions<AppDbContext> options, Tenants tenant):base(options)
        {
            this.tenant = tenant;
        }

        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            optionsBuilder.UseSqlServer(tenant.ConnectionString);
        }

        public DbSet<Users> Users { get; set; }

    }

Add Services

I have used two services for this application. One for tenant operation and another for application operation. Like

multi-tenant applications with ASP.NET Core

namespace MultiTenantApp.Services
{
    public interface ITenantService
    {
       Tenants GetTenantBySubDomain(string subDomain);
       Tenants GetTenantByEmail(string email);
    }

    public class TenantService : ITenantService
    {
        private readonly TenantDbContext tdbc;

        public TenantService(TenantDbContext tdbc)
        {
            this.tdbc = tdbc;
        }

        public Tenants GetTenantByEmail(string email)
        {
         throw new NotImplementedException();
        }

        public Tenants GetTenantBySubDomain(string subdomain)
        {
         throw new NotImplementedException();
        }
    }
}

 

  • GetTenantBySubDomain()- This method will return a tenant by sub-domain.
  • GetTenantByEmail ()- This method will return a tenant by email.
namespace MultiTenantApp.Services
{
    public interface IAppUserService
    {
        public string GetTenantByEmail(string email);
        public string Signin(Signin model);
    }
    public class AppUserService : IAppUserService
    {
        public string GetTenantByEmail(string email)
        {
            throw new NotImplementedException();
        }

        public string Signin(Signin model)
        {
            throw new NotImplementedException();
        }
    }
}
  • GetTenantByEmail()- Here, this method will return a valid URL with a sub-domain.
  • Signin()- This method is used to sign in to this portal. It will return a URL as a string.

TenantResolver implementation

public async Task<TenantContext<Tenants>> ResolveAsync(HttpContext context)
        {   // get sub-domain form browser current url. if sub-domain is not exists then will set empty string
            string subDomainFromUrl = context.Request.Host.Value.ToLower().Split(".")[0] ?? string.Empty;
            // checking has any tenant by current sub-domain. 
            var result = this.tenantService.GetTenantBySubDomain(subDomainFromUrl);
            Tenants tenant = new();
            // checking has any subdomain is exists in current url
            if (!string.IsNullOrEmpty(result.SubDomain))
            {
                // checking orginal sub-domain and current url sub-domain
                if (!result.SubDomain.Equals(subDomainFromUrl)) return null; // if sub-domain is different then return null
                else
                {
                    tenant.CustomerId = result.CustomerId;
                    tenant.Customer = result.Customer;
                    tenant.Host = result.Host;
                    tenant.SubDomain = result.SubDomain;
                    tenant.Logo = result.Logo;
                    tenant.ThemeColor = result.ThemeColor;
                    tenant.ConnectionString = result.ConnectionString;
                    return await Task.FromResult(new TenantContext<Tenants>(tenant));
                }
            }
            else return await Task.FromResult(new TenantContext<Tenants>(tenant));

        }

This resolver will resolve a multitenant strategy in each HTTP request. If the tenant is valid, then the HTTP request will execute else. The application will show an error. Here I am checking the sub-domain in each request. So, if the sub-domain exists and is valid, the app will work fine; otherwise shows an error. This is a demo and my logic and implementation so anyone can implement his logic.

Controller Implementation

Now add a Signin action in HomeController for view. Like,

public IActionResult Signin(string emailid="")
        {
            ViewBag.Email = emailid;
            return View();
        }

Also, add Signin Post Action for Signin. Like,

[HttpPost]
        public IActionResult Signin(Signin model)
        {
            // checking model state
            if (ModelState.IsValid)
            {
                // checking email at first time
                if (model.Password is null)
                {
                    // retrieve tenant information by user email
                    var result = this.appUserService.GetTenantByEmail(model.Email);
                    // if valid email then redirect for password
                    if (result is not null) return Redirect(result + "?emailid=" + model.Email);
                    else // if email is invalid then clear Email-ViewBag to stay same page and get again email
                    {
                        ViewBag.Email = string.Empty;
                        ViewBag.Error = "Provide valid email";
                    }
                }
                else // this block for password verification, when user provide password to signin
                {
                    var result = this.appUserService.Signin(model);
                    if (result is null) // if password is wrong then again provide valid password
                    {
                        ViewBag.Email = model.Email;
                        ViewBag.Error = "Provide valid password";
                    }
                    else return Redirect(result); // if password is valid then portal will open for user access
                }
            }
            else ViewBag.Email = ""; // if email is invalid then clear Email-ViewBag to stay same page and get again email
            return View();
        }

The detail I have implemented the into the source code. So don't worry; I have provided the source file.

Logout action

public IActionResult Logout()
        {
            return Redirect("http://localhost:5057");
        }

Go to the master _Layout.cshtml page and inject the Tenant class to access the required property. I will use the ThemeColor property to change the theme according to the user's colour. Like,

@inject Tenants tenants;

<body style="background-color:@tenants.ThemeColor">

Same as the index and privacy files. I will use the Customer name from the tenant class. Like

@inject Tenants tenants;
@{
    ViewData["Title"] = "Home Page";
}

<h1>@ViewData["Title"]</h1>

<h4>of @tenants.Customer</h4>

Run the project and sign in by different users. Like

First, for the red user

multi-tenant applications with ASP.NET Core

 

and second for green user

multi-tenant applications with ASP.NET Core