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 的生命周期和变更追踪机制。一旦脱离上下文或误用查询方式,值就“凭空消失”。
