Shadow Properties in EF Core Explained

Shadow Properties in EF Core Explained

6 min read··

dotnetef-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 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

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, 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 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:

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.

Loading comments...

Whenever you're ready, there are 4 ways I can help you:

  1. Pragmatic Clean Architecture: Join 5,000+ students in this comprehensive course that will teach you the system I use to ship production-ready applications using Clean Architecture. Learn how to apply the best practices of modern software architecture.
  2. Modular Monolith Architecture: Join 2,800+ engineers in this in-depth course that will transform the way you build modern systems. You will learn the best practices for applying the Modular Monolith architecture in a real-world scenario.
  3. Pragmatic REST APIs: Join 1,900+ students in this course that will teach you how to build production-ready REST APIs using the latest ASP.NET Core features and best practices. It includes a fully functional UI application that we'll integrate with the REST API.
  4. Patreon Community: Join a community of 5,000+ engineers and software architects. You will also unlock access to the source code I use in my YouTube videos, early access to future videos, and exclusive discounts for my courses.

The .NET Weekly

Become a Better .NET Software Engineer

Join 66,000+ engineers who are improving their skills every Saturday morning.