What are shadow properties in EF Core?

5 minadvancedEF-Coreshadow-propertiesmodeling

Quick Answer

Shadow properties exist in the EF Core model and change tracker but not on the .NET entity class — they're persisted to the database without polluting your domain model. Common uses include foreign keys you don't want as CLR properties and audit fields (CreatedAt/UpdatedAt). They're configured in `OnModelCreating` and accessed via `EF.Property<T>()` or the change tracker.

Detailed Answer

Shadow Properties are properties that exist in the EF Core model but not in the .NET entity class. They only exist in the database and change tracker.

Why Use Shadow Properties?

  • Foreign keys you don't want in your domain model
  • Audit fields (CreatedDate, ModifiedDate)
  • Soft delete flags
  • Database-specific metadata
  • Keep domain model clean

1. Basic Shadow Property Configuration:

public class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
    public decimal Price { get; set; }
    // No CreatedDate or ModifiedDate properties in class!
}

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    // Add shadow properties
    modelBuilder.Entity()
        .Property("CreatedDate")
        .HasDefaultValueSql("GETDATE()");
        
    modelBuilder.Entity()
        .Property("ModifiedDate");
        
    modelBuilder.Entity()
        .Property("CreatedBy")
        .HasMaxLength(100);
}

2. Accessing Shadow Properties:

// Setting shadow property values
var product = new Product { Name = "Laptop", Price = 999.99m };
context.Products.Add(product);

// Access through Entry API
context.Entry(product).Property("CreatedBy").CurrentValue = "john.doe";
context.Entry(product).Property("ModifiedDate").CurrentValue = DateTime.UtcNow;

await context.SaveChangesAsync();

// Reading shadow property values
var createdDate = context.Entry(product).Property("CreatedDate").CurrentValue;
Console.WriteLine($"Created: {createdDate}");

3. Querying Shadow Properties:

// Use EF.Property in LINQ queries
var recentProducts = context.Products
    .Where(p => EF.Property(p, "CreatedDate") > DateTime.UtcNow.AddDays(-7))
    .ToList();

// Order by shadow property
var products = context.Products
    .OrderByDescending(p => EF.Property(p, "ModifiedDate"))
    .ToList();

// Select shadow properties
var productInfo = context.Products
    .Select(p => new
    {
        p.Name,
        Created = EF.Property(p, "CreatedDate"),
        Modified = EF.Property(p, "ModifiedDate")
    })
    .ToList();

4. Shadow Foreign Keys:

public class Order
{
    public int Id { get; set; }
    public Customer Customer { get; set; }
    // No CustomerId property!
}

public class Customer
{
    public int Id { get; set; }
    public string Name { get; set; }
}

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity()
        .HasOne(o => o.Customer)
        .WithMany()
        .HasForeignKey("CustomerId"); // Shadow FK
}

// Usage
var order = new Order { Customer = customer };
context.Orders.Add(order);
await context.SaveChangesAsync();

// Access shadow FK
var customerId = context.Entry(order).Property("CustomerId").CurrentValue;

5. Audit Trail with Shadow Properties:

public abstract class AuditableEntity
{
    public int Id { get; set; }
}

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    foreach (var entityType in modelBuilder.Model.GetEntityTypes())
    {
        if (typeof(AuditableEntity).IsAssignableFrom(entityType.ClrType))
        {
            modelBuilder.Entity(entityType.ClrType)
                .Property("CreatedDate")
                .HasDefaultValueSql("GETDATE()");
                
            modelBuilder.Entity(entityType.ClrType)
                .Property("ModifiedDate");
                
            modelBuilder.Entity(entityType.ClrType)
                .Property("CreatedBy")
                .HasMaxLength(256);
                
            modelBuilder.Entity(entityType.ClrType)
                .Property("ModifiedBy")
                .HasMaxLength(256);
        }
    }
}

