Start Debugging

Migrar do seeding com HasData para UseAsyncSeeding no EF Core 11

Um guia passo a passo para mover os dados de seed do HasData para UseSeeding e UseAsyncSeeding no EF Core 11, incluindo a armadilha da migracao DeleteData que apaga suas linhas existentes se voce pular esse cuidado.

Mover os dados de seed de HasData para UseSeeding/UseAsyncSeeding no EF Core 11 e um trabalho de meio dia para uma aplicacao tipica, e a unica coisa que vai te morder nao e a API nova: e que apagar uma chamada HasData faz o proximo dotnet ef migrations add emitir uma instrucao DeleteData que remove exatamente essas linhas de todo banco de dados contra o qual a migracao roda. Se voce apagar o HasData e adicionar o UseSeeding na mesma implantacao sem pensar na ordem, pode apagar dados de referencia em producao. Este guia percorre a migracao na ordem que evita isso, usando .NET 11, EF Core 11 (Microsoft.EntityFrameworkCore 11.0) e C# 14. Reserve uma tarde para uma aplicacao media, a maior parte gasta auditando quais seeds ja deveriam ter sido movidos ha anos.

Vale a pena? Para qualquer coisa dinamica, com chave gerada, calculada ou condicional: sim, e ja estava atrasado. Para uma tabela de referencia genuinamente estatica de tres linhas, deixe no HasData. O objetivo aqui nao e “apagar todo HasData”, e “mover os seeds que nunca pertenceram ao modelo”.

Por que sair do HasData

HasData embute seus dados de seed no snapshot do modelo. Esse unico fato de design e a raiz de cada motivo para sair:

A equipe do EF renomeou HasData para “model-managed data” justamente para desencorajar seu uso como ferramenta geral de seeding. UseSeeding e UseAsyncSeeding, introduzidos no EF Core 9 e vigentes no EF Core 11, sao codigo de aplicacao comum que roda contra um DbContext vivo, sem nenhum desses limites. Se voce ainda esta decidindo quais seeds mover, a matriz de decisao HasData vs UseSeeding traca a linha linha por linha.

O que quebra

AreaMudancaSeveridade
Bancos de dados existentesRemover uma chamada HasData gera DeleteData, que apaga as linhas do seed na migracaoalta
IdempotenciaHasData era diferenciado automaticamente; UseSeeding roda toda vez e precisa de uma verificacao de existencia escrita a maoalta
Dois caminhos de codigoO tooling chama o UseSeeding sincrono, a inicializacao async chama UseAsyncSeeding; implemente apenas um e o outro nao faz nada em silencioalta
Controle de versaoOs valores do seed saem do snapshot da migracao e vivem no codigo de inicializacao; eles nao ficam mais capturados no historico de migracoesmedia
Integridade referencialDados dos quais chaves estrangeiras de outras tabelas dependem agora aterrissam em um callback de inicializacao, nao dentro da transacao da migracaomedia
Configuracao de testesTestes que dependiam do HasData rodar via EnsureCreated ainda funcionam, mas apenas se ambos os overloads forem implementadosbaixa

A linha do topo e a que acaba em um relatorio de incidente. Todo o resto e mecanico.

Checklist pre-voo

Antes de tocar em uma linha de codigo de seeding:

Passos da migracao

A ordem importa. Fazer isso na sequencia errada e exatamente como voce apaga dados de referencia de producao.

1. Escreva primeiro os callbacks UseSeeding e UseAsyncSeeding

Adicione o novo caminho de seeding antes de apagar qualquer coisa. Ambos os overloads, logica identica, verificacao de existencia no topo de cada um. Fatore o corpo em metodos compartilhados para que as versoes sincrona e async nao divirjam:

// Program.cs -- .NET 11, ASP.NET Core 11, EF Core 11 (Microsoft.EntityFrameworkCore 11.0), C# 14
builder.Services.AddDbContext<AppDbContext>(options =>
    options
        .UseSqlServer(builder.Configuration.GetConnectionString("Default"))
        .UseSeeding((context, _) => SeedStatuses(context))
        .UseAsyncSeeding((context, _, ct) => SeedStatusesAsync(context, ct)));

static void SeedStatuses(DbContext context)
{
    string[] required = ["Pending", "Shipped", "Delivered"];
    var existing = context.Set<OrderStatus>()
        .Where(s => required.Contains(s.Name))
        .Select(s => s.Name)
        .ToHashSet();

    var missing = required
        .Where(name => !existing.Contains(name))
        .Select(name => new OrderStatus { Name = name })
        .ToList();

    if (missing.Count > 0)
    {
        context.Set<OrderStatus>().AddRange(missing);
        context.SaveChanges();
    }
}

static async Task SeedStatusesAsync(DbContext context, CancellationToken ct)
{
    string[] required = ["Pending", "Shipped", "Delivered"];
    var existing = await context.Set<OrderStatus>()
        .Where(s => required.Contains(s.Name))
        .Select(s => s.Name)
        .ToListAsync(ct);

    var missing = required
        .Where(name => !existing.Contains(name))
        .Select(name => new OrderStatus { Name = name })
        .ToList();

    if (missing.Count > 0)
    {
        context.Set<OrderStatus>().AddRange(missing);
        await context.SaveChangesAsync(ct);
    }
}

