Shadow properties are properties defined in the EF Core model and mapped to database columns, but they do not exist on the CLR entity class. EF Core tracks their values in the change tracker, so you can query and update them without putting them on the entity. Foreign keys, audit metadata, and infrastructure state can live there instead of in the domain object's public API, which keeps entities focused.
The tradeoff: query and update access is less obvious unless the convention is consistent.
What Are Shadow Properties?
Shadow properties are properties that exist in the EF Core model - mapped to database columns - but don't exist as CLR properties on your entity class. They're tracked by the change tracker and included in queries, but your domain model doesn't know about them.
This keeps your entity classes clean. Infrastructure concerns like CreatedAt, UpdatedAt, or hidden foreign keys stay in the EF Core configuration layer where they belong.
Defining Shadow Properties
Define a shadow property in your entity configuration:
public class OrderConfiguration : IEntityTypeConfiguration<Order>
{
public void Configure(EntityTypeBuilder<Order> builder)
{
builder.Property<DateTime>("CreatedAt");
builder.Property<DateTime>("UpdatedAt");
builder.Property<string>("CreatedBy").HasMaxLength(100);
}
}
These properties don't exist on the Order class, but EF Core creates the corresponding database columns:
CREATE TABLE "Orders" (
"Id" uuid NOT NULL,
"Status" varchar(50) NOT NULL,
"CreatedAt" timestamptz NOT NULL,
"UpdatedAt" timestamptz NOT NULL,
"CreatedBy" varchar(100),
CONSTRAINT "PK_Orders" PRIMARY KEY ("Id")
);
Note that CreatedBy is nullable.
A shadow property with a reference type is optional unless you add IsRequired(), because there is no nullable reference type annotation to infer from.
Your Order entity stays focused on domain logic without audit infrastructure leaking in.
When to Use Shadow Properties
Shadow properties are most useful for:
- Audit fields -
CreatedAt,UpdatedAt,CreatedBythat every entity needs but aren't part of the domain - Foreign keys - EF Core automatically creates shadow foreign keys for navigation properties
- Soft delete flags -
IsDeletedcolumns used by query filters but hidden from the entity - Concurrency tokens -
RowVersionorxmincolumns for optimistic concurrency - Tenant IDs - multi-tenancy discriminators that don't belong in the domain model
Automatic Shadow Properties
EF Core creates shadow properties automatically for foreign keys when you define a navigation property without a corresponding foreign key property:
public class Order
{
public Guid Id { get; set; }
public Customer Customer { get; set; } // Navigation property
// No CustomerId property defined
}
EF Core creates a shadow property named CustomerId of type Guid. You can see this in your migrations:
"CustomerId" uuid NOT NULL
You can configure the shadow foreign key explicitly:
builder.HasOne(o => o.Customer)
.WithMany(c => c.Orders)
.HasForeignKey("CustomerId");
Accessing Shadow Properties
Since shadow properties don't exist on the entity, you use EF.Property<T>() to access them in LINQ queries:
var recentOrders = await context.Orders
.OrderByDescending(o => EF.Property<DateTime>(o, "CreatedAt"))
.Take(10)
.ToListAsync();
var ordersCreatedToday = await context.Orders
.Where(o => EF.Property<DateTime>(o, "CreatedAt") >= DateTime.UtcNow.Date)
.ToListAsync();
For reading or writing shadow properties on a specific entity, use the change tracker:
var entry = context.Entry(order);
// Read
var createdAt = entry.Property<DateTime>("CreatedAt").CurrentValue;
// Write
entry.Property<DateTime>("UpdatedAt").CurrentValue = DateTime.UtcNow;
Audit Fields With Shadow Properties
A common pattern is setting audit shadow properties automatically in SaveChangesAsync. This is where shadow properties really shine:
public class AppDbContext : DbContext
{
public override async Task<int> SaveChangesAsync(
CancellationToken ct = default)
{
var now = DateTime.UtcNow;
foreach (var entry in ChangeTracker.Entries())
{
if (entry.Metadata.FindProperty("CreatedAt") is null)
{
continue;
}
if (entry.State == EntityState.Added)
{
entry.Property("CreatedAt").CurrentValue = now;
entry.Property("UpdatedAt").CurrentValue = now;
}
if (entry.State == EntityState.Modified)
{
entry.Property("UpdatedAt").CurrentValue = now;
}
}
return await base.SaveChangesAsync(ct);
}
}
Every entity gets CreatedAt and UpdatedAt without any of them knowing those fields exist. You could also use an EF Core interceptor for this.
Applying Shadow Properties to All Entities
Instead of configuring shadow properties on each entity individually, apply them in OnModelCreating:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
foreach (var entityType in modelBuilder.Model.GetEntityTypes())
{
if (entityType.IsOwned())
{
continue;
}
modelBuilder.Entity(entityType.ClrType)
.Property<DateTime>("CreatedAt")
.HasDefaultValueSql("now()");
modelBuilder.Entity(entityType.ClrType)
.Property<DateTime>("UpdatedAt")
.HasDefaultValueSql("now()");
}
}
This ensures consistency across all entities. I skip owned types because they're stored as part of their parent entity.
Shadow Properties With Query Filters
Shadow properties work well with global query filters for multi-tenancy or soft delete:
// Define shadow property
builder.Property<bool>("IsDeleted").HasDefaultValue(false);
// Apply query filter
builder.HasQueryFilter(o => !EF.Property<bool>(o, "IsDeleted"));
Now deleted entities are filtered out of all queries automatically, and the IsDeleted flag isn't part of your domain model.
For soft delete, intercept the delete operation:
foreach (var entry in ChangeTracker.Entries())
{
if (entry.State == EntityState.Deleted &&
entry.Metadata.FindProperty("IsDeleted") is not null)
{
entry.State = EntityState.Modified;
entry.Property("IsDeleted").CurrentValue = true;
}
}
Indexing Shadow Properties
You can create indexes on shadow properties for query performance:
builder.HasIndex("CreatedAt");
builder.HasIndex("IsDeleted");
// Composite index
builder.HasIndex("IsDeleted", "CreatedAt");
Shadow Properties vs Regular Properties
When should you use a shadow property instead of a regular property?
- Audit timestamps: shadow property, unless your domain logic actually reads
CreatedAt(for example, to enforce a cancellation window). - Foreign keys: shadow by default. Promote to a regular property if you filter or join by the FK often -
EF.Property<Guid>(o, "CustomerId")everywhere gets old fast. - Soft delete flags: shadow property, unless the domain has behavior attached to deletion (restore workflows, "deleted by" rules).
- Concurrency tokens: shadow property, unless domain logic compares versions explicitly.
- Domain-meaningful data: always a regular property. If the business talks about it, it belongs on the class.
The rule is simple: if the property is infrastructure or persistence-only, make it a shadow property. If the domain model needs it, make it a regular property.
There's a middle ground worth knowing: backing fields. If the domain needs the value internally but you don't want a public setter, map the column to a private field instead of using a shadow property.
Gotchas to Watch Out For
A few things that trip people up with shadow properties:
Detached entities lose shadow values. Shadow property values live in the change tracker, not on the object. If you serialize an entity, send it to a client, and re-attach it later, the shadow values are gone. EF Core will treat them as unset.
String-based access means no compiler safety. A typo in EF.Property<DateTime>(o, "CraetedAt") fails at runtime, not at compile time. Keep the property names in constants if you access them in more than one place.
They don't show up in projections automatically. If you need a shadow property value in a DTO, you must select it explicitly with EF.Property<T>() in the projection.
Summary
Shadow properties are useful for persistence metadata that should not become part of the domain API. Define them through one convention, use constants for string-based access, and project them explicitly when callers need the value. If the business gives a field meaning, make it a regular property instead of hiding it in the change tracker.
Frequently Asked Questions
What are shadow properties in EF Core?
Shadow properties are properties defined in the EF Core model and mapped to database columns, but they do not exist on the CLR entity class. EF Core tracks their values in the change tracker, so you can query and update them without polluting your domain model.
How do I query a shadow property in EF Core?
Use the static EF.Property<T>(entity, "PropertyName") method inside LINQ queries. For a specific tracked entity, access the value through context.Entry(entity).Property("PropertyName").CurrentValue.
When does EF Core create shadow properties automatically?
When you define a navigation property without a corresponding foreign key property, EF Core creates a shadow foreign key named after the navigation and the principal key, for example CustomerId for a Customer navigation.
Are shadow properties included in migrations?
Yes. Shadow properties are part of the model, so migrations create real database columns for them, and you can index them like any other column.
What is the difference between shadow properties and backing fields in EF Core?
A shadow property has no member on the entity class at all. A backing field maps a database column to a private field, so the value still lives on the object but is hidden behind the public API. Use backing fields when domain logic needs the value internally, and shadow properties when it does not.



