.NET

19 jun, 2018

Aplicações MultiTenant no ASP.NET Core 2.0 utilizando o SaaSKit

Publicidade

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.

À esquerda temos um exemplo de um único cliente, cada um com seu serviço e seu banco dados. Ao meio, temos diversos clientes acessando um único serviço, exemplificando a arquitetura MultiTenant. À direita, mais um exemplo de MultiTenant, Nssa figura, temos três clientes acessando um único serviço e um único banco de dados.

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!