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- 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: