efcore.pg icon indicating copy to clipboard operation
efcore.pg copied to clipboard

Repeated UpdateData for List<string> property with HasData and migrations

Open Varorbc opened this issue 2 months ago • 2 comments

Bug description

When using EF Core's HasData to seed data for an entity that contains a List property, every time I run Add-Migration, even if nothing has changed in the entity or the seed data, EF Core generates UpdateData statements for the List field.

Your code

await using var context = new BlogContext();
await context.Database.EnsureDeletedAsync();
await context.Database.EnsureCreatedAsync();

public class BlogContext : DbContext
{
    public DbSet<MyEntity> MyEntities { get; set; }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        => optionsBuilder
            .UseNpgsql("Host=localhost;Username=test;Password=test")
            .LogTo(Console.WriteLine, LogLevel.Information)
            .EnableSensitiveDataLogging();

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<MyEntity>().HasData(
                new MyEntity
                {
                    Id = 1,
                    Tags = new List<string> { "A", "B", "C" }
                }
            );
    }
}

public class MyEntity
{
    public int Id { get; set; }
    public List<string> Tags { get; set; }
}

EF Core version

9.0.4

Database provider

Npgsql.EntityFrameworkCore.PostgreSQL

Target framework

.NET 9.0

Operating system

Windows 11

IDE

Visual Studio 2022 17.4

Varorbc avatar Oct 11 '25 00:10 Varorbc

Thanks @Varorbc, I can see this happening now - will investigate.

roji avatar Oct 11 '25 12:10 roji

As a workaround, i have done this in OnModelCreating:


    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);

        AddManualListComparer<int>(modelBuilder);
        AddManualListComparer<string>(modelBuilder);
        AddManualListComparer<long>(modelBuilder);
    }

    private static void AddManualListComparer<TElement>(ModelBuilder modelBuilder)
    {
        foreach (var type in modelBuilder.Model.GetEntityTypes())
        {
            foreach (var property in type.GetProperties())
            {
                // Fix Npgsql bug #3635: List<int> primitive collections have broken ValueComparer
                // https://github.com/npgsql/efcore.pg/issues/3635
                if (property.ClrType == typeof(List<TElement>) && property.IsPrimitiveCollection)
                {
                    property.SetValueComparer(
                        new ValueComparer<List<TElement>>(
                            (c1, c2) =>
                                (c1 == null && c2 == null)
                                || (c1 != null && c2 != null && c1.SequenceEqual(c2)),
                            c => c == null ? 0 : c.Aggregate(0, (a, v) => HashCode.Combine(a, v)),
                            c => c == null ? null! : c.ToList()
                        )
                    );
                }
            }
        }
    }

It's a bit of a brute-force solution, but it at least prevents the extra migration changes :)

zlepper avatar Dec 01 '25 15:12 zlepper