Note que esta versao nao fixa mais Id a mao. O banco de dados atribui. E esse o ponto todo: se voce antes atribuia chaves a mao no HasData, referencias de chave estrangeira existentes podem apontar para esses valores especificos, o que e o que torna o passo 2 delicado.

Verifique: rode a aplicacao contra um banco de dados local vazio com as chamadas HasData ainda presentes. Ela deve iniciar de forma limpa sem linhas duplicadas. A verificacao de existencia deve fazer um segundo inicio nao fazer nada (uma consulta de leitura, zero escritas).

2. Decida como preservar as linhas existentes antes de remover o HasData

Este e o passo que todos pulam e depois lamentam. Quando voce apaga uma chamada HasData e roda dotnet ef migrations add, o EF gera um DeleteData no metodo Up para cada linha previamente semeada:

// .NET 11, EF Core 11 -- what removing HasData generates
migrationBuilder.DeleteData(
    table: "OrderStatuses",
    keyColumn: "Id",
    keyValues: new object[] { 1, 2, 3 });

Em um banco de dados existente, esse DELETE roda. Se outras tabelas tiverem chaves estrangeiras apontando para essas linhas, o delete ou falha com um erro FOREIGN KEY constraint failed ou, pior, cascateia. Voce tem tres opcoes seguras:

Para qualquer coisa com chaves estrangeiras de entrada, a opcao B e quase sempre o que voce quer. As linhas nao precisam se mover; apenas a propriedade delas.

Verifique: depois de gerar a migracao de remocao, leia o arquivo gerado antes de aplica-lo. Confirme que as chamadas DeleteData visam exatamente as linhas que voce espera, e que voce escolheu e aplicou A, B ou C para cada tabela.

3. Remova as chamadas HasData de OnModelCreating

Agora apague o bloco HasData dos seeds que voce esta movendo:

// Before -- .NET 11, EF Core 11
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<OrderStatus>().HasData(
        new OrderStatus { Id = 1, Name = "Pending" },
        new OrderStatus { Id = 2, Name = "Shipped" },
        new OrderStatus { Id = 3, Name = "Delivered" });
}

// After -- the HasData block is gone; seeding lives in UseSeeding now
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    // OrderStatus seeding moved to UseSeeding in Program.cs
}

Verifique: o projeto ainda compila, e dotnet ef migrations has-pending-model-changes reporta uma mudanca pendente (o seed removido) que voce esta prestes a capturar.

4. Gere e revise a migracao de remocao

# .NET 11, EF Core 11
dotnet ef migrations add RemoveOrderStatusSeed

Abra a migracao gerada e aplique sua decisao do passo 2. Se voce escolheu a opcao B, apague o DeleteData de Up e o InsertData correspondente de Down. Deixe intactas as mudancas do snapshot do modelo; essas estao corretas e necessarias.

Verifique: rode dotnet ef migrations script contra seu banco de dados de staging e leia o SQL. Confirme que nenhum DELETE FROM OrderStatuses inesperado sobrevive se sua intencao era preservar as linhas.

5. Aplique contra staging e confirme que os dados sobrevivem

# .NET 11, EF Core 11
dotnet ef database update

Depois inicie a aplicacao para que UseAsyncSeeding rode.

Verifique: consulte a tabela. As linhas de referencia estao presentes exatamente uma vez. Inicie a aplicacao uma segunda vez e confirme que nenhum duplicado apareceu, o que prova que a verificacao de existencia funciona. Se chaves estrangeiras referenciam as linhas, confirme que essas relacoes estao intactas.

Teste de fumaca pos-migracao

Rode esta checklist contra staging antes de enviar:

Plano de rollback

Esta migracao e reversivel, mas leia as letras miudas. Se voce escolheu a opcao B (editou a mao removendo o DeleteData), o Down a nivel de schema nao faz nada para os dados, entao reverter a migracao deixa as linhas no lugar e simplesmente restaura o snapshot do modelo. Readicionar o bloco HasData e gerar uma migracao nova te devolve ao mundo antigo, embora o EF va querer fazer InsertData de qualquer linha que estiver faltando.

Se voce escolheu a opcao A (deixou o apagar-e-ressemear acontecer), o rollback e mais arriscado: as linhas agora tem chaves geradas pelo banco de dados, entao reverter para um modelo HasData que espera chaves fixas nao vai bater. Nesse caso, nao tente um rollback in-place de dados ao vivo. Restaure do backup que voce fez no passo pre-voo. E por isso que o backup pre-voo nao e opcional.

Armadilhas que enfrentamos

Se suas entidades sao records, nada disso muda: records funcionam corretamente com EF Core 11 sob tanto HasData quanto UseSeeding. E se voce esta ajustando a camada de dados enquanto esta aqui, a mesma disciplina de “o que roda e quando” aparece em os interceptores do EF Core 11 para auditoria.

Relacionados

Fontes

Comments

Sign in with GitHub to comment. Reactions and replies thread back to the comments repo.

< Voltar