EF Core Shadow Properties
Use shadow properties in Entity Framework Core.
By EMEPublished: February 20, 2025
entity frameworkshadow propertiesconfigurationtracking
A Simple Analogy
Shadow properties are like secret attributes. They exist in the database but not in your C# class.
Why Shadow Properties?
- Clean entities: Keep DTOs simple
- Audit tracking: Add CreatedAt, ModifiedBy
- Infrastructure: Don't pollute domain
- Flexibility: Change without class changes
- Relationships: Foreign keys optional
Defining Shadow Properties
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<User>()
.Property<DateTime>("CreatedAt")
.HasDefaultValueSql("GETUTCDATE()");
modelBuilder.Entity<User>()
.Property<DateTime?>("ModifiedAt");
modelBuilder.Entity<User>()
.Property<string>("ModifiedBy")
.HasMaxLength(256);
}
public class User
{
public int Id { get; set; }
public string Name { get; set; }
// CreatedAt, ModifiedAt, ModifiedBy are shadow properties
}
Accessing Shadow Properties
var user = new User { Name = "Alice" };
context.Users.Add(user);
// Set shadow property via Entry
context.Entry(user).Property("CreatedAt").CurrentValue = DateTime.UtcNow;
context.Entry(user).Property("ModifiedBy").CurrentValue = "admin";
await context.SaveChangesAsync();
// Read shadow property
var createdAt = context.Entry(user).Property("CreatedAt").CurrentValue;
Console.WriteLine($"User created at: {createdAt}");
Querying with Shadow Properties
// Include shadow properties in queries
var users = await context.Users
.Select(u => new
{
u.Id,
u.Name,
CreatedAt = EF.Property<DateTime>(u, "CreatedAt"),
ModifiedBy = EF.Property<string>(u, "ModifiedBy")
})
.ToListAsync();
foreach (var user in users)
{
Console.WriteLine($"{user.Name} created at {user.CreatedAt} by {user.ModifiedBy}");
}
Interceptor Pattern
public class AuditInterceptor : SaveChangesInterceptor
{
public override ValueTask<InterceptionResult<int>> SavingChangesAsync(
DbContextEventData eventData,
InterceptionResult<int> result,
CancellationToken ct = default)
{
var context = eventData.Context;
foreach (var entry in context.ChangeTracker.Entries())
{
if (entry.State == EntityState.Added)
{
entry.Property("CreatedAt").CurrentValue = DateTime.UtcNow;
entry.Property("CreatedBy").CurrentValue = GetCurrentUser();
}
if (entry.State == EntityState.Modified)
{
entry.Property("ModifiedAt").CurrentValue = DateTime.UtcNow;
entry.Property("ModifiedBy").CurrentValue = GetCurrentUser();
}
}
return base.SavingChangesAsync(eventData, result, ct);
}
}
// Register
builder.Services.AddDbContext<AppContext>(options =>
options.AddInterceptors(new AuditInterceptor()));
Best Practices
- Use for audit: CreatedAt, ModifiedAt
- Keep simple: Avoid complex logic
- Document clearly: Shadow properties are hidden
- Test queries: Ensure they work
- Consider visibility: When to use vs properties
Related Concepts
- Entity auditing
- Change tracking
- Database computed columns
- Interceptors
Summary
Shadow properties add columns to the database without adding properties to your C# classes. Useful for audit fields and infrastructure concerns.