Entity Framework Core is the most commonly used ORM in .NET Core applications. It is a lightweight, extensible, open-source, and cross-platform version of the popular Entity Framework data access technology. One of the features in EF Core is Shadow Properties. In this article, we shall look into Shadow Properties in EF Core.

Shadow properties are properties that are not defined in your .NET entity class but are defined for that entity type in the EF Core model. This means that we can have columns for these properties in our tables, but we will not have a corresponding property field in our entity class. This is really helpful while designing our domain models as this helps to avoid unnecessary fields from the domain models that don't add any value to our domain logic but are required in our database.

One of the most common usages I see for Shadow Properties is audit fields that we commonly use in our domain models. We usually use fields like CreatedOn, LastModifiedOn, etc in our entities to store audit information.

These fields really don't provide much value to our domain but are actually useful. So removing these fields from our entities will make our domain model cleaner and by using Shadow Properties we can make these columns in the database too.

We shall be creating a .NET Core Web API project to learn Shadow Properties. I shall be using .NET CLI and Visual Studio Code for development. But the same can be developed using Visual Studio.

namespace EFCoreShadowProperty.Models
{
    [Auditable]
    public class Employee
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public int Age { get; set; }
    }
}

Add another class Department,

namespace EFCoreShadowProperty.Models
{
    [Auditable]
    public class Department
    {
        public int Id { get; set; }
        public string Name { get; set; }
    }
}

You may notice that I have decorated both classes with a custom attribute named [Auditable]. This attribute will be used by the EF Core DBContext to add our shadow properties while creating the database. We will be overriding the OnModelCreating method of DBContext to add the fields CreatedOn and LastModifiedOn on the entities that are decorated with this attribute. We will also override the SaveChanges method of DBContext to add the current timestamp values on these fields if the entity is decorated with this attribute. The implementation of AuditableAttribute is given below.

using System;

namespace EFCoreShadowProperty.Models
{
    [AttributeUsage(AttributeTargets.Class)]
    public class AuditableAttribute : Attribute
    {
    }
}

Now, let's add our DBContext class. Create a class named EmployeeContext and add the following code.

using System;
using System.Linq;
using System.Reflection;
using EFCoreShadowProperty.Models;
using JetBrains.Annotations;
using Microsoft.EntityFrameworkCore;

namespace EFCoreShadowProperty
{
    public class EmployeeContext : DbContext
    {
        public DbSet<Employee> Employees { get; set; }
        public DbSet<Department> Departments { get; set; }

        public EmployeeContext(DbContextOptions options) : base(options) { }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            foreach (var entityType in modelBuilder.Model.GetEntityTypes())
            {
                if (entityType.ClrType.GetCustomAttributes(typeof(AuditableAttribute), true).Length > 0)
                {
                    modelBuilder.Entity(entityType.Name).Property<DateTime>("CreatedOn");
                    modelBuilder.Entity(entityType.Name).Property<DateTime>("LastModifiedOn");
                }
            }

            base.OnModelCreating(modelBuilder);
        }

        public override int SaveChanges()
        {
            ChangeTracker.DetectChanges();
            var timestamp = DateTime.Now;

            foreach (var entry in ChangeTracker.Entries()
                                .Where(e => e.State == EntityState.Added || e.State == EntityState.Modified))
            {
                if (entry.Entity.GetType().GetCustomAttributes(typeof(AuditableAttribute), true).Length > 0)
                {
                    entry.Property("LastModifiedOn").CurrentValue = timestamp;

                    if (entry.State == EntityState.Added)
                    {
                        entry.Property("CreatedOn").CurrentValue = timestamp;
                    }
                }
            }
            return base.SaveChanges();
        }
    }
}

Now, let's create a Web API to wire up the database we created. Add the following code in the ConfigureServices method in the Startup.cs class. Add your connection string in the appsettings.json file.

services.AddDbContext<EmployeeContext>(opts =>
{
    opts.UseSqlServer(Configuration["ConnectionStrings:DefaultConnection"]);
});

Add a new controller class named EmployeesController. Add the following code to the controller.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using EFCoreShadowProperty.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;

namespace EFCoreShadowProperty.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class EmployeesController : ControllerBase
    {
        private readonly EmployeeContext _employeeContext;

        public EmployeesController(EmployeeContext employeeContext)
        {
            this._employeeContext = employeeContext;
        }

        [HttpGet]
        [Route("{id}", Name = "Get")]
        public ActionResult<string> Get(int id)
        {
            var employee = _employeeContext.Employees
                .Where(e => e.Id == id)
                .Select(e => new
                {
                    Id = e.Id,
                    Age = e.Age,
                    Name = e.Name,
                    CreatedOn = EF.Property<DateTime>(e, "CreatedOn"),
                    LastModifiedOn = EF.Property<DateTime>(e, "LastModifiedOn")
                });

            return Ok(employee);
        }

        [HttpPost]
        public ActionResult Post([FromBody] Employee employee)
        {
            _employeeContext.Employees.Add(employee);
            _employeeContext.SaveChanges();
            return CreatedAtRoute("Get", new { id = employee.Id }, employee);
        }

        [HttpPut("{id}")]
        public ActionResult Put(int id, [FromBody] Employee employee)
        {
            var employeeFromDb = _employeeContext.Employees.SingleOrDefault(e => e.Id == id);
            employeeFromDb.Age = employee.Age;
            employeeFromDb.Name = employee.Name;
            _employeeContext.SaveChanges();
            return Ok();
        }
    }
}

The controller class contains three actions. The Post method will accept an Employee object and save it in the database. Since we have overridden the SaveChanges method to set the Shadow Properties with the current timestamp this API will set those values in the database. The Put method accepts an id and an employee object as the parameter. This method will update the existing employee record in the database corresponding to the ID with the input values. In this case, only the LastModifiedOn shadow property will be updated.

The Get method fetches the employee record from the database corresponding to the ID supplied. You can see that I have used the static Property method of the EF utility class to fetch the Shadow Property values in the query.

var employee = _employeeContext.Employees
                    .Where(e => e.Id == id)
                    .Select(e => new
                    {
                        Id = e.Id,
                        Age = e.Age,
                        Name = e.Name,
                        CreatedOn = EF.Property<DateTime>(e, "CreatedOn"), // Querying Shadow Property
                        LastModifiedOn = EF.Property<DateTime>(e, "LastModifiedOn") // Querying Shadow Property
                    });

Summary

In this article, we looked at Shadow Properties in EF Core. We have also developed a simple Web API application that uses Shadow Properties for storing the audit field values in the database.