Isaac.

database

EF Core Shadow Properties

Use shadow properties in Entity Framework Core.

By Emem IsaacDecember 30, 20222 min read
#entity framework#shadow properties#configuration#tracking
Share:

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

  1. Use for audit: CreatedAt, ModifiedAt
  2. Keep simple: Avoid complex logic
  3. Document clearly: Shadow properties are hidden
  4. Test queries: Ensure they work
  5. 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.

Share:

Written by Emem Isaac

Expert Software Engineer with 15+ years of experience building scalable enterprise applications. Specialized in ASP.NET Core, Azure, Docker, and modern web development. Passionate about sharing knowledge and helping developers grow.

Ready to Build Something Amazing?

Let's discuss your project and explore how my expertise can help you achieve your goals. Free consultation available.

💼 Trusted by 50+ companies worldwide | ⚡ Average response time: 24 hours