Shadow Properties in EF Core: Database Columns Without CLR Properties

Not every database column belongs on your entity class. Audit timestamps, foreign key IDs for navigation-only relationships, and infrastructure metadata are useful in the database but clutter your domain model. EF Core's shadow properties let you define columns that exist only in the EF model — not on the CLR type.

What Are Shadow Properties?

A shadow property is a property that exists in the EF Core model and maps to a database column, but has no corresponding property on the .NET class. You define them in OnModelCreating:

Example.cs
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<BlogPost>()
        .Property<DateTime>("LastModified");
}

This creates a LastModified column in the BlogPosts table, but BlogPost the class knows nothing about it.

Reading and Writing Shadow Properties

Since there's no CLR property to access, you work with shadow properties through the Entry API or EF.Property in queries:

Example.cs
// Writing a shadow property
var post = await context.BlogPosts.FindAsync(1);
context.Entry(post!).Property("LastModified").CurrentValue = DateTime.UtcNow;
await context.SaveChangesAsync();

// Reading a shadow property
var lastModified = context.Entry(post!).Property<DateTime>("LastModified").CurrentValue;

Querying with Shadow Properties

Use EF.Property<T>() to reference shadow properties in LINQ queries:

Example.cs
var recentPosts = await context.BlogPosts
    .OrderByDescending(p => EF.Property<DateTime>(p, "LastModified"))
    .Take(10)
    .ToListAsync();

var oldPosts = await context.BlogPosts
    .Where(p => EF.Property<DateTime>(p, "LastModified") < DateTime.UtcNow.AddYears(-1))
    .ToListAsync();

EF Core translates EF.Property calls directly into SQL column references — no client-side evaluation.

Automatic Audit Timestamps

Shadow properties are ideal for automatic auditing. Override SaveChanges to set timestamps without touching entity classes:

AppDbContext.cs
public class AppDbContext : DbContext
{
    public DbSet<BlogPost> BlogPosts => Set<BlogPost>();
    public DbSet<Comment> Comments => Set<Comment>();

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        foreach (var entityType in modelBuilder.Model.GetEntityTypes())
        {
            modelBuilder.Entity(entityType.ClrType)
                .Property<DateTime>("CreatedAt")
                .HasDefaultValueSql("GETUTCDATE()");

            modelBuilder.Entity(entityType.ClrType)
                .Property<DateTime>("UpdatedAt");
        }
    }

    public override int SaveChanges()
    {
        var now = DateTime.UtcNow;

        foreach (var entry in ChangeTracker.Entries()
            .Where(e => e.State is EntityState.Added or EntityState.Modified))
        {
            entry.Property("UpdatedAt").CurrentValue = now;

            if (entry.State == EntityState.Added)
            {
                entry.Property("CreatedAt").CurrentValue = now;
            }
        }

        return base.SaveChanges();
    }
}

Every entity now gets CreatedAt and UpdatedAt columns without a single property being added to any entity class.

Foreign Keys as Shadow Properties

EF Core automatically creates shadow properties for foreign keys when you define navigations without explicit FK properties:

Example.cs
public class BlogPost
{
    public int Id { get; set; }
    public string Title { get; set; } = string.Empty;
    public Category Category { get; set; } = null!; // Navigation only
}

public class Category
{
    public int Id { get; set; }
    public string Name { get; set; } = string.Empty;
}

EF Core creates a shadow property called CategoryId on the BlogPost entity. You can query it:

Example.cs
var posts = await context.BlogPosts
    .Where(p => EF.Property<int>(p, "CategoryId") == 3)
    .ToListAsync();

You can also configure these explicit shadow FKs:

Example.cs
modelBuilder.Entity<BlogPost>()
    .Property<int>("CategoryId");

modelBuilder.Entity<BlogPost>()
    .HasOne(p => p.Category)
    .WithMany()
    .HasForeignKey("CategoryId");

Indexing Shadow Properties

Shadow properties can be indexed like any other column:

Example.cs
modelBuilder.Entity<BlogPost>()
    .HasIndex("LastModified");

modelBuilder.Entity<BlogPost>()
    .HasIndex("CategoryId", "LastModified");

When to Use Shadow Properties

Good uses:

Avoid when:

Shadow properties keep your entity classes focused on domain concerns while letting EF Core manage the infrastructure columns the database needs. They're a clean separation between your domain model and your persistence model.