// Auto-populate on SaveChanges
public override int SaveChanges()
{
    var entries = ChangeTracker.Entries()
        .Where(e => e.Entity is AuditableEntity && 
                   (e.State == EntityState.Added || e.State == EntityState.Modified));
    
    foreach (var entry in entries)
    {
        var currentUser = GetCurrentUser(); // Your user service
        
        if (entry.State == EntityState.Added)
        {
            entry.Property("CreatedDate").CurrentValue = DateTime.UtcNow;
            entry.Property("CreatedBy").CurrentValue = currentUser;
        }
        
        entry.Property("ModifiedDate").CurrentValue = DateTime.UtcNow;
        entry.Property("ModifiedBy").CurrentValue = currentUser;
    }
    
    return base.SaveChanges();
}

6. Soft Delete with Shadow Property:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    // Add IsDeleted shadow property to all entities
    foreach (var entityType in modelBuilder.Model.GetEntityTypes())
    {
        modelBuilder.Entity(entityType.ClrType)
            .Property("IsDeleted")
            .HasDefaultValue(false);
            
        // Global query filter
        var parameter = Expression.Parameter(entityType.ClrType, "e");
        var property = Expression.Property(parameter, "IsDeleted");
        var filter = Expression.Lambda(
            Expression.Equal(property, Expression.Constant(false)),
            parameter);
            
        modelBuilder.Entity(entityType.ClrType).HasQueryFilter(filter);
    }
}

// Soft delete implementation
public void SoftDelete(T entity) where T : class
{
    context.Entry(entity).Property("IsDeleted").CurrentValue = true;
}

// Usage
var product = context.Products.Find(1);
SoftDelete(product);
context.SaveChanges();

// Query automatically filters soft-deleted records
var products = context.Products.ToList(); // Only non-deleted

// Include soft-deleted records
var allProducts = context.Products
    .IgnoreQueryFilters()
    .ToList();

7. Indexing Shadow Properties:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity()
        .Property("CreatedDate");
        
    modelBuilder.Entity()
        .HasIndex("CreatedDate")
        .HasDatabaseName("IX_Product_CreatedDate");
}

8. Shadow Properties vs Regular Properties:

AspectShadow PropertiesRegular Properties
In Entity ClassNoYes
Type SafetyRuntime onlyCompile-time
IntelliSenseNoYes
AccessEF.Property()Direct access
RefactoringManualAutomatic
Use CaseInfrastructure concernsDomain model

Advantages:

  • Keep domain model clean
  • Separate infrastructure concerns
  • Database-specific features
  • Flexible schema management
  • Audit without polluting model

Disadvantages:

  • No compile-time safety
  • No IntelliSense support
  • String-based access (typo-prone)
  • Less discoverable
  • More complex queries

Best Practices:

  • Use for infrastructure concerns only
  • Document shadow properties clearly
  • Create helper methods for common access patterns
  • Consider using interfaces for better discoverability
  • Use constants for property names to avoid typos
// Good practice: Use constants
public static class ShadowProperties
{
    public const string CreatedDate = nameof(CreatedDate);
    public const string ModifiedDate = nameof(ModifiedDate);
    public const string IsDeleted = nameof(IsDeleted);
}

// Usage
var createdDate = context.Entry(product)
    .Property(ShadowProperties.CreatedDate)
    .CurrentValue;

Summary

Entity Framework Core provides a powerful and flexible ORM solution for .NET applications. Key takeaways:

  1. EF Core vs EF6: Cross-platform, better performance, modern features
  2. Code First vs Database First: Choose based on project requirements
  3. Loading Strategies: Eager, lazy, and explicit loading - each with specific use cases
  4. Migrations: Track and manage database schema changes
  5. Patterns: Repository and Unit of Work for better architecture
  6. N+1 Problem: Use eager loading and projections to avoid performance issues
  7. Query Optimization: AsNoTracking, projections, compiled queries, and proper indexing
  8. Tracking: Understand when to use tracking vs no-tracking queries
  9. Owned Entities: Model value objects effectively
  10. Concurrency: Use optimistic concurrency control with RowVersion
  11. Async Operations: Always use async in web applications for scalability
  12. Shadow Properties: Keep infrastructure concerns separate from domain model

Master these concepts to build efficient, maintainable, and scalable applications with Entity Framework Core!