如何使用EF获取数据库生成的插入实体主键?
解决EF Core中SaveChangesAsync后获取新增实体ID并记录日志的问题
当前你的代码可以正常记录删除实体和修改属性的日志,但插入实体时EntidadeId只能设为"0"——因为调用SaveChangesAsync()前无法获取数据库生成的ID;而调用之后,所有被跟踪的条目状态会变为Unchanged,无法区分哪些是刚插入的实体。
原问题代码
public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default) { var entitys = ChangeTracker.Entries() .Where(e => (e.State == EntityState.Added || e.State == EntityState.Modified || e.State == EntityState.Deleted) && (e.Entity is EntidadeBase)); foreach (EntityEntry entrada in entitys.ToList()) { if (entrada.State == EntityState.Added || entrada.State == EntityState.Deleted) { var log = new Log(); log.UsuarioId = UsuarioId; log.Entidade = entrada.Entity.GetType().Name; log.TipoOperacao = entrada.State == EntityState.Added ? TipoOperacao.Incluir : TipoOperacao.Excluir; log.EntidadeId = entrada.State == EntityState.Added ? "0" : ((EntidadeBase)entrada.Entity).GetId(); Historico.Add(log); } else { foreach (PropertyEntry prop in entrada.Properties.Where(p => p.IsModified)) { var log = new Log(); log.TipoOperacao = TipoOperacao.Alterar; log.Atributo = prop.Metadata.PropertyInfo?.Name; log.EntidadeId = ((EntidadeBase)entrada.Entity).GetId(); log.TipoOperacao = TipoOperacao.Alterar; log.ValorAntigo = prop.OriginalValue?.ToString(); log.ValorNovo = prop.CurrentValue?.ToString(); Historico.Add(log); } } } base.SaveChangesAsync(cancellationToken); }
解决方案:拆分日志处理流程
核心思路是先收集新增实体的引用,执行SaveChanges获取ID后再生成插入日志,同时确保删除和修改的日志在SaveChanges前完成处理(避免实体状态变化后无法访问属性)。
修改后的代码:
public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default) { // 收集待新增的实体,同时处理删除、修改的日志 var addedEntities = new List<EntidadeBase>(); var entityEntries = ChangeTracker.Entries() .Where(e => (e.State is EntityState.Added or EntityState.Modified or EntityState.Deleted) && e.Entity is EntidadeBase) .ToList(); foreach (var entry in entityEntries) { if (entry.State == EntityState.Added) { // 暂存新增实体,等SaveChanges后再生成日志 addedEntities.Add((EntidadeBase)entry.Entity); } else if (entry.State == EntityState.Deleted) { // 删除日志必须在SaveChanges前处理,否则实体会被分离无法访问 var log = new Log { UsuarioId = UsuarioId, Entidade = entry.Entity.GetType().Name, TipoOperacao = TipoOperacao.Excluir, EntidadeId = ((EntidadeBase)entry.Entity).GetId() }; Historico.Add(log); } else if (entry.State == EntityState.Modified) { // 修改日志可提前处理,实体ID已存在,属性变更值也能正常获取 foreach (var prop in entry.Properties.Where(p => p.IsModified)) { var log = new Log { TipoOperacao = TipoOperacao.Alterar, Atributo = prop.Metadata.PropertyInfo?.Name, EntidadeId = ((EntidadeBase)entry.Entity).GetId(), ValorAntigo = prop.OriginalValue?.ToString(), ValorNovo = prop.CurrentValue?.ToString() }; Historico.Add(log); } } } // 执行SaveChanges,此时EF会自动将数据库生成的ID填充到新增实体中 var result = await base.SaveChangesAsync(cancellationToken); // 生成新增实体的日志,此时能获取到正确的ID foreach (var entity in addedEntities) { var log = new Log { UsuarioId = UsuarioId, Entidade = entity.GetType().Name, TipoOperacao = TipoOperacao.Incluir, EntidadeId = entity.GetId() }; Historico.Add(log); } // 保存新增实体的日志(如果Historico是DbSet,需再次调用SaveChanges) if (addedEntities.Any()) { await base.SaveChangesAsync(cancellationToken); } return result; }
关键说明
- 新增实体处理:先暂存实体引用,不提前生成日志,等SaveChanges后实体的主键ID被EF填充,再调用
GetId()获取正确值。 - 删除实体处理:必须在SaveChanges前生成日志,因为执行删除后实体状态会变为
Detached,无法再访问其属性。 - 修改实体处理:可提前生成日志,因为修改的实体已存在于数据库中,ID和属性变更值都能正常读取。
- 日志保存:如果
Historico是当前DbContext的DbSet<Log>,新增的日志条目会被上下文跟踪,需要再次调用SaveChanges完成日志的持久化。
可选优化(事务一致性)
如果需要保证实体变更和日志的原子性,可以用EF的事务包裹两次SaveChanges:
using (var transaction = await Database.BeginTransactionAsync(cancellationToken)) { try { // 执行实体变更和删除、修改日志的保存 var result = await base.SaveChangesAsync(cancellationToken); // 生成并保存新增实体的日志 foreach (var entity in addedEntities) { var log = new Log { UsuarioId = UsuarioId, Entidade = entity.GetType().Name, TipoOperacao = TipoOperacao.Incluir, EntidadeId = entity.GetId() }; Historico.Add(log); } await base.SaveChangesAsync(cancellationToken); await transaction.CommitAsync(cancellationToken); return result; } catch { await transaction.RollbackAsync(cancellationToken); throw; } }
内容的提问来源于stack exchange,提问作者AndreR
相关产品推荐
相关产品推荐

