在C#项目中使用EFCore操作数据库时,我们常常需要为实体添加创建时间和修改时间的追踪功能,但如果不想把这些字段直接定义在实体类里,EFCore的影子属性就是非常合适的解决方案。影子属性是不在实体类中定义,却在EFCore模型中被定义和使用的属性,对应的数据库字段会正常生成,且不会污染实体类的结构。

什么是EFCore影子属性
影子属性是EFCore模型的一部分,它们不存在于实体类的CLR类型中,只能通过EFCore的ChangeTracker或者DbContext.Entry()方法来访问。影子属性非常适合用来存储不需要在业务层暴露的字段,比如审计相关的创建时间、修改时间、操作人ID等。
配置影子属性追踪创建与修改时间
第一步:定义实体类
首先我们有一个基础的实体类,不需要包含创建时间和修改时间的属性:
// 基础实体类,不包含时间追踪字段
public class User
{
public int Id { get; set; }
public string UserName { get; set; }
public string Email { get; set; }
}
第二步:在DbContext中配置影子属性
我们需要在DbContext的OnModelCreating方法中,为User实体配置两个影子属性:CreatedTime和UpdatedTime,分别对应创建时间和修改时间:
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata;
public class AppDbContext : DbContext
{
public DbSet<User> Users { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
// 配置User实体的影子属性
modelBuilder.Entity<User>(entity =>
{
// 创建时间影子属性,类型为DateTime
entity.Property<DateTime>("CreatedTime")
.HasColumnType("datetime")
.HasDefaultValueSql("GETDATE()"); // 数据库层面设置默认值
// 修改时间影子属性,类型为DateTime
entity.Property<DateTime>("UpdatedTime")
.HasColumnType("datetime");
});
}
// 其他DbContext配置,比如数据库连接等
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseSqlServer("Server=.;Database=TestDb;Trusted_Connection=True;");
}
}
第三步:保存数据时自动更新修改时间
仅仅配置影子属性还不够,我们需要在保存数据的时候,自动为新增实体的CreatedTime赋值,为修改实体的UpdatedTime赋值。可以重写DbContext的SaveChanges或者SaveChangesAsync方法来实现:
public class AppDbContext : DbContext
{
// 省略之前的OnModelCreating和OnConfiguring配置
public override int SaveChanges()
{
// 获取所有状态为Added或者Modified的实体
var entries = ChangeTracker.Entries<User>();
foreach (var entry in entries)
{
if (entry.State == EntityState.Added)
{
// 新增实体时,设置CreatedTime为当前时间
entry.Property("CreatedTime").CurrentValue = DateTime.Now;
// 新增时同时设置UpdatedTime为当前时间
entry.Property("UpdatedTime").CurrentValue = DateTime.Now;
}
else if (entry.State == EntityState.Modified)
{
// 修改实体时,更新UpdatedTime为当前时间
entry.Property("UpdatedTime").CurrentValue = DateTime.Now;
// 防止修改时误改CreatedTime
entry.Property("CreatedTime").IsModified = false;
}
}
return base.SaveChanges();
}
public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
{
var entries = ChangeTracker.Entries<User>();
foreach (var entry in entries)
{
if (entry.State == EntityState.Added)
{
entry.Property("CreatedTime").CurrentValue = DateTime.Now;
entry.Property("UpdatedTime").CurrentValue = DateTime.Now;
}
else if (entry.State == EntityState.Modified)
{
entry.Property("UpdatedTime").CurrentValue = DateTime.Now;
entry.Property("CreatedTime").IsModified = false;
}
}
return await base.SaveChangesAsync(cancellationToken);
}
}
访问影子属性的值
因为影子属性不在实体类中定义,所以不能直接通过实体对象访问,需要通过DbContext的Entry方法来获取:
using (var db = new AppDbContext())
{
var user = db.Users.FirstOrDefault(u => u.Id == 1);
if (user != null)
{
// 获取创建时间影子属性的值
var createdTime = db.Entry(user).Property<DateTime>("CreatedTime").CurrentValue;
// 获取修改时间影子属性的值
var updatedTime = db.Entry(user).Property<DateTime>("UpdatedTime").CurrentValue;
Console.WriteLine($"用户创建时间:{createdTime}, 最后修改时间:{updatedTime}");
}
}
注意事项
- 影子属性的名称是区分大小写的,配置和访问的时候需要保证名称完全一致。
- 如果数据库已经存在对应的表,添加影子属性之后需要生成新的迁移文件并更新数据库,不然会报错。
- 在查询的时候,如果需要按照影子属性排序或者筛选,可以使用EFCore的
EF.Property方法:db.Users.OrderBy(u => EF.Property<DateTime>(u, "CreatedTime")).ToList()。 - 如果实体有基类,也可以把影子属性的配置放到基类的模型配置中,这样所有继承的实体都可以自动拥有这些影子属性。
总结
通过EFCore的影子属性,我们可以很方便地为实体添加隐藏的创建时间和修改时间追踪字段,不需要修改实体类的定义,也不会把不需要的字段暴露给业务层。这种方式既保持了实体类的简洁,又实现了审计字段的自动管理,非常适合在需要统一追踪实体变更时间的场景中使用。