Business needs to grow in order to be successful and handle an increasing number of clients and partners, and if a company is not ready to respond to this load then there is a big chance that opportunities can be missed. This brings a topic of scalability into the game, as one of the main requirements that a company should address. As one of the possible ways to address this requirement is to build a multi-tenant solution. And as this topic gains more on importance, lots of options are available to achieve this, for example using Microsoft Elastic database (elastic tools). However in particular cases, like the case I faced on my project, not all of the product requirements could be satisfied with the already available options. This brought me to the idea of gathering my experience on this topic and presenting it below.
As we all are aware there are two main approached to tackle applications scaling — horizontal and vertical. Horizontal scaling will bring you the benefit of scaling on the fly and will imply dealing with multiple databases as each tenant has its own database/shard. Vertical approach to scaling presumes having one database that serves several tenants.
In my article I will address the approach of horizontal scaling with a step-by-step guide on how to build a multi-tenant web API application.
If you would like to refresh some aspects of multi-tenant architecture or what are pros and cons it
brings to the project, then I recommend visiting these resources:
- Why Cloud Architecture Matters: The Multi-Instance Advantage over Multi-Tenant
- Why Multi-Tenant Application Architecture Matters in 2017
- Design patterns for multi-tenant SaaS applications and Azure SQL Database
Architecture
Let’s briefly take a look at the architecture first. The example below is designed based on N-tire
architecture and has the following layers:
- Presentation layer or web api
- Service layer that will accommodate all the business logic
- Data access layer that is implemented using UnitOfWork and Repository patterns. As ORM in this example I used Entity Framework Core.
The key component of tenant separation is ContextFactory that contains logic to get the tenant id from HTTP header, retrieve a tenant database name using DataBaseManager and replace a database name in the connection string. As a result a database context (Entity Framework context) is created.
The diagram below demonstrate this architecture.
Implementation
As you can see architecture is not that complicated here, and skimming through it, I’d suggest to
focus on the steps to implement it.
1. Create ContextFactory
As I mentioned before ContextFactory is key component of whole architecture. It construct Entity Framework context (in current example DeviceApiContext) with specific to tenant database
/// <summary>
/// Entity Framework context service
/// (Switches the db context according to tenant id field)
/// </summary>
/// <seealso cref="IContextFactory" />
public class ContextFactory : IContextFactory
{
private const string TenantIdFieldName = "tenantid";
private const string DatabaseFieldKeyword = "Database";
private readonly HttpContext httpContext;
private readonly IOptions<ConnectionSettings> settings;
private readonly IDataBaseManager dataBaseManager;
public ContextFactory(
IHttpContextAccessor httpContentAccessor,
IOptions<ConnectionSettings> connectionSetting,
IDataBaseManager dataBaseManager)
{
this.httpContext = httpContentAccessor.HttpContext;
this.settings = connectionSetting;
this.dataBaseManager = dataBaseManager;
}
public IDbContext DbContext
{
get
{
var dbOptionsBuidler = this.ChangeDatabaseNameInConnectionString();
// Add new (changed) database name to db options
var bbContextOptionsBuilder = new DbContextOptionsBuilder<TeleServiceApiContext>();
bbContextOptionsBuilder.UseSqlServer(dbOptionsBuidler.ConnectionString);
return new DevicesApiContext(bbContextOptionsBuilder.Options);
}
}
// Gets tenant id from HTTP header
private string TenantId
{
get
{
if (this.httpContext == null)
{
throw new ArgumentNullException(nameof(this.httpContext));
}
string tenantId = this.httpContext.Request.Headers[TenantIdFieldName].ToString();
if (tenantId == null)
{
throw new ArgumentNullException(nameof(tenantId));
}
return tenantId;
}
}
private SqlConnectionStringBuilder ChangeDatabaseNameInConnectionString()
{
var sqlConnectionBuilder = new SqlConnectionStringBuilder(this.settings.Value.DefaultConnection);
string dataBaseName = this.dataBaseManager.GetDataBaseName(this.TenantId);
if (dataBaseName == null)
{
throw new ArgumentNullException(nameof(dataBaseName));
}
// Remove old DataBase name from connection string AND add new one
sqlConnectionBuilder.Remove(DatabaseFieldKeyword);
sqlConnectionBuilder.Add(DatabaseFieldKeyword, dataBaseName);
return sqlConnectionBuilder;
}
}
Source code of ContextFactory available Here
2. Add the data base manager
Database manager orchestrates all tenants metadata such as tenant database name, activation
status of tenants (activated/deactivated) and bunch of other properties. To demonstrate a base principle I used dictionary in the current solution. Later on dictionary should be replaced with more appropriate solutions, like SQL or NoSQL database that contains tenants metadata. This idea is similar to shard map manager that is used in Microsoft Elastic Tools. Also tenant metadata may includes fields to store database name, options to activate/deactivate tenants, even tenant styles for front-end application based on CSS/SASS/LESS file an on and on
/// <summary>
/// Contains all tenants database mappings and options
/// </summary>
public class DataBaseManager : IDataBaseManager
{
/// <summary>
/// IMPORTANT NOTICE: The solution uses simple dictionary for demo purposes.
/// The Best "Real-life" solutions would be creating 'RootDataBase' with
/// all Tenants Parameters/Options like: TenantName, DatabaseName, other configuration.
/// </summary>
private readonly Dictionary<Guid, string> tenantConfigurationDictionary = new Dictionary<Guid, string>
{
{
Guid.Parse("b0ed668d-7ef2-4a23-a333-94ad278f45d7"), "DeviceDb"
},
{
Guid.Parse("e7e73238-662f-4da2-b3a5-89f4abb87969"), "DeviceDb-ten2"
}
};
/// <summary>
/// Gets the name of the data base.
/// </summary>
/// <param name="tenantId">The tenant identifier.</param>
/// <returns>db name</returns>
public string GetDataBaseName(string tenantId)
{
var dataBaseName = this.tenantConfigurationDictionary[Guid.Parse(tenantId)];
if (dataBaseName == null)
{
throw new ArgumentNullException(nameof(dataBaseName));
}
return dataBaseName;
}
}
Source code of DataBaseManager
available Here
3. Add Unit of Work class (contains commit to specific context method)
UnitOfWork solves two tasks. It commits all changes made by Entity Framework in entities and dispose specific context.
/// <summary>
/// The Entity Framework implementation of UnitOfWork
/// </summary>
public sealed class UnitOfWork : IUnitOfWork
{
/// <summary>
/// The DbContext
/// </summary>
private IDbContext dbContext;
/// <summary>
/// Initializes a new instance of the <see cref="UnitOfWork"/> class.
/// </summary>
/// <param name="context">The object context</param>
public UnitOfWork(IDbContext context)
{
this.dbContext = context;
}
/// <summary>
/// Saves all pending changes
/// </summary>
/// <returns>The number of objects in an Added, Modified, or Deleted state</returns>
public int Commit()
{
// Save changes with the default options
return this.dbContext.SaveChanges();
}
/// <inheritdoc/>
public int Commit(IDbContext context)
{
// Change context
this.dbContext = context;
// Save changes with the default options
return this.dbContext.SaveChanges();
}
/// <summary>
/// Disposes the current object
/// </summary>
public void Dispose()
{
this.Dispose(true);
GC.SuppressFinalize(obj: this);
}
/// <summary>
/// Disposes all external resources.
/// </summary>
/// <param name="disposing">The dispose indicator.</param>
private void Dispose(bool disposing)
{
if (disposing)
{
if (this.dbContext != null)
{
this.dbContext.Dispose();
this.dbContext = null;
}
}
}
}
Source code of UnitOfWork available Here
4. Add Generic Repository class.
Repository will make changes in EF entities and Unit of Work will commit changes to tenants database. Be aware that EF making changes in memory, using Tracking Mechanism.
/// <summary>
/// Generic repository, contains CRUD operation of EF entity
/// </summary>
/// <typeparam name="T">Entity type</typeparam>
public class Repository<T> : IRepository<T>
where T : class
{
/// <summary>
/// Used to query and save instances of
/// </summary>
private readonly DbSet<T> dbSet;
/// <summary>
/// Gets the EF context.
/// </summary>
/// <value>
/// The context.
/// </value>
public IDbContext Context { get; }
/// <summary>
/// Initializes a new instance of the <see cref="Repository{T}" /> class.
/// </summary>
/// <param name="contextFactory">The context service.</param>
public Repository(IContextFactory contextFactory)
{
this.Context = contextFactory.DbContext;
this.dbSet = this.Context.Set<T>();
}
/// <inheritdoc />
public void Add(T entity)
{
return this.dbSet.Add(entity);
}
/// <inheritdoc />
public T Get<TKey>(TKey id)
{
return this.dbSet.Find(id);
}
/// <inheritdoc />
public async Task<T> GetAsync<TKey>(TKey id)
{
return await this.dbSet.FindAsync(id);
}
/// <inheritdoc />
public T Get(params object[] keyValues)
{
return this.dbSet.Find(keyValues);
}
/// <inheritdoc />
public IQueryable<T> FindBy(Expression<Func<T, bool>> predicate)
{
return this.dbSet.Where(predicate);
}
/// <inheritdoc />
public IQueryable<T> FindBy(Expression<Func<T, bool>> predicate, string include)
{
return this.FindBy(predicate).Include(include);
}
/// <inheritdoc />
public IQueryable<T> GetAll()
{
return this.dbSet;
}
/// <inheritdoc />
public IQueryable<T> GetAll(string include)
{
return this.dbSet.Include(include);
}
/// <inheritdoc />
public bool Exists(Expression<Func<T, bool>> predicate)
{
return this.dbSet.Any(predicate);
}
/// <inheritdoc />
public void Delete(T entity)
{
return this.dbSet.Remove(entity);
}
/// <inheritdoc />
public void Update(T entity)
{
return this.dbSet.Update(entity);
}
}
Source code of Repository available Here
5. Add tenant header operation filter.
TenantHeaderOperationFilter class will add tenant id field to all API calls (as HTTP header). In solutions which uses OIDS services e.g IdentityServer or auth0.com tenant can be injected to JWT token.
/// <summary>
/// Adds Tenant Id field to API endpoints
/// </summary>
/// <seealso cref="Swashbuckle.AspNetCore.SwaggerGen.IOperationFilter" />
public class TenantHeaderOperationFilter : IOperationFilter
{
/// <summary>
/// Applies the specified operation.
/// </summary>
/// <param name="operation">The operation.</param>
/// <param name="context">The context.</param>
public void Apply(Operation operation, OperationFilterContext context)
{
if (operation.Parameters == null)
{
operation.Parameters = new List<IParameter>();
}
operation.Parameters.Add(new NonBodyParameter
{
Name = "tenantid",
In = "header",
Description = "tenantid",
Required = true,
Type = "string",
});
}
}
This is how API will look like after filter applied.
Current example of service class (DeviceService.cs) contains functions to retrieving device by id and add new device for specific tenants. Source Code of service layer available Here
Conclusion
In this article I was explained how to build “ready to go” Multi tenant solution and given some suggestions how it can be used in your product/business. As I mentioned before this solution is ready to go so it can be used as “Boilerplate” project or partially.
Source code
The project’s source code available in Git repository here
Top comments (0)