C# - Aplicações Multi-Tenant


 Hoje veremos o que são aplicações multi-tenant e como podemos criar estas aplicações na linguagem C#.

Uma aplicação multi-tenant é aquela que suporta múltiplos clientes (tenants) em uma única instância da aplicação. Cada cliente possui seus próprios dados isolados e configurações específicas, mas compartilha a mesma infraestrutura e código base da aplicação.

Quando se trata de criar aplicações multi-tenant usando o EF Core temos à disposição suporte nativo para implementar essa arquitetura e existem várias abordagens que podemos usar. A seguir eu vou apresenar uma abordagem comum usada.

A estratégia mais comum é adicionar uma coluna chamada "TenantId" nas tabelas que precisam ser segregadas por cliente sendo que essa coluna é usada para distinguir os dados de cada cliente. e cada entidade deve ter uma propriedade que representa o TenantId. Por exemplo:

public class Customer
{
  public int Id { get; set; }
 
public string Name { get; set; }
 
public int TenantId { get; set; }
}

O valor do TenantId é definido para cada entidade, indicando a qual cliente ela pertence. Dessa forma, é possível filtrar as entidades com base no TenantId para garantir que cada cliente acesse apenas seus próprios dados.

Agora, para configurar o EF Core para trabalhar com a aplicação multi-tenant, você pode utilizar o recurso de filtros globais (global filters) do EF Core. Os filtros globais permitem aplicar uma condição de filtragem em todas as consultas feitas no contexto do EF Core. Nesse caso, podemos aplicar um filtro global baseado no TenantId para garantir que apenas os dados do cliente atual sejam retornados.

Você pode configurar os filtros globais no método OnModelCreating do seu contexto de banco de dados, da seguinte maneira:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
   modelBuilder.Entity<Customer>().HasQueryFilter(c =>
                               c.TenantId == GetCurrentTenantId());
  
// Outras configurações de mapeamento...
}

Nesse exemplo, GetCurrentTenantId() é uma função fictícia que retorna o identificador do cliente atual. Você precisa implementar essa função de acordo com a lógica da sua aplicação, para obter o TenantId correto.

Com essa configuração, sempre que uma consulta for executada no EF Core, o filtro global será aplicado automaticamente, garantindo que apenas os dados do cliente atual sejam retornados.

Essa é apenas uma das abordagens para implementar aplicações multi-tenant usando o EF Core. Existem outras estratégias e considerações, dependendo dos requisitos específicos da sua aplicação.

Estratégias para criar aplicações Multi-Tenant

Existem duas abordagens principais para criar aplicações multi-tenant :

  1. Considerar um único banco de dados e isolamento lógico dos clientes,
  2. Ter Múltiplos bancos de dados com isolamento físico dos clientes.

1-  Isolamento Lógico em um Único Banco de Dados:

Nesta abordagem, todos os dados dos clientes são armazenados em um único banco de dados, mas são isolados logicamente através do uso de identificadores de tenant em cada tabela. A coluna TenantId é usada para distinguir os dados de cada cliente.

Assim considerando a classe Customer:

public class Customer
{
  public int Id { get; set; }
 
public string Name { get; set; }
 
public int TenantId { get; set; }
}

Para recuperar os dados de um cliente específico, é necessário aplicar um filtro baseado no TenantId em todas as consultas. Isso pode ser feito usando filtros globais no EF Core, como mencionado anteriormente.

Vantagens:

Desvantagens:

2- Múltiplos Bancos de Dados com Isolamento Físico

Nessa abordagem, cada cliente possui seu próprio banco de dados separado, proporcionando um isolamento físico completo. Cada banco de dados é independente e contém apenas os dados de um cliente específico.

Por exemplo, suponha que você tenha dois clientes: ClienteA e ClienteB. Você teria dois contextos de banco de dados:

// Contexto de Banco de Dados para o ClienteA
public
class ClienteADbContext : DbContext
{
  
// Configuração para se conectar ao banco de dados do ClienteA
}

// Contexto de Banco de Dados para o ClienteB
public
class ClienteBDbContext : DbContext
{
 
// Configuração para se conectar ao banco de dados do ClienteB
}

Vantagens:

Desvantagens:

A escolha entre essas abordagens depende dos requisitos e restrições específicos do seu projeto. A abordagem de isolamento lógico em um único banco de dados pode ser mais adequada para aplicações com um número limitado de clientes ou onde o isolamento físico dos dados não é uma preocupação crítica. Ela oferece simplicidade na gestão do banco de dados e menor custo de infraestrutura.

Por outro lado, a abordagem de múltiplos bancos de dados com isolamento físico é mais adequada quando há uma necessidade forte de isolamento completo dos dados entre clientes, alta escalabilidade e garantia de segurança. Ela oferece maior flexibilidade na gestão e escalabilidade dos dados, mas também requer um maior esforço na administração dos bancos de dados e um investimento mais significativo em infraestrutura.

Além disso, é importante considerar fatores como requisitos de desempenho, níveis de segurança exigidos, complexidade da aplicação e capacidade de gerenciamento antes de decidir a abordagem a ser adotada.

Lembre-se de que essas são apenas duas das várias abordagens possíveis para criar aplicações multi-tenant, e cada projeto pode exigir uma abordagem personalizada com base em seus requisitos e restrições específicas. É fundamental avaliar cuidadosamente esses requisitos e considerar as vantagens e desvantagens de cada abordagem antes de tomar uma decisão.

