Implementação de Sharding com EF Core

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 particionadas
  • OnModelCreating() 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.

Tags: EFCore EntityFramework Sharding Particionamento CSharp

Publicado em 8-22 16:55