.NET 6升级至.NET 7后Npgsql驱动EF Core迁移报错问题
EF Core 7.0.0 + Npgsql迁移失败:重复键CreatedAt异常
将项目中Npgsql及相关依赖升级至7.0.0版本后,所有EF Core数据库迁移操作均无法完成,项目DbContext中包含数据种子填充逻辑。
TransportationContext上下文代码
public class TransportationContext: DbContext { public DbSet<Gender> Genders { get; set; } = null!; public DbSet<Language> Languages { get; set; } = null!; public DbSet<Region> Regions { get; set; } = null!; public DbSet<Role> Roles { get; set; } = null!; public DbSet<Policy> Policies { get; set; } = null!; public DbSet<User> Users { get; set; } = null!; public DbSet<Servant> Servants { get; set; } = null!; public DbSet<Vehicle> Vehicles { get; set; } = null!; public DbSet<DiscountCode> DiscountCodes { get; set; } = null!; public DbSet<DiscountCodeUser> DiscountCodeUsers { get; set; } = null!; public DbSet<Service> Services { get; set; } = null!; public DbSet<ServiceBaseType> ServiceBaseTypes { get; set; } = null!; public DbSet<ServiceAreaType> ServiceAreaTypes { get; set; } = null!; public DbSet<ServiceAreaParams> ServiceAreaParams { get; set; } = null!; public DbSet<BaseNightPeriods> BaseNightPeriods { get; set; } = null!; public DbSet<ServantStatus> ServantStatuses { get; set; } = null!; public DbSet<Tasks> Tasks { get; set; } = null!; public DbSet<ServantDailyStatistic> ServantDailyStatistics { get; set; } = null!; public DbSet<CanceledTask> CanceledTasks { get; set; } = null!; public DbSet<CancelReason> CancelReasons { get; set; } = null!; public DbSet<CancelReasonTranslation> CancelReasonTranslations { get; set; } = null!; public DbSet<Request> Requests { get; set; } = null!; public DbSet<RequestServant> RequestServants { get; set; } = null!; public DbSet<Group> Groups { get; set; } = null!; public DbSet<ServantDailyOnlinePeriod> ServantDailyOnlinePeriods { get; set; } = null!; public DbSet<ServantHourlyStatistic> ServantHourlyStatistics { get; set; } = null!; public DbSet<TaskHourlyStatistic> TaskHourlyStatistics { get; set; } = null!; public DbSet<DailyStatistic> DailyStatistics { get; set; } = null!; public DbSet<ActiveRole> ActiveRoles { get; set; } = null!; [Obsolete("Obsolete")] static TransportationContext() { NpgsqlConnection.GlobalTypeMapper.MapEnum<DiscountCodeTypeEnum>(); NpgsqlConnection.GlobalTypeMapper.MapEnum<TaskStatusEnum>(); NpgsqlConnection.GlobalTypeMapper.MapEnum<RequestTypeEnum>(); NpgsqlConnection.GlobalTypeMapper.MapEnum<RequestStatusEnum>(); } public TransportationContext(DbContextOptions<TransportationContext> options) : base(options) { } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { optionsBuilder.UseNpgsql(o => o.UseNetTopologySuite()); // optionsBuilder.ConfigureWarnings(w => w.Throw(RelationalEventId.MultipleCollectionIncludeWarning)); } protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.HasPostgresExtension("postgis"); modelBuilder.HasPostgresEnum<DiscountCodeTypeEnum>(); modelBuilder.HasPostgresEnum<TaskStatusEnum>(); modelBuilder.HasPostgresEnum<RequestTypeEnum>(); modelBuilder.HasPostgresEnum<RequestStatusEnum>(); modelBuilder.Entity<Policy>().HasData(GetPolicies()); modelBuilder.Entity<Role>().HasData(GetBaseRoles()); modelBuilder .Entity<Policy>() .HasMany(p => p.Roles) .WithMany(r => r.Policies) .UsingEntity(j => j.HasData(GetRolePolicies(1))); modelBuilder.Entity<DiscountPeriods>(builder => { // StartTime is a TimeOnly property and time on database builder.Property(x => x.StartTime) .HasConversion<TimeOnlyConverter, TimeOnlyComparer>(); // EndTime is a TimeOnly property and time on database builder.Property(x => x.EndTime) .HasConversion<TimeOnlyConverter, TimeOnlyComparer>(); }); modelBuilder.Entity<ServantDailyStatistic>(builder => { // StartTime is a TimeOnly property and time on database builder.Property(x => x.Date) .HasConversion<DateOnlyConverter, DateOnlyComparer>(); }); foreach (var entityType in modelBuilder.Model.GetEntityTypes()) //If the actual entity is an auditable type. if (typeof(Auditable).IsAssignableFrom(entityType.ClrType)) { //This adds (In a reflection type way), a Global Query Filter //That always excludes deleted items. You can opt out by using dbSet.IgnoreQueryFilters() var parameter = Expression.Parameter(entityType.ClrType, "p"); var deletedCheck = Expression.Lambda( Expression.Equal(Expression.Property(parameter, "DeletedAt"), Expression.Constant(null)), parameter); modelBuilder.Entity(entityType.ClrType).HasQueryFilter(deletedCheck); } } public async Task<int> SaveChangesAsync() { AddTimestamps(); return await base.SaveChangesAsync(); } }
实体修改操作
尝试给RequestServant实体添加IsOnline属性:
public class RequestServant : Auditable { public int Id { get; set; } public bool IsOnline {get; set; } public int RequestId { get; set; } public int ServantId { get; set; } }
迁移时抛出的异常
System.ArgumentException: An item with the same key has already been added. Key: CreatedAt at System.Collections.Generic.Dictionary`2.TryInsert(TKey key, TValue value, InsertionBehavior behavior) at System.Collections.Generic.Dictionary`2.Add(TKey key, TValue value) at System.Linq.Enumerable.ToDictionary[TSource,TKey](IEnumerable`1 source, Func`2 keySelector, IEqualityComparer`1 comparer) at Microsoft.EntityFrameworkCore.Migrations.Internal.MigrationsModelDiffer.AddSeedData(IEntityType entityType, Dictionary`2 identityMaps, EntityState initialState) at Microsoft.EntityFrameworkCore.Migrations.Internal.MigrationsModelDiffer.TrackData(IRelationalModel source, IRelationalModel target, DiffContext diffContext) at Microsoft.EntityFrameworkCore.Migrations.Internal.MigrationsModelDiffer.GetDataOperations(IRelationalModel source, IRelationalModel target, DiffContext diffContext)+MoveNext() at System.Linq.Enumerable.ConcatIterator`1.MoveNext() at Microsoft.EntityFrameworkCore.Migrations.Internal.MigrationsModelDiffer.Sort(IEnumerable`1 operations, DiffContext diffContext) at Microsoft.EntityFrameworkCore.Migrations.Internal.MigrationsModelDiffer.GetDifferences(IRelationalModel source, IRelationalModel target) at Microsoft.EntityFrameworkCore.Migrations.Design.MigrationsScaffolder.ScaffoldMigration(String migrationName, String rootNamespace, String subNamespace, String language) at Microsoft.EntityFrameworkCore.Design.Internal.MigrationsOperations.AddMigration(String name, String outputDir, String contextType, String namespace) at Microsoft.EntityFrameworkCore.Design.OperationExecutor.AddMigrationImpl(String name, String outputDir, String contextType, String namespace) at Microsoft.EntityFrameworkCore.Design.OperationExecutor.AddMigration.<>c__DisplayClass0_0.<.ctor>b__0() at Microsoft.EntityFrameworkCore.Design.OperationExecutor.OperationBase.<>c__DisplayClass3_0`1.<Execute>b__0() at Microsoft.EntityFrameworkCore.Design.OperationExecutor.OperationBase.Execute(Action action) An item with the same key has already been added. Key: CreatedAt
数据种子填充及时间戳相关方法
private static IEnumerable<Policy> GetPolicies() { var allPolicies = Enum.GetValues<PolicyEnum>(); return allPolicies.Select(p => new Policy { Id = (int)p, Name = p.ToString() }); } private static IEnumerable<Policy> GetBaseRoles() { var allPolicies = Enum.GetValues<BaseRoles>(); return allPolicies.Select(p => new Policy { Id = (int)p, Name = p.ToString() }); } private static IEnumerable<object> GetRolePolicies(int roleId) { var allPolicies = Enum.GetValues<PolicyEnum>(); return allPolicies.Select(p => new { PoliciesId = (int)p, RolesId = roleId }); } private void AddTimestamps() { var entities = ChangeTracker.Entries() .Where(x => x.Entity is Auditable && (x.State == EntityState.Added || x.State == EntityState.Modified)); foreach (var entity in entities) { var now = DateTime.UtcNow; // current datetime if (entity.State == EntityState.Added) { ((Auditable)entity.Entity).CreatedAt = now; ((Auditable)entity.Entity).UpdatedAt = now; } ((Auditable)entity.Entity).UpdatedAt = now; } }
后续排查
将Microsoft.EntityFrameworkCore从7.0.0降级至6.6及相关依赖包后,迁移成功生成,推测这是EF Core 7.0.0的Bug,计划在GitHub上查找相关问题或提交新的Issue。
内容的提问来源于stack exchange,提问作者Syar Sardar
相关产品推荐
相关产品推荐

