Nesse artigo, vamos ver o que é uma Arquitetura MultiTenant e como aplicar no .Net Core, lembrando que a arquitetura MultiTenant não é exclusividade do .Net.
Multi-Tenant ou também chamado de Multitenancy, significa dizer que uma única aplicação Web pode responder a diversos clientes de uma forma que aparenta que eles estão falando com diferentes aplicações, ou seja, diversos clientes acessando o mesmo host, porém, isso é transparente para o usuário que está usando o sistema.
Os diferentes interfaces dos aplicativo são chamados de Tenants (inquilinos), porque conceitualmente eles vivem no mesmo espaço, no caso a aplicação WEB, e podem ser endereçados individualmente. Isso significa dizer que todas as minhas aplicações estarão hospedadas em um único e mesmo servidor, mas cada um terá seu endereço; no caso, sua url.
Ou seja: TENANT > CLIENTES.
Fazendo uma analogia, por exemplo uma cidade em que temos as casas; cada casa tem seu endereço e não temos acesso aos pertences da casa do vizinho, porém, compartilhamos recursos da cidade como ruas, parques e calçadas.

A ideia principal de uma aplicação MultiTenant é garantir o isolamento de implementação, de dados e de customização.
- Isolamento dos dados
- Ser flexível
- Solução escalável
Vantagens do MultiTenant no SaaS
- Reduz os custos de investimento a longo prazo
- Atualizações simples
- Fácil customização
- Maximização do uso de recursos
- Infinitos Clientes
- Dependendo da implementação da arquitetura, uma manutenção serve para todos os clientes
- Escalabilidade
Desvantagens do MultiTenant no SaaS
- Mais complexo
- Menos flexível em alguns casos
- Se o servidor cair, todos os clientes cairão
Como começar?
Instalar o SaasKit.Multitenancy via nuget:
- Install-Package SaasKit.Multitenancy -Version 1.1.4 (ou outra versão; avaliar no link a última versão do pacote).
Criar um arquivo AppTenant.cs.
namespace MultiTenant
{
public class AppTenant
{
public string Name { get; set; }
public string[] HostNames { get; set; }
}
}
Essa classe representa os nossos tenants, onde a propriedade Name é o nome do tenant (ex: abc) e o HostNames são os hosts (ex: abc.com, xyz.com)
Após essa etapa, criar o arquivo AppTenantResolver.cs.
using Microsoft.AspNetCore.Http;
using SaasKit.Multitenancy;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace MultiTenant
{
public class AppTenantResolver : ITenantResolver<AppTenant>
{
IEnumerable<AppTenant> tenants = new List<AppTenant>(new[]
{
new AppTenant {
Name = "Tenant 1",
HostNames = new[] { "localhost:6001" }
},
new AppTenant {
Name = "Tenant 2",
HostNames = new[] { "localhost:6002" }
}
});
public async Task<TenantContext<AppTenant>> ResolveAsync(HttpContext context)
{
TenantContext<AppTenant> tenantContext = null;
var tenant = tenants.FirstOrDefault(t =>
t.HostNames.Any(h => h.Equals(context.Request.Host.Value.ToLower())));
if (tenant != null)
{
tenantContext = new TenantContext<AppTenant>(tenant);
}
return tenantContext;
}
}
}
Das linhas 11 a 21, crio uma lista com meus Tenants que serão utilizados. Essa lista poderia vir de um banco de dados. Essa classe implementa a interface ITenantResolver, que é do pacote SaaSKit, e portanto precisamos implementar o método ResolveAsync, que fará a captação do host. Nas linhas 27 e 28 captamos pelo host qual é meu Tenant. Retornando assim o cliente que fez a chamada do serviço.
Feito isso, precisamos alterar nossa Program.cs para ficar dessa maneira:
using System.IO;
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting;
namespace MultiTenant
{
public class Program
{
public static void Main(string[] args)
{
var host = new WebHostBuilder()
.UseKestrel()
.UseContentRoot(Directory.GetCurrentDirectory())
.UseUrls("http://localhost:6001", "http://localhost:6002")
.UseIISIntegration()
.UseStartup<Startup>()
.Build();
host.Run();
}
public static IWebHost BuildWebHost(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>()
.Build();
}
}
Percebam a linha 12; estou utilizando o Kestrel e na linha 14 adiciono minhas urls que serão aceitas pela aplicação.
Agora temos que alterar nosso arquivo de Startup.cs para ficar da seguinte forma:
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
namespace MultiTenant
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
services.AddMultitenancy<AppTenant, AppTenantResolver>();
services.AddMvc();
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseBrowserLink();
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
}
app.UseStaticFiles();
app.UseMultitenancy<AppTenant>();
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
}
}
}
Percebam a linha 17, onde adiciono via AddMultitenancy os objetos que representam os tenants (AppTenant) e o objeto Resolver, que implementa a interface do SaaSKit (AppTenantResolver), e a linha 33 que adiciono no pipeline a utilização do MultiTenant.
Agora precisamos alterar nossa Controller para receber o tenant via GET. No caso do exemplo, fiz a alteração na HomeController.cs.
using System.Diagnostics;
using Microsoft.AspNetCore.Mvc;
using MultiTenant.Models;
namespace MultiTenant.Controllers
{
public class HomeController : Controller
{
private AppTenant tenant;
public HomeController(AppTenant tenant)
{
this.tenant = tenant;
}
public IActionResult Index()
{
return View();
}
public IActionResult About()
{
ViewData["Message"] = "Your application description page.";
return View();
}
public IActionResult Contact()
{
ViewData["Message"] = "Your contact page.";
return View();
}
public IActionResult Error()
{
return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
}
}
}
Percebam a linha 9, onde adiciono uma propriedade privada AppTenant e no parâmetro do construtor eu recebo via injeção de dependência o meu Tenant. Com isso, podemos buscar as configurações específicas do Tenant no banco de dados, por exemplo.
Agora devemos alterar nossa View. Para o exemplo, alterei no _Layout.cshtml, mas vocês podem alterar como acharem melhor.
@inject AppTenant Tenant;
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>@Tenant.Name</title>
<environment include="Development">
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.css" />
<link rel="stylesheet" href="~/css/site.css" />
</environment>
<environment exclude="Development">
<link rel="stylesheet" href="https://ajax.aspnetcdn.com/ajax/bootstrap/3.3.7/css/bootstrap.min.css"
asp-fallback-href="~/lib/bootstrap/dist/css/bootstrap.min.css"
asp-fallback-test-class="sr-only" asp-fallback-test-property="position" asp-fallback-test-value="absolute" />
<link rel="stylesheet" href="~/css/site.min.css" asp-append-version="true" />
</environment>
</head>
<body>
@if (Tenant.Name == "Tenant 1")
{
<style>
body {
background-color: red;
}
</style>
}
else
{
<style>
body {
background-color: lightblue;
}
</style>
}
<nav class="navbar navbar-inverse navbar-fixed-top">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
@if (Tenant.Name == "Tenant 1")
{
<a asp-area="" asp-controller="Home" asp-action="Index" class="navbar-brand">Tenant 1</a>
}
else
{
<a asp-area="" asp-controller="Home" asp-action="Index" class="navbar-brand">Tenant 2</a>
}
</div>
<div class="navbar-collapse collapse">
<ul class="nav navbar-nav">
<li><a asp-area="" asp-controller="Home" asp-action="Index">Home</a></li>
<li><a asp-area="" asp-controller="Home" asp-action="About">About</a></li>
<li><a asp-area="" asp-controller="Home" asp-action="Contact">Contact</a></li>
</ul>
</div>
</div>
</nav>
<div class="container body-content">
@RenderBody()
<hr />
<footer>
<p>© 2018 - MultiTenant</p>
</footer>
</div>
<environment include="Development">
<script src="~/lib/jquery/dist/jquery.js"></script>
<script src="~/lib/bootstrap/dist/js/bootstrap.js"></script>
<script src="~/js/site.js" asp-append-version="true"></script>
</environment>
<environment exclude="Development">
<script src="https://ajax.aspnetcdn.com/ajax/jquery/jquery-2.2.0.min.js"
asp-fallback-src="~/lib/jquery/dist/jquery.min.js"
asp-fallback-test="window.jQuery"
crossorigin="anonymous"
integrity="sha384-K+ctZQ+LL8q6tP7I94W+qzQsfRV2a+AfHIi9k8z8l9ggpc8X+Ytst4yBo/hH+8Fk">
</script>
<script src="https://ajax.aspnetcdn.com/ajax/bootstrap/3.3.7/bootstrap.min.js"
asp-fallback-src="~/lib/bootstrap/dist/js/bootstrap.min.js"
asp-fallback-test="window.jQuery && window.jQuery.fn && window.jQuery.fn.modal"
crossorigin="anonymous"
integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa">
</script>
<script src="~/js/site.min.js" asp-append-version="true"></script>
</environment>
@RenderSection("Scripts", required: false)
</body>
</html>
Percebam a linha 1 que eu injeto, o Tenant que veio da minha Controller. Das linhas 21 a 37, muda a cor de fundo conforme meu Tenant. Se for Tenant 1, cor de fundo verde claro, e se não, cor de fundo azul claro
E por fim, devemos alterar o arquivo launchSettings.json que está na pasta “Properties”.
{
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:50504/",
"sslPort": 0
}
},
"profiles": {
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"MultiTenant": {
"commandName": "Project",
"launchBrowser": true,
"launchUrl": "http://localhost:6001/",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}
Finalmente adiciono uma configuração para iniciar o projeto das linhas 18 a 24.

Iniciado a aplicação, agora está escutando os dois hosts configurados conforme figura abaixo:

Então, acessando o localhost:6001, temos o Tenant 1 com fundo verde.

E acessando o localhost:6002, temos o Tenant 2 com fundo azul:

O código completo está no meu GitHub para quem quiser conhecer mais.
Segue os slides da minha apresentação no Visual Studio Summit 2018 sobre o tema.
Valeu e até a próxima!



