EF Core加载未修改实体保存触发主键约束冲突问题排查
我正在开发一个使用Entity Framework Core将数据持久化到Microsoft SQL Server的Azure Function应用。当前遇到的问题是:在服务代码中加载活跃的NotificationRecord记录,未做任何修改就将其添加到上下文并保存,却触发了主键约束冲突错误,提示无法插入重复主键值。我搞不懂为什么Entity Framework Core会把这些已存在的实体当成新实体来尝试插入,附上相关代码,求帮忙排查原因。
错误信息
Microsoft.EntityFrameworkCore.DbUpdateException: An error occurred while saving the entity changes. See the inner exception for details.
Microsoft.Data.SqlClient.SqlException (0x80131904): Violation of PRIMARY KEY constraint 'PK_NotificationRecords'. Cannot insert duplicate key in object 'dbo.NotificationRecords'. The duplicate key value is (02289e67-e7f1-401e-f603-08dcf4ef7c1b).
Service代码
private async Task DispatchNotifications(List<ApplicationsByOwner> applicationsByOwners, CancellationToken cancellationToken) { var activeNotificationRecords = (await notificationRepository.GetActiveNotificationRecords(assetType: AssetType.Application, cancellationToken: cancellationToken)).ToList(); await notificationRepository.AddNotificationRecordsAsync(activeNotificationRecords, cancellationToken); await notificationRepository.SaveChangesAsync(cancellationToken); // ERROR HERE }
DbContext代码
public sealed class AssetOwnershipContext : DbContext { public DbSet<NotificationRecord> NotificationRecords { get; set; } public AssetOwnershipContext(DbContextOptions<AssetOwnershipContext> options) : base(options) { Database.EnsureCreated(); } protected override void OnModelCreating(ModelBuilder modelBuilder) { // NotificationRecord modelBuilder.Entity<NotificationRecord>().Property(p => p.CreatedAt).HasDefaultValueSql("GETUTCDATE()"); modelBuilder.Entity<NotificationRecord>().Property(p => p.UpdatedAt).HasDefaultValueSql("GETUTCDATE()").ValueGeneratedOnAdd(); modelBuilder.Entity<NotificationRecord>().Property(d => d.AssetType).HasConversion<string>(); base.OnModelCreating(modelBuilder); } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { base.OnConfiguring(optionsBuilder); } }
Repository代码
public async Task<IEnumerable<NotificationRecord>> GetActiveNotificationRecords(Guid? owner = default, AssetType? assetType = default, CancellationToken cancellationToken = default) { var query = context.NotificationRecords.Where(x => x.IsActive); if (owner.HasValue) { query = query.Where(x => x.OwnerId == owner); } if (assetType.HasValue) { query = query.Where(x => x.AssetType == assetType); } return await query.ToListAsync(cancellationToken); } public async Task AddNotificationRecordsAsync(IEnumerable<NotificationRecord> notificationRecords, CancellationToken cancellationToken = default) { await context.NotificationRecords.AddRangeAsync(notificationRecords, cancellationToken); } public async Task<bool> SaveChangesAsync(CancellationToken cancellationToken = default) { return await context.SaveChangesAsync(cancellationToken) >= 0; }
NotificationRecord实体代码
[Index(nameof(OwnerId))] [Index(nameof(AssetId))] [Index(nameof(UserPrincipalName))] [Index(nameof(CreatedAt))] public class NotificationRecord { [Key] [DatabaseGenerated(DatabaseGeneratedOption.Identity)] public Guid Id { get; init; } [Required] public Guid OwnerId { get; set; } [Required] public Guid AssetId { get; set; } [Required] public AssetType AssetType { get; set; } [MaxLength(256)] public string? DisplayName { get; set; } [MaxLength(256)] public string? GivenName { get; set; } [MaxLength(256)] public string? Surname { get; set; } [Required][MaxLength(256)] public string? UserPrincipalName { get; set; } // [Required] public ICollection<Notification> Notifications { get; } = new List<Notification>(); [Required] public DateTime? CreatedAt { get; set; } [Required] public DateTime? UpdatedAt { get; set; } [Required] public bool IsActive { get; set; } = true; }
核心原因
调用AddRangeAsync方法处理已存在的实体时,EF Core会将这些实体标记为Added状态,尝试插入新记录。而这些实体本身已经在数据库中存在,主键重复直接导致约束冲突。
额外可能的影响因素:
- 如果DbContext的生命周期管理不当(比如每次操作都新建上下文),加载的实体可能处于
Detached状态,EF Core无法识别它们是已存在的记录。 Id属性使用init修饰符不影响状态跟踪,但如果上下文无法关联到已加载的实体,依然会被视为新实体。
解决方法
移除多余的Add操作:你的代码中没有修改实体,完全不需要调用
AddNotificationRecordsAsync,直接删除这一行即可,调用SaveChangesAsync不会触发任何不必要的插入操作。关联Detached实体(如果需要):如果确实需要将处于
Detached状态的实体重新关联到上下文(比如跨上下文操作),用AttachRange替代AddRangeAsync,它会将实体标记为Unchanged状态,EF Core不会尝试插入,只会跟踪后续修改:
public void AttachNotificationRecords(IEnumerable<NotificationRecord> notificationRecords) { context.NotificationRecords.AttachRange(notificationRecords); }
之后在Service中按需调用该方法即可。
- 检查上下文生命周期:在Azure Function中确保DbContext使用Scoped生命周期,避免频繁创建新上下文导致实体状态丢失。
内容的提问来源于stack exchange,提问作者Shuzheng

