Entity Framework Core Events and Diagnostic Listeners are very helpful in debugging our EF Core code. Events are called when something happens in EF Core codes. For example DbContext.SaveChangesFailed event is called when SaveChanges or SaveChangesAsync method is failed so we can use this event to find out the cause of the failure. Diagnostic listeners allow listening for any EF Core event for obtaining diagnostic information of the app. In this tutorial we are going to implement each of these 2 in our code.
Some of the important events raised by EF core are given below.
| Event | Description |
|---|---|
| DbContext.SavingChanges | At the start of SaveChanges or SaveChangesAsync |
| DbContext.SavedChanges | At the end of a successful SaveChanges or SaveChangesAsync |
| DbContext.SaveChangesFailed | At the end of a failed SaveChanges or SaveChangesAsync |
| ChangeTracker.Tracked | When an entity is tracked by the context |
| ChangeTracker.StateChanged | When a tracked entity changes its state |
The EF Core ChangeTracker.StateChanged event is called when a tracked entity changes it’s state. We will create this event to find out the time when an entity changes state.
First of all define an interface called “IHasTimestamps.cs” that defines 3 properties – Added, Deleted & Modified.
public interface IHasTimestamps
{
DateTime? Added { get; set; }
DateTime? Deleted { get; set; }
DateTime? Modified { get; set; }
}
Next, add the extensions method called “HasTimestampsExtensions.cs” for the interface. It contains a method called “ToStampString” which will return the timestamp of the entity when it is Added, Modified or Deleted.
public static class HasTimestampsExtensions
{
public static string ToStampString(this IHasTimestamps entity)
{
return $"{GetStamp("Added", entity.Added)}{GetStamp("Modified", entity.Modified)}{GetStamp("Deleted", entity.Deleted)}";
}
static string GetStamp(string state, DateTime? dateTime) => dateTime == null ? "" : $" {state} on: {dateTime}";
}
In our entity class “Employee.cs” we inherit this interface and call the ToStampString method on the override of the ToString method.
public class Employee : IHasTimestamps
{
public int Id { get; set; }
public string Name { get; set; }
public string Company { get; set; }
public string Designation { get; set; }
[NotMapped]
public DateTime? Added { get; set; }
[NotMapped]
public DateTime? Deleted { get; set; }
[NotMapped]
public DateTime? Modified { get; set; }
public override string ToString() => $"Employee {Id}{this.ToStampString()}";
}
Finally on the application’s DbContext constructor, we define the StateChanged and Tracked events to call the handler named UpdateTimestamps.
public CompanyContext(DbContextOptions<CompanyContext> options) : base(options)
{
ChangeTracker.StateChanged += UpdateTimestamps;
ChangeTracker.Tracked += UpdateTimestamps;
}
In the DbContext define the UpdateTimestamps handler as given below.
private static void UpdateTimestamps(object sender, EntityEntryEventArgs e)
{
if (e.Entry.Entity is IHasTimestamps entityWithTimestamps)
{
switch (e.Entry.State)
{
case EntityState.Deleted:
entityWithTimestamps.Deleted = DateTime.UtcNow;
Console.WriteLine($"Stamped for delete: {e.Entry.Entity}");
break;
case EntityState.Modified:
entityWithTimestamps.Modified = DateTime.UtcNow;
Console.WriteLine($"Stamped for update: {e.Entry.Entity}");
break;
case EntityState.Added:
entityWithTimestamps.Added = DateTime.UtcNow;
Console.WriteLine($"Stamped for insert: {e.Entry.Entity}");
break;
}
}
}
The work of this handler is to write to the console the time when the entities implementing IHasTimestamps interface are first tracked, added, modified and deleted.
On running the below insert code, we get the timestamp when the record is inserted.
context.Add(
new Employee
{
Name = "Rock",
Company = "WWE",
Designation = "Heavy Weight Champion"
});
context.SaveChanges();
On the console window we get the message.
Stamped for insert: Employee 0 Added on: 17-11-2025 12:53:50
Similarly the update code given below.
var e = context.Employee.FirstOrDefault();
e.Name = "Brock";
context.SaveChanges();
Will produce the following message on the console window.
Stamped for update: Employee 1 Modified on: 17-11-2025 13:00:26
The deletion of the employee.
context.Remove(context.Employee.Where(a => a.Name == "John").FirstOrDefault());
context.SaveChanges();
Will produce the following message on the console window.
Stamped for delete: Employee 7 Deleted on: 17-11-2025 13:03:13
Diagnostic Listeners allow listening for any EF Core event. Implementing of a Diagnostic Listener involves creating 2 observers:
The Diagnostic Observer is given below. It implements IObserver<DiagnosticListener> interface.
public class DiagnosticObserver : IObserver<DiagnosticListener>
{
public void OnCompleted()
=> throw new NotImplementedException();
public void OnError(Exception error)
=> throw new NotImplementedException();
public void OnNext(DiagnosticListener value)
{
if (value.Name == DbLoggerCategory.Name) // "Microsoft.EntityFrameworkCore"
{
value.Subscribe(new KeyValueObserver());
}
}
}
Now register this Observer in the Program.cs class of the app.
DiagnosticListener.AllListeners.Subscribe(new DiagnosticObserver());
The Observer created above looks for Diagnostic Listener named “Microsoft.EntityFrameworkCore” (check the code inside the OnNext() method). When Microsoft.EntityFrameworkCore Diagnostic Listener is found, a new key-value observer is created to subscribe to the actual EF Core events.
Next add the KeyValueObserver.cs class in the app.
public class KeyValueObserver : IObserver<KeyValuePair<string, object>>
{
public void OnCompleted()
=> throw new NotImplementedException();
public void OnError(Exception error)
=> throw new NotImplementedException();
public void OnNext(KeyValuePair<string, object> value)
{
if (value.Key == CoreEventId.ContextInitialized.Name)
{
var payload = (ContextInitializedEventData)value.Value;
Console.WriteLine($"Entity Framework Core is initializing {payload.Context.GetType().Name} ");
}
if (value.Key == RelationalEventId.ConnectionOpening.Name)
{
var payload = (ConnectionEventData)value.Value;
Console.WriteLine($"Entity Framework Core is opening a connection to {payload.Connection.ConnectionString} ");
}
}
}
In the above code, inside the OnNext method, each EF Core event is caught with a key/value pair. The key is the name of the event, which can be obtained from CoreEventId, RelationalEventId or SqlServerEventId. The value is a payload type specific to the event.
The above code above handles the ContextInitialized and the ConnectionOpening EF Core events. For ContextInitialized, the payload is ContextInitializedEventData and for ConnectionOpening, the payload is ConnectionEventData.
We now run the below EF Core create and remove records code.
context.Add(
new Employee
{
Name = "Rock",
Company = "WWE",
Designation = "Heavy Weight Champion"
});
context.Remove(context.Employee.Where(a => a.Name == "John").FirstOrDefault());
context.SaveChanges();
In the console window we will find the messages given from the Diagnostic Listerner.
Entity Framework Core is initializing CompanyContext
Entity Framework Core is opening a connection to Data Source=(localdb)\MSSQLLocalDB;Initial Catalog=EFCoreLogging;Integrated Security=True;Connect Timeout=30;Encrypt=False;TrustServerCertificate=False;ApplicationIntent=ReadWrite;MultiSubnetFailover=False
Entity Framework Core is initializing CompanyContext
Entity Framework Core is opening a connection to Data Source=(localdb)\MSSQLLocalDB;Initial Catalog=EFCoreLogging;Integrated Security=True;Connect Timeout=30;Encrypt=False;TrustServerCertificate=False;ApplicationIntent=ReadWrite;MultiSubnetFailover=False