Ao instanciar um contexto DbContext, cada operação CRUD invoca sequencialmente os métodos OnConfiguring() e OnModelCreating().
OnConfiguring()será utilizado para substituir implementações de serviço, permitindo a operação com tabelas particionadasOnModelCreating()será empregado para redefinir o mapeamento entre entidades e tabelas do banco de dados
A cada chamada de OnModelCreating(), o sistema verifica se o mapeamento entidade-tabela foi alterado, aplicando o novo mapeamento quando necessário.
A detecção de mudanças é realizada através da substituição da implementação da interface IModelCacheKeyFactory. Mais detalhes em: Alternando entre múltiplos modelos com o mesmo tipo de DbContext.
Implementação de IModelCacheKeyFactory
ApplicationDbContext é uma implementação de DbContext, enquanto PartitioningKey é uma propriedade pública de ApplicationDbContext. Dependendo da regra de particionamento, o mapeamento será diferente a cada instância.
public class CustomModelCacheKeyFactory : IModelCacheKeyFactory
{
public object Create(DbContext context)
=> context is ApplicationDbContext appContext
? (context.GetType(), appContext.PartitioningKey)
: (object)context.GetType();
}
Configuração em OnConfiguring()
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
base.OnConfiguring(optionsBuilder);
if (!string.IsNullOrEmpty(this.PartitioningKey))
{
optionsBuilder.ReplaceService<IModelCacheKeyFactory, CustomModelCacheKeyFactory>();
}
}
Implementação de IModelCustomizer
Durante a execução de OnModelCreating(), o método Customize() da implementação IModelCustomizer é invocado internamente. Podemos estabelecer as relações de mapeamento neste método.
Implemantação através de herança:
IEntityLocator é um localizador de tipos - implemente conforme sua necessidade.
public class PartitioningModelCustomizer : ModelCustomizer
{
public PartitioningModelCustomizer(ModelCustomizerDependencies dependencies)
: base(dependencies)
{
}
public override void Customize(ModelBuilder modelBuilder, DbContext context)
{
base.Customize(modelBuilder, context);
var appContext = context as ApplicationDbContext;
var entityLocator = appContext.ServiceProvider.GetService<IEntityLocator>();
var partitionableTypes = entityLocator.LocatePartitionableEntities();
if (partitionableTypes?.Any() == true)
{
if (!string.IsNullOrEmpty(appContext.PartitioningKey))
{
foreach (var entityType in partitionableTypes)
{
string tableName = $"{entityType.Name}_{appContext.PartitioningKey}";
switch (appContext.Database.ProviderName)
{
case "Microsoft.EntityFrameworkCore.SqlServer":
modelBuilder.Entity(entityType).ToTable(tableName);
break;
case "Pomelo.EntityFrameworkCore.MySql":
modelBuilder.Entity(entityType).ToTable(tableName.ToLowerInvariant());
break;
case "Microsoft.EntityFrameworkCore.Sqlite":
modelBuilder.Entity(entityType).ToTable(tableName);
break;
default:
modelBuilder.Entity(entityType).ToTable(tableName);
break;
}
}
}
}
}
}
Atualização em OnConfiguring()
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
base.OnConfiguring(optionsBuilder);
if (!string.IsNullOrEmpty(this.PartitioningKey))
{
optionsBuilder
.ReplaceService<IModelCacheKeyFactory, CustomModelCacheKeyFactory>()
.ReplaceService<IModelCustomizer, PartitioningModelCustomizer>();
}
}
Modificação no Construtor de ApplicationDbContext
Conforme mencionado anteriormente, a propriedade PartitioningKey precisa ser inicializada. Existem duas abordagens:
- Passagem por parâmetro no construtor
- Obtenção através de serviço injetado
Passagem por Parâmetro
public string PartitioningKey { get; private set; }
public ApplicationDbContext(string partitioningKey, DbContextOptions options)
: base(options)
{
PartitioningKey = partitioningKey;
}
Obtenção via Serviço
IPartitionResolver é uma interface personalizada para resolver a chave de particionamento - implemente conforme necessário.
protected ApplicationDbContext(DbContextOptions options, IServiceProvider serviceProvider)
: base(options)
{
PartitioningKey = serviceProvider.GetService<IPartitionResolver>().ResolveKey();
}
Forma de Utilização
Exemplo utilizendo passagem por parâmetro no construtor:
var optionsBuilder = new DbContextOptionsBuilder<ApplicationDbContext>();
optionsBuilder.UseSqlServer("connectionString");
var options = optionsBuilder.Options;
using (var dbContext = new ApplicationDbContext("202304", options))
{
// Operações com a tabela particionada para 202304
}
Transações entre Múltiplos Contextos
Para transações que abrangem múltiplos contextos, é essencial utilizar a mesma conexão de banco. Portanto, modifique a configuração para compartilhar a conexão:
var optionsBuilder = new DbContextOptionsBuilder<ApplicationDbContext>();
IDbConnection connection = new SqlConnection("connectionString");
optionsBuilder.UseSqlServer(connection);
var options = optionsBuilder.Options;
using (var primaryContext = new ApplicationDbContext("202304", options))
{
using (var transaction = await primaryContext.Database.BeginTransactionAsync())
{
using (var secondaryContext = new ApplicationDbContext("202305", options))
{
await secondaryContext.Database.UseTransactionAsync(transaction.GetDbTransaction());
// Operações em ambos os contextos
await transaction.CommitAsync();
}
}
}
Considerações Finais
A implementação de particionamento com EF Core segue essencialmente este padrão. É possível consultar frameworks open-source para adaptações específicas ao seu sistema, já que soluções genéricas podem não atender todos os cenários. Este guia oferece os fundamentos para construir uma solução personalizada.