A seguir vou fornecer um exemplo prático de como implementar o suporte multi-tenant com EF Core no .NET 7.0. Neste exemplo, vamos considerar a abordagem de isolamento lógico em um único banco de dados.

Exemplo prático : Isolamento lógico com um banco de dados

Passo 1 :  Definir a entidade e contexto de banco de dados

1- Entidade

public class Customer
{
  public int Id { get; set; }
 
public string Name { get; set; }
 
public int TenantId { get; set; }
}

2- Contexto

public class ApplicationDbContext : DbContext
{
 
public DbSet<Customer> Customers { get; set; }
 
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
  {
   
// Configurar a string de conexão do banco de dados aqui
    optionsBuilder.UseSqlServer(
"Sua_ConnectionString");
  }
 
protected override void OnModelCreating(ModelBuilder modelBuilder)
  {
    
base.OnModelCreating(modelBuilder);
   
// Aplicar filtro global para o TenantId
    modelBuilder.Entity<Customer>().HasQueryFilter(c =>
                          c.TenantId == GetCurrentTenantId());
  }
}

Passo 2: Implementar um provedor de contexto de banco de dados multi-tenant

public class MultiTenantDbContextProvider<TContext> where TContext : DbContext
{

  private readonly IHttpContextAccessor _httpContextAccessor;
 
public MultiTenantDbContextProvider(IHttpContextAccessor httpContextAccessor)
  {
     _httpContextAccessor = httpContextAccessor;
  }

  public TContext GetContext()
  {
    
var tenantId = GetCurrentTenantId(); // Obter o TenantId do contexto atual
    
var optionsBuilder = new DbContextOptionsBuilder<TContext>();

     optionsBuilder.UseSqlServer("YourConnectionString");
    
var dbContext = (TContext)Activator.CreateInstance(typeof(TContext),
                                                      optionsBuilder.Options);

      // Configurar o TenantId para o contexto do banco de dados
      dbContext.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
      dbContext.ChangeTracker.AutoDetectChangesEnabled =
false;
      dbContext.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.TrackAll;
      dbContext.ChangeTracker.AutoDetectChangesEnabled =
true;
     
return dbContext;
   }

   private int GetCurrentTenantId()
   {
      
// Implemente a lógica para obter o TenantId do contexto atual
          (por exemplo, a partir do cabeçalho HTTP)

       // Exemplo simplificado:
       
var tenantIdString = _httpContextAccessor.HttpContext.Request.Headers["TenantId"];
       
int.TryParse(tenantIdString, out int tenantId);
       
return tenantId;
   }
}

Passo 3: Configurar a injeção de dependência na classe Program

// Configurar o provedor de contexto de banco de dados multi-tenant
builder.Services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
builder.Services.AddScoped<MultiTenantDbContextProvider<ApplicationDbContext>>();

// Configurar o DbContext

builder.Services.AddDbContext<ApplicationDbContext>((serviceProvider, optionsBuilder) =>
{
   
var multiTenantDbContextProvider =           serviceProvider.GetRequiredService<MultiTenantDbContextProvider<ApplicationDbContext>>();

   
var dbContext = multiTenantDbContextProvider.GetContext();

    optionsBuilder.UseInternalServiceProvider(serviceProvider).UseDbContext(dbContext);
});
 

Agora, sempre que você precisar acessar o contexto do banco de dados em uma determinada classe ou controlador, você pode injetar o ApplicationDbContext e usar normalmente. O provedor de contexto de banco de dados multi-tenant garantirá que os dados retornados sejam filtrados corretamente com base no TenantId do contexto atual.

public class MeuService
{
  
private readonly ApplicationDbContext _dbContext;

  
public MyService(ApplicationDbContext dbContext)
   {
      _dbContext = dbContext;
   }

  
public List<Customer> GetCustomers()
   {
     
return _dbContext.Customers.ToList();
   }
}

Neste exemplo, a classe MeuService recebe o ApplicationDbContext como uma dependência. O provedor de contexto multi-tenant garante que o contexto do banco de dados seja configurado corretamente para o TenantId do contexto atual, e os dados retornados serão filtrados automaticamente com base nesse TenantId.

Lembre-se de que este exemplo é uma simplificação e você pode ajustá-lo e expandi-lo para atender às necessidades específicas da sua aplicação.

Com essa implementação, você pode criar uma aplicação multi-tenant no .NET 7.0 usando o EF Core, onde os dados de cada cliente são isolados logicamente em um único banco de dados. O filtro global é aplicado automaticamente pelo EF Core para garantir que cada cliente tenha acesso apenas aos seus próprios dados.

É importante destacar que este exemplo aborda apenas o isolamento lógico dos dados. Se você precisar de isolamento físico com múltiplos bancos de dados separados, seria necessário adaptar a implementação para lidar com essa abordagem específica.

Certifique-se de ajustar a implementação de acordo com os requisitos da sua aplicação e considerar aspectos como segurança, escalabilidade e gerenciamento de dados ao escolher a abordagem multi-tenant mais adequada para o seu caso.

E estamos conversados.

"Não retribuam mal com mal, nem insulto com insulto; ao contrário, bendigam; pois para isso vocês foram chamados, para receberem bênção por herança"
1 Pedro 3:9

Referências:


José Carlos Macoratti