影子属性是EF Core中仅存在于模型和数据库、不映射到C#类字段的属性,需在OnModelCreating中用Fluent API配置,通过ChangeTracker或EF.Property读写,适用于审计字段、软删除等场景。

Shadow Property 是 EF Core 中一种“只活在模型和数据库里、却不在你的 C# 类中出现”的属性。它不占实体类字段,但能参与查询、排序、保存和变更跟踪——本质上是 EF Core 帮你悄悄管理的一列数据。
影子属性怎么定义?只能用 OnModelCreating + Fluent API
你不能用 [Column] 或 [DatabaseGenerated] 这类数据注解来声明影子属性,EF Core 明确禁止。必须在 OnModelCreating 里用 Property<t>("PropertyName")</t> 显式配置:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.Property<DateTime>("LastUpdated")
.HasDefaultValueSql("GETUTCDATE()");
<pre class="brush:php;toolbar:false;">modelBuilder.Entity<Post>()
.Property<bool>("IsDeleted")
.HasDefaultValue(false);}
- 字符串名
"LastUpdated"不校验是否存在于Blog类中——哪怕你真写了同名字段,EF Core 也会把它当影子属性覆盖(除非你明确用HasColumnName指向物理列) - 类型
<datetime></datetime>必须和数据库列类型兼容;SQL Server 的DATETIME2对应DateTime,PostgreSQL 的TIMESTAMP WITH TIME ZONE则建议用DateTimeOffset - 没设默认值或生成策略时,插入新记录会报错(如
NOT NULL列无值),所以通常要配HasDefaultValue或ValueGeneratedOnAddOrUpdate
怎么读写影子属性?不能点出来,得靠 ChangeTracker 和 EF.Property
你写 blog.LastUpdated 会编译失败——它根本不是 Blog 的成员。所有操作都依赖实体是否被上下文追踪(tracked):
- 写入(新增/更新时):
context.Entry(blog).Property("LastUpdated").CurrentValue = DateTime.UtcNow; - 查询中排序:
context.Blogs.OrderBy(b => EF.Property<datetime>(b, "LastUpdated"))</datetime> - 加载后取值:
var time = context.Entry(blog).Property("LastUpdated").CurrentValue; - 注意:
EF.Property只能在 LINQ 查询中用(生成 SQL),不能在内存集合上调用,否则抛InvalidOperationException
哪些场景适合用影子属性?别为了炫技而用
影子属性不是语法糖,它是有明确设计意图的“隐藏状态容器”:
- 审计字段:如
CreatedBy、CreatedAt,业务层不该改、也不该暴露给 API —— 影子属性配合拦截器(SaveChanges重写)刚好闭环 - 软删除标记:
IsDeleted存库但不进 DTO,再配合全局查询过滤器(HasQueryFilter)自动剔除 - 外键列:当你只用导航属性(
public Blog Blog { get; set; })却不想要public int BlogId { get; set; }字段时,EF Core 默认就创建了BlogId影子外键 - 临时业务标记:比如后台任务需要打标
_ProcessingLock,仅用于数据库行锁,完全不参与领域逻辑
容易踩的坑:值丢了、查不到、迁移失败
影子属性最常出问题的地方不是不会写,而是忘了它“只活在 ChangeTracker 里”:
- 用
AsNoTracking()查询后,Entry(x).Property("X").CurrentValue返回null或默认值——因为没被追踪 - 迁移脚本里没生成对应列?检查是否漏了
.Property<t>("Name")</t>,或者类型不匹配导致 EF Core 忽略了该配置 - 用
context.Blogs.FromSqlRaw(...)手写 SQL 查询时,影子属性值不会自动填充——除非你在 SQL 里显式 SELECT 出来并映射 - 并发更新冲突检测(
ConcurrencyCheck)不能直接加在影子属性上,得先用IsRowVersion()或手动设IsConcurrencyToken()
影子属性真正难的不是语法,而是时刻记住:它没有 C# 字段支撑,所有访问都绕不开 EF Core 的生命周期和变更追踪机制。一旦脱离上下文或误用查询方式,值就“凭空消失”。










