Entity Framework中ValueGeneratedOnAddOrUpdate不生效,如何自动生成时间字段?
问题
使用Entity Framework向表中添加和更新记录时,无法让CreatedAt和ModifiedAt字段自动生成值。尝试使用ValueGeneratedOnAddOrUpdate()属性,但未达到预期效果,相关代码片段如下:
using FazendaEletronica.Domain.Entity; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; namespace FazendaEletronica.Infra.Data.EntitiesConfiguration { public class AnimalConfiguration : IEntityTypeConfiguration<Animal> { public void Configure(EntityTypeBuilder<Animal> builder) { builder.HasKey(t => t.Id); builder.Property(p => p.Number).IsRequired(); builder.Property(p => p.SurName).HasMaxLength(100); builder.Property(p => p.BirthDay).IsRequired().HasColumnType("Date"); builder.Property(p => p.CharAnimalSex).HasMaxLength(1).IsRequired(); builder.Property(p => p.AnimalStatus).IsRequired(); builder.Property(p => p.Active).IsRequired().HasColumnType("bit"); builder.Property(p => p.SaleDate).HasColumnType("Date"); builder.HasOne(a => a.AnimalDad).WithMany(a => a.AnimalsDad).HasForeignKey(a => a.AnimalDadId).IsRequired(false); builder.HasOne(p => p.AnimalClass).WithMany(a => a.Animals).HasForeignKey(f => f.AnimalClassId).IsRequired(); builder.HasOne(p => p.AnimalPurpose).WithMany(a => a.Animals).HasForeignKey(f => f.AnimalPurposeId).IsRequired(); builder.HasOne(p => p.UserFarm).WithMany(p => p.Animals).HasForeignKey(f => f.UserFarmId).IsRequired(); builder.Property(p => p.ModifiedAt).ValueGeneratedOnAddOrUpdate(); builder.Property(p => p.CreatedAt).ValueGeneratedOnAdd(); } } }
想知道为什么ValueGeneratedOnAddOrUpdate()无法自动生成值,以及其他实现该效果的方法。
原因分析
ValueGeneratedOnAddOrUpdate()仅用于告知EF Core:该属性的值由数据库自动生成,EF Core在插入/更新时不应覆盖此值,或在操作后重新获取生成的值。但它不会主动生成或设置值——如果数据库本身没有为该字段配置自动生成规则(比如SQL Server的DEFAULT GETDATE()),EF Core不会自动填充这些字段的内容。
另外,DateTime类型字段并非EF Core默认支持自动生成的类型,必须配合数据库配置或代码逻辑才能实现自动赋值。
解决方案
以下是几种可靠的实现方式:
1. 数据库层面配置默认值
在实体配置中同时设置数据库默认值,结合ValueGeneratedOnAddOrUpdate()实现自动生成:
// CreatedAt:插入时使用数据库当前时间 builder.Property(p => p.CreatedAt) .HasDefaultValueSql("GETDATE()") .ValueGeneratedOnAdd(); // ModifiedAt:插入和更新时都使用数据库当前时间 builder.Property(p => p.ModifiedAt) .HasDefaultValueSql("GETDATE()") .ValueGeneratedOnAddOrUpdate();
注意:不同数据库的时间函数不同,比如MySQL用NOW(),PostgreSQL用CURRENT_TIMESTAMP。
2. 重写DbContext的SaveChanges方法
在DbContext中统一处理所有实体的时间戳字段,无需每个实体单独配置:
public override int SaveChanges(bool acceptAllChangesOnSuccess) { UpdateTimestamps(); return base.SaveChanges(acceptAllChangesOnSuccess); } public override Task<int> SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = default) { UpdateTimestamps(); return base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken); } private void UpdateTimestamps() { var entries = ChangeTracker.Entries() .Where(e => e.Entity is BaseEntity && (e.State == EntityState.Added || e.State == EntityState.Modified)); foreach (var entry in entries) { var now = DateTime.UtcNow; // 建议使用UTC时间避免时区问题 if (entry.State == EntityState.Added) { ((BaseEntity)entry.Entity).CreatedAt = now; } ((BaseEntity)entry.Entity).ModifiedAt = now; } }
需要先定义包含时间戳字段的基类,让所有需要该功能的实体继承:
public abstract class BaseEntity { public DateTime CreatedAt { get; set; } public DateTime ModifiedAt { get; set; } // 可添加其他公共字段如Id等 }
3. 使用EF Core拦截器
EF Core 3.0+支持拦截器,可在执行SQL前自动处理时间戳逻辑,无需修改实体基类:
public class TimestampInterceptor : SaveChangesInterceptor { public override ValueTask<InterceptionResult<int>> SavingChangesAsync(DbContextEventData eventData, InterceptionResult<int> result, CancellationToken cancellationToken = default) { var context = eventData.Context; if (context == null) return base.SavingChangesAsync(eventData, result, cancellationToken); var entries = context.ChangeTracker.Entries() .Where(e => e.State is EntityState.Added or EntityState.Modified); foreach (var entry in entries) { var now = DateTime.UtcNow; if (entry.State == EntityState.Added && entry.Property("CreatedAt") != null) { entry.Property("CreatedAt").CurrentValue = now; } if (entry.Property("ModifiedAt") != null) { entry.Property("ModifiedAt").CurrentValue = now; } } return base.SavingChangesAsync(eventData, result, cancellationToken); } }
然后在DbContext的OnConfiguring方法中注册拦截器:
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { optionsBuilder.AddInterceptors(new TimestampInterceptor()); }
内容的提问来源于stack exchange,提问作者Victor

