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 Core]()
![multi-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]()