# Shadow Properties in EF Core Explained

> Shadow properties exist in the EF Core model but not in your entity classes. They are useful for audit fields, foreign keys, and metadata you want in the database but not in your domain model.

Published: 2026-08-26. Author: Milan Jovanović.

Canonical: https://milanjovanovic.tech/blog/shadow-properties-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 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](https://milanjovanovic.tech/blog/change-tracker-ef-core) 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.

![A shadow property such as CreatedAt exists in the EF Core model and as a database column, but has no matching property on the Order entity class](https://milanjovanovic.tech/blogs/articles/shadow-properties-ef-core/shadow-property-layers.png)

## Defining Shadow Properties

Define a shadow property in your entity configuration:

```csharp
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:

```sql
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`, `CreatedBy` that 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** - `IsDeleted` columns used by [query filters](https://milanjovanovic.tech/blog/how-to-use-global-query-filters-in-ef-core) but hidden from the entity
- **Concurrency tokens** - `RowVersion` or `xmin` columns 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:

```csharp
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](https://milanjovanovic.tech/blog/ef-core-migrations-best-practices):

```sql
"CustomerId" uuid NOT NULL
```

You can configure the shadow foreign key explicitly:

```csharp
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:

```csharp
var recentOrders = await context.Orders
    .OrderByDescending(o => EF.Property<DateTime>(o, "CreatedAt"))
    .Take(10)
    .ToListAsync();
```

```csharp
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](https://milanjovanovic.tech/blog/change-tracker-ef-core):

```csharp
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:

```csharp
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](https://milanjovanovic.tech/blog/how-to-use-ef-core-interceptors) for this.

## Applying Shadow Properties to All Entities

Instead of configuring shadow properties on each entity individually, apply them in `OnModelCreating`:

```csharp
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](https://milanjovanovic.tech/blog/owned-types-ef-core-ddd) because they're stored as part of their parent entity.

## Shadow Properties With Query Filters

Shadow properties work well with [global query filters](https://milanjovanovic.tech/blog/how-to-use-global-query-filters-in-ef-core) for multi-tenancy or soft delete:

```csharp
// 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:

```csharp
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:

```csharp
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.
