EF Core - Multi-Tenant Applications

EF Core - Multi-Tenant Applications

Ola pessoALL,

Ha algum tempo recebi uma duvida de como poderíamos trabalhar com o entity framework num cenário onde temos um banco por cliente e resolvi escrever sobre a solução proposta.

Quando posso escolher, eu prefiro trabalhar com colunas identificadoras no banco de dados, como por exemplo ter um ClientId nas tabelas e forcar regras para sempre especificar essa coluna nas queries, mas em alguns cenários o sistema já trabalha com um banco por cliente ou acaba sendo uma necessidade do negocio.
Não vou discutir qual seria melhor ou pior alternativa, pois ambas estrategias tem suas vantagens e desvantagens.
(um pequeno comentário, conversando com o Fabricio Lima chegamos em um consenso que um hibrido de ambos os modelos seria uma excelente solução também.

Para quem preferir, gravei toda a sessao enquanto estava construindo o exemplo.

Cenário

Montei um cenário simples, com um banco master e um banco para cada cliente

Estrutura dos bancos no servidor sql

Ao invés de nomear os bancos com o id do cliente optei por seguir um padrão simples MultiTenant_SlugDoCliente para o nosso exemplo evita que eu precise fazer validações extras, mas para um ambiente de produção você prefira usar uma outra estrategia.

Implementação

Para registrar um DbContext no contêiner do asp.net core usamos um código parecido com o trecho a seguir:

services.AddDbContext<WorkshopDataContext>(builder =>
{
	builder.UseSqlServer(configuration.GetConnectionString("DefaultConnection"),
		providerOptions => providerOptions.EnableRetryOnFailure());

	builder.EnableSensitiveDataLogging();
});

No nosso exemplo temos 2 DbContexts no projeto.

  • MasterDataContext - Contexto com as tabelas relacionados ao controle dos clientes do sistema
  • CustomerDataContext - Contexto com as tabelas do nosso sistema e que sempre deve ser criado de acordo com o cliente da requisição.

Nesse cenário podemos utilizar o método padrão para registrar o MasterDataContext mas precisamos de um mecanismo que nos permita criar um CustomerDataContext para cada requisição de acordo com o cliente da requisição.

public void ConfigureServices(IServiceCollection services)
        {
            services.AddControllers();

            services.AddDbContext<MasterDataContext>(c =>
                c.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));

            services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
            services.AddCustomerDbContext(Configuration);
        }

No código anterior utilizamos o método AddDbContext para registrar o nosso MasterDataContext e criamos um novo método chamado AddCustomerDbContext para registrar o nosso CustomerDataContext
O objetivo é manter o uso do contêiner de DI do asp.net core e o comportamento de ter um único DbContext para todo o escopo da requisição, sendo assim, não só os Controllers como também os serviços e classes que precisem de uma instancia do CustomerDataContext vão poder utilizar a mesma instancia.

public static class CustomerDataContextExtensions
    {
        public static void AddCustomerDbContext(this IServiceCollection services, IConfiguration configuration)
        {
            services.AddScoped(provider =>
            {
                var httpContext = provider.GetService<IHttpContextAccessor>();

                // In this sample we are using a customer identifier as the firs segment in the url request
                // Ex: http://localhost:5000/clienta/contacts
                //     http://localhost:5000/clientb/contacts
                var clientSlug = httpContext.HttpContext.Request.Path.Value.Split("/", StringSplitOptions.RemoveEmptyEntries)[0];

                // If you need to perform any validation like if the customer exists
                // or if it has a valid subscription you can request a master context
                // and perform validations
                //var masterContext = provider.GetService<MasterDataContext>();

                var connString = configuration.GetClientConnectionString(clientSlug);
                var opts = new DbContextOptionsBuilder<CustomerDataContext>();
                opts.UseSqlServer(connString, s => s.EnableRetryOnFailure());
                opts.EnableSensitiveDataLogging();

                return new CustomerDataContext(opts.Options);
            });
        }
    }

Por ser um exemplo eu omiti algumas validações que precisaríamos fazer em um cenário real, mas observem que consigo requisitar qualquer serviço direto do contêiner do asp.net core inclusive o nosso MasterDataContext.

Por fim, ao executarmos a nossa aplicacao temos o nosso CustomerDataContext criado de acordo com o cliente da requisicao.

Espero que tenham gostado e nos vemos na próxima live session.
Se gostou do modelo, deixa nos comentários quais os assuntos que gostariam de ver nas próximas lives.

[]s rsantosdev!