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!