Explore Shadow Properties in EF Core

Introduction

In the previous post, we discovered Query Filters, a feature for EF Core 2.0 and later. Now I want to take a look at another popular feature, Shadow Properties. When designing applications we tend to keep our code clean and simple, however there are times that you need to add properties other than what is required in your main business use cases, CreatedOn and LastUpdatedOn are such well-known properties.

Our code will look like the next:

public class Author
{
  // omitted properties

  public DateTimeOffset CreatedOn { get; set; }
  public DateTimeOffset? LastUpdatedOn { get; set; }
}

Shadow Properties

This is no good since we are eager to have our domain models reflect business models, this is where EF Core shadow properties come handy. Configuring shadow properties is so simple, in the OnModelCreating method of your DbContext (alternatively you could use IEntityTypeConfiguration<T>) :

builder.Property<DateTimeOffset>(“CreatedOn”)
builder.Property<DateTimeOffset>(“LastUpdatedOn”);

and then remove the actual properties from the underlying class. To access and set the values of shadow properties, you could use the change tracker or static method Property of EF class.

context.Entry(author).Property("CreatedOn").CurrentValue = DateTime.Now;
// or
var authors = context.Authors.OrderBy(
     a => EF.Property<DateTime>(a, "LastUpdatedOn"));

Now we need a mechanism to tell the DbContext to automatically set values of these properties based on the state of the entity, but before that let me say that Shadow Properties come with a limitation, they are accessible only through the Change Tracker API, that means if they are loaded outside the DbContext or with AsNoTracking in DbContext, the values of these properties are not accessible.

Well, so far so good, let's make our DbContext more smart with these values so that we don’t need to be worry about when and how to set these values when coding our business rules in the application. That, could be achieved by overriding the SaveChanges and SaveChangesAsync methods of DbContext and set these properties for the desired entities. To do so, I create an interface, IHaveTrackingDates but unlike ICanBeSofDeleted this is just a flag interface.

public interface IHaveTrackingDates { }

public class Author : ICanBeSoftDeleted , IHaveTrackingDates
{
 // omitted code
}

And then call the following method in the OnModelCreating method of the DbContext and remove the previous config:

public static void AddTrackingDatesShadowProperties(this ModelBuilder modelBuilder)
{
    var trackingDatesEntities =
                typeof(IHaveTrackingDates)
                .Assembly.GetTypes()
                    .Where(
                        type => typeof(IHaveTrackingDates)
                                    .IsAssignableFrom(type)
                                && type.IsClass
                                && !type.IsAbstract);

    foreach (var entity in trackingDatesEntities)
    {
        modelBuilder.Entity(entity)
            .Property<DateTimeOffset>("CreatedOn")
            .IsRequired();
        modelBuilder.Entity(entity)
            .Property<DateTimeOffset?>("LastUpdatedOn")
            .IsRequired(false)
            .HasColumnName("LastUpdatedAt");
    }
}

Last, make your DbContext is smart enough by telling it how to detect these entities and set their properties, call this method in the overridden SaveChanges methods.


private void UpdateTrackingDates()
{
    var trackingDatesChangedEntries = this.ChangeTracker.Entries()
        .Where(entry => entry.State == EntityState.Added
                    || entry.State == EntityState.Modified
            && entry.Entity is IHaveTrackingDates
        );

    foreach (var entry in trackingDatesChangedEntries)
    {
        if (entry.State == EntityState.Modified)
            this.Entry(entry).Property("LastUpdatedOn")
                .CurrentValue = DateTimeOffset.UtcNow;
        else if (entry.State == EntityState.Added)
            this.Entry(entry).Property("CreatedOn")
                .CurrentValue = DateTimeOffset.UtcNow;
    }
}

public override int SaveChanges()
{
    this.UpdateTrackingDates();
    return base.SaveChanges();
}

public override Task<int> SaveChangesAsync(
    CancellationToken cancellationToken = new CancellationToken())
{
    this.UpdateTrackingDates();
    return base.SaveChangesAsync(cancellationToken);
}

Conclusion

Shadow properties are giving us an opportunity to clean up our domain models from codes that are not necessarily based on the business rules, as other features and technologies they have their own pros and cons we might consider when using them.

You could find the complete code of this post on GitHub. Now that we know how to handle shadow properties and also able to add global query filters, it would be a good practice to change the soft delete mechanism as a shadow property as well. Have a great day and enjoy coding!

Buy Me a Coffee at ko-fi.com

Create Syntax Trees using Roslyn APIs

Sometimes when we want to generate code whether it is source generators or code fixes for a code analyzer, it is required to know how a syntax tree could be created using the Roslyn Compiler API. There are two ways, that we will discuss them in this post.

Custom Configuration Providers in ASP.NET Core

A colleague of mine had a requirement in which he was interested to put the configuration settings of an ASP.NET Core application inside a SQL Server table and read them at startup of the application with a mechanism similar to what we already have for JSON files; also he wanted to have the ability that these variable being overridden or override other settings if applicable. In this article, I am going to describe how that could be achievable and how to implement such feature.

Explore Global Query Filters in EF Core

In this article we are going to check one of the features of Entity Framework Core, Global Query Filters; this was introduced first in EF Core 2.0 and is just a query predicate that will be appended to the end of where clause for queries on entities for which this feature has been activated.

An error has occurred. This application may no longer respond until reloaded. Reload x