Entity Framework Core shadow properties (C#)
Introduction
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. The value and state of these properties is maintained purely in the Change Tracker. Shadow properties are useful when there is data in the database that should not be exposed on the mapped entity types. Code samples presented will provided simple, useful examples for using shadow properties using a class project for Entity Framework Core code logic with a Windows form project to present data from a SQL-Server database,
Configuring shadow properties
Using the two tables, shadow properties will be LastUpdated, LastUser, CreatedBy, CreatedAt and IsDeleted.
Open the dbContext class for a project, traverse to OnModelCreating and add the following for each column to be defined as a shadow property.
modelBuilder.Entity<T>().Property<Type>("PropertyName");
For the properties mentioned earlier this is what's needed to define those properties as shadow properties in OnModelCreating method.
modelBuilder.Entity<Contact1>().Property<DateTime?>("LastUpdated");
modelBuilder.Entity<Contact1>().Property<string>("LastUser");
modelBuilder.Entity<Contact1>().Property<DateTime?>("CreatedAt");
modelBuilder.Entity<Contact1>().Property<string>("CreatedBy");
modelBuilder.Entity<Contact1>().Property<bool>("isDeleted");
Since information read, added, edited and removed will need to be seen in a DataGridView and several TextBoxes the model implements INotificationPropertyChanged so the model shown below has been modified after reverse engineering the database table. The properties in the region only are needed if those properties will be displayed in the user interface and if not they can be removed from the model.
using System;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations.Schema;
using System.Runtime.CompilerServices;
namespace Backend.Models
{
[Table("Contact1")]
public partial class Contact1 : INotifyPropertyChanged
{
private int _contactId;
private string _firstName;
private string _lastName;
private DateTime? _lastUpdated;
private string _lastUser;
private DateTime? _createdAt;
private string _createdBy;
public int ContactId
{
get => _contactId;
set
{
_contactId = value;
OnPropertyChanged();
}
}
public string FirstName
{
get => _firstName;
set
{
_firstName = value;
OnPropertyChanged();
}
}
public string LastName
{
get => _lastName;
set
{
_lastName = value;
OnPropertyChanged();
}
}
#region Not needed for shadow properties only for displaying in DataGridView
public DateTime? LastUpdated
{
get => _lastUpdated;
set
{
_lastUpdated = value;
OnPropertyChanged();
}
}
public string LastUser
{
get => _lastUser;
set
{
_lastUser = value;
OnPropertyChanged();
}
}
public DateTime? CreatedAt
{
get => _createdAt;
set
{
_createdAt = value;
OnPropertyChanged();
}
}
public string CreatedBy
{
get => _createdBy;
set
{
_createdBy = value;
OnPropertyChanged();
}
}
#endregion
public override string ToString() => $"{FirstName} {LastName}";
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
}
Setting shadow property values
Shadow property values can be set in a method prior to executing SaveChanges or SaveChangesAsync. The following overridden SaveChanges, first a scan is done for tracked changes using ChangeTracker.DetectChanges.
Those entities which have change are looped through with a conditional for add/modified and deleted. If added or modified the LastUpdated and LastUser property values are set then an additional if for added records which will set the CreatedAt and CreatedBy properties. For records marked for removal their state is changed to Modified and the IsDeleted property is set to true. Note for records marked IsDeleted = true the following filters deleted records. Note that soft deletes to work properly outside of Entity Framework core code those who work with this data need to understand the IsDeleted property.
modelBuilder.Entity<Contact1>()
.HasQueryFilter(m =>
EF.Property<bool>(m, "isDeleted") == false);
Once the above steps have been followed a developer can forget about the columns setup as shadow properties with a caution, if the model property changes in the underlying database table make sure to update code which touches the columns.
Take time to read the Microsoft documentation for in depth details on other aspects of shadow properties.
Front end code samples
In a Windows Form project there are two forms, ContactForm which works solely with adding/modifying data while taking advantage of proper data binding to immediately reflect changes while Contact1Form tacks on removal of records.
When a shadow property are not defined in the model the following shows how to obtain their values. In general the main place to use these values in in a filter or within SSMS.
var contact = _bindingListContacts[dataGridView1.CurrentRow.Index];
var lastUser = (string)_context.Entry(contact).Property("LastUser").CurrentValue;
var lastUpdated = (DateTime?)_context.Entry(contact).Property("LastUpdated").CurrentValue;
var creationDate = (DateTime?)_context.Entry(contact).Property("CreatedAt").CurrentValue;
Summary
Shadow properties provide options to work with properties not defined in a model were in some cases using A column in the database table to have a default value may not work according to business requirement. Best to seek a database solution then if this fails to meet business requirements consider shadow properties.
There are two cases for using shadow properties
- When foreign key properties are need for relations without exposing them to the public eye for security reasons.
- For columns which are handled by the backend e.g. setting last changed.
See also
- Entity Framework Core HasValueGenerator (C#)
- Entity Framework Core/Windows Forms tips and tricks
- Entity Framework Core 3.x Global Query Filters (C#)
- Windows Forms: Entity Framework Core Reverse Engineering databases
- .NET Core: Secure your web applications using IdentityServer 4
- Entity Framework Windows Form validation
External resources
- Porting from Entity Framework 6 to Entity Framework Core
- Entity Framework Core API Reference
- Installing Entity Framework Core
- EF Core Tools and Extensions
Source code
See the following GitHub repository which includes full source code, database creation scripts. Before building the solution first perform NuGet restore all packages.