Nota
O acesso a esta página requer autorização. Podes tentar iniciar sessão ou mudar de diretório.
O acesso a esta página requer autorização. Podes tentar mudar de diretório.
Muitos aplicativos de linha de negócios são projetados para trabalhar com vários clientes. É importante proteger os dados para que os dados dos clientes não sejam "vazados" ou vistos por outros clientes e potenciais concorrentes. Esses aplicativos são classificados como "multilocatário" porque cada cliente é considerado um locatário do aplicativo com seu próprio conjunto de dados.
Advertência
Este artigo usa um banco de dados local que não exige que o usuário seja autenticado. Os aplicativos de produção devem usar o fluxo de autenticação mais seguro disponível. Para obter mais informações sobre autenticação para aplicativos de teste e produção implantados, consulte Fluxos de autenticação seguros.
Importante
Este documento fornece exemplos e soluções "no estado em que se encontram". Estas não pretendem ser "melhores práticas", mas sim "práticas de trabalho" para sua consideração.
Sugestão
Você pode visualizar o código-fonte deste exemplo no GitHub
Suporte a multilocação
Existem muitas abordagens para implementar a multilocação em aplicativos. Uma abordagem comum (que às vezes é um requisito) é manter os dados de cada cliente em um banco de dados separado. O esquema é o mesmo, mas os dados são específicos do cliente. Outra abordagem é particionar os dados em um banco de dados existente por cliente. Isso pode ser feito usando uma coluna em uma tabela ou tendo uma tabela em vários esquemas com um esquema para cada locatário.
| Abordagem | Coluna destinada ao inquilino? | Esquema por inquilino? | Várias bases de dados? | Suporte EF Core |
|---|---|---|---|---|
| Discriminador (coluna) | Sim | Não | Não | Filtro de consulta global |
| Base de dados por inquilino | Não | Não | Sim | Configuração |
| Esquema por locatário | Não | Sim | Não | Não suportado |
Para a abordagem de banco de dados por locatário, mudar para o banco de dados correto é tão simples quanto fornecer a string de ligação correta. Quando os dados são armazenados em um único banco de dados, um filtro de consulta global pode ser usado para filtrar automaticamente linhas pela coluna ID do locatário, garantindo que os desenvolvedores não escrevam acidentalmente código que possa acessar dados de outros clientes.
Esses exemplos devem funcionar bem na maioria dos modelos de aplicativos, incluindo console, WPF, WinForms e aplicativos ASP.NET Core. Os aplicativos Blazor Server exigem consideração especial.
Blazor Server aplicações e o ciclo de vida da fábrica
O padrão recomendado para usar o Entity Framework Core em aplicativos Blazor é registrar o DbContextFactory e, em seguida, chamá-lo para criar uma nova instância de DbContext cada operação. Por padrão, a fábrica é um singleton, portanto, existe apenas uma cópia para todos os usuários da aplicação. Isso geralmente é bom porque, embora a fábrica seja compartilhada, as instâncias individuais DbContext não são.
Para multilocação, no entanto, a cadeia de conexão pode mudar por usuário. Como a fábrica armazena em cache a configuração com o mesmo tempo de vida, isso significa que todos os usuários devem compartilhar a mesma configuração. Portanto, o tempo de vida deve ser alterado para Scoped.
Este problema não ocorre em aplicações Blazor WebAssembly porque o singleton é limitado ao utilizador. Os aplicativos Blazor Server, por outro lado, apresentam um desafio único. Embora o aplicativo seja um aplicativo web, ele é "mantido vivo" pela comunicação em tempo real usando o SignalR. Uma sessão é criada por usuário e dura além da solicitação inicial. Deve ser fornecida uma nova instância para cada utilizador, de modo a permitir novas configurações. O tempo de vida dessa fábrica especializada tem escopo definido e uma nova instância é criada para cada sessão de utilizador.
Um exemplo de solução (base de dados única)
Uma solução possível é criar um serviço simples ITenantService que lida com a configuração do tenant atual do usuário. Ele fornece retornos de chamada para que o código seja notificado quando o locatário for alterado. A implementação (com os retornos de chamada omitidos para clareza) pode ter esta aparência:
namespace Common
{
public interface ITenantService
{
string Tenant { get; }
void SetTenant(string tenant);
string[] GetTenants();
event TenantChangedEventHandler OnTenantChanged;
}
}
O DbContext pode então gerenciar a multi-locação. A abordagem depende da sua estratégia de banco de dados. Se você estiver armazenando todos os locatários em um único banco de dados, provavelmente usará um filtro de consulta. O ITenantService é passado para o construtor por meio de injeção de dependência e usado para resolver e armazenar o identificador do inquilino.
public ContactContext(
DbContextOptions<ContactContext> opts,
ITenantService service)
: base(opts) => _tenant = service.Tenant;
O OnModelCreating método é substituído para especificar o filtro de consulta:
protected override void OnModelCreating(ModelBuilder modelBuilder)
=> modelBuilder.Entity<MultitenantContact>()
.HasQueryFilter(mt => mt.Tenant == _tenant);
Isso garante que cada consulta seja filtrada para o locatário em cada solicitação. Não há necessidade de filtrar no código do aplicativo porque o filtro global será aplicado automaticamente.
O provedor de inquilinos e DbContextFactory são configurados durante a inicialização da aplicação assim, usando o Sqlite como exemplo:
builder.Services.AddDbContextFactory<ContactContext>(
opt => opt.UseSqlite("Data Source=singledb.sqlite"), ServiceLifetime.Scoped);
Observe que o tempo de vida do serviço está configurado com ServiceLifetime.Scoped. Isso permite que ele dependa do fornecedor de locatário.
Observação
As dependências devem sempre fluir para o singleton. Isso significa que um Scoped serviço pode depender de outro Scoped serviço ou de um Singleton serviço, mas um Singleton serviço só pode depender de outros Singleton serviços: Transient => Scoped => Singleton.
Vários esquemas
Advertência
Este cenário não é suportado diretamente pelo EF Core e não é uma solução recomendada.
Em uma abordagem diferente, o mesmo banco de dados pode manipular tenant1 e tenant2 usando esquemas de tabela.
-
Inquilino1 -
tenant1.CustomerData -
Inquilino2 -
tenant2.CustomerData
Se não estiver a utilizar o EF Core para lidar com atualizações de base de dados com migrações e já tiver tabelas de multi-esquema, pode substituir o esquema num DbContext em OnModelCreating desta forma (o esquema para a tabela CustomerData é definido como referente ao tenant):
protected override void OnModelCreating(ModelBuilder modelBuilder) =>
modelBuilder.Entity<CustomerData>().ToTable(nameof(CustomerData), tenant);
Vários bancos de dados e cadeias de conexão
A versão de banco de dados múltiplo é implementada passando uma string de ligação diferente para cada inquilino. Isso pode ser configurado na inicialização, resolvendo o provedor de serviços e usando-o para criar a cadeia de conexão. Uma cadeia de conexão por seção de locatário é adicionada ao arquivo de configuração appsettings.json.
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"ConnectionStrings": {
"TenantA": "Data Source=tenantacontacts.sqlite",
"TenantB": "Data Source=tenantbcontacts.sqlite"
},
"AllowedHosts": "*"
}
O serviço e a configuração são ambos injetados no DbContext:
public ContactContext(
DbContextOptions<ContactContext> opts,
IConfiguration config,
ITenantService service)
: base(opts)
{
_tenantService = service;
_configuration = config;
}
O locatário é então usado para buscar a cadeia de conexão em OnConfiguring:
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
var tenant = _tenantService.Tenant;
var connectionStr = _configuration.GetConnectionString(tenant);
optionsBuilder.UseSqlite(connectionStr);
}
Isso funciona bem para a maioria dos cenários, a menos que o usuário possa alternar locatários durante a mesma sessão.
Mudança de inquilinos
Na configuração anterior para vários bancos de dados, as opções são armazenadas em cache no Scoped nível. Isso significa que, se o usuário alterar o locatário, as opções não serão reavaliadas e, portanto, a alteração do locatário não será refletida nas consultas.
A solução fácil para isso quando o locatário pode mudar é definir o tempo de vida para Transient. Isso garante que o locatário seja reavaliado junto com a cadeia de conexão cada vez que um DbContext é pedido. O usuário pode mudar de locatário quantas vezes quiser. A tabela seguinte ajuda-o a escolher qual o tempo de vida útil que faz mais sentido para a sua fábrica.
| Cenário | Base de dados única | Várias bases de dados |
|---|---|---|
| O usuário permanece em um único locatário | Scoped |
Scoped |
| O utilizador pode mudar de inquilino | Scoped |
Transient |
O padrão de Singleton ainda faz sentido se o seu banco de dados não assumir dependências de escopo do utilizador.
Notas de desempenho
O EF Core foi projetado para que as instâncias de DbContext sejam criadas rapidamente, com o mínimo de sobrecarga possível. Por esse motivo, criar um novo DbContext por operação geralmente é suficiente. Se essa abordagem estiver afetando o desempenho do seu aplicativo, considere o uso do pool DbContext.
Conclusão
Esta é uma orientação de trabalho para implementar multilocação em aplicativos EF Core. Se você tiver mais exemplos ou cenários ou quiser fornecer comentários, abra um problema e faça referência a este documento.