Shadow Properties In Entity Framework Core

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.

  • Create a folder named EFCoreShadowProperty and open VS Code from this directory.
  • Open the terminal and run the command - dotet new webapi --name EFCoreShadowProperty. This shall create a Web API project.
  • Add a new folder called Models. Let's add some model classes in this folder.
  • Add a class called Employee.
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();
        }
    }
}
  • In the DBContext class, we are overriding both the OnModelCreating and SaveChangesmethods.
  • In the OnModelCreating method, we loop through all the entities registered in the DBContext and use reflection to check whether the entity is applied with Auditableattribute and if it is applied then add the properties CreatedOn and LastModifiedOn to that entity.
  • We use a similar approach to the SaveChanges method. Here, we will loop through all the entries in the ChangeTracker and use reflection to check whether the entity is applied with the Auditable attribute and if it is applied, we set the current timestamp to LastModifiedOnproperty. If the state is EntityState.Added, we set the CreatedOn property with the current timestamp.
  • Now, let's add the migration. Open the terminal and run the following command - dotnet ef migrations add AuditFieldShadowProperty
  • After the migrations are created run the following command in the terminal - dotnet ef database update. This shall create the database for you with the tables.

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.


Similar Articles