Entity Framework Core Change Tracker keeps track of all the loaded entities changes and these changes are applied to the database when SaveChanges method is called.
Entities are tracked on the following conditions :
An Entity is stopped tracking when :
An entity can have 5 following states which are described by the EntityState enum:
We will read an entity along with it’s related entity and then perform some changes to them. Here the same DbContext instance is used to both query for entities and update them by calling SaveChanges. This approach works best because EF Core automatically tracks the state of queried entities and then detects any changes made to these entities when SaveChanges is called.
The entities are given below.
public class Department
{
public int Id { get; set; }
public string Name { get; set; }
public IList<Employee> Employees { get; } = new List<Employee>();
}
public class Employee
{
public int Id { get; set; }
public string Name { get; set; }
public string Designation { get; set; }
public int DepartmentId { get; set; }
public Department Department { get; set; } = null!;
}
We query the entity by the name of “Development” along with all it’s “Employees”. We then change the department name to .NET Development and designation of “Junior” employees to “Trainee”. See the below code.
var dept = context.Department.Include(e => e.Employees).First(e => e.Name == "Development");
dept.Name = ".NET Development";
foreach (var emp in dept.Employees.Where(e => e.Designation.Contains("Junior")))
{
emp.Designation = emp.Designation.Replace("Junior", "Trainee");
}
// code for Change Tracker Debug View
context.ChangeTracker.DetectChanges();
Console.WriteLine(context.ChangeTracker.DebugView.LongView);
context.SaveChanges();
Note that we added the code for Change Tracker Debug View just before the SaveChanges. It offers a great way to see detailed information in the console window of the entities being tracked along with the messages when the change tracker detects state and fixes up relationships of the entities. This code is:
context.ChangeTracker.DetectChanges();
Console.WriteLine(context.ChangeTracker.DebugView.LongView);
The Department table has only 1 record and is shown below.
Id | Name |
---|---|
1 | Development |
The Employee table has 3 records as shown below.
Id | Name | Designation | DepartmentId |
---|---|---|---|
1 | John | Junior | 1 |
2 | Rahul | Manager | 1 |
3 | Alice | Lead | 1 |
When the SaveChanges method is called to Update the database, the 2 Update commands executes – one for the Department table and other for the Employee table. You can check them on the console window.
UPDATE [Department] SET [Name] = @p0
OUTPUT 1
WHERE [Id] = @p1;
UPDATE [Employee] SET [Designation] = @p2
OUTPUT 1
WHERE [Id] = @p3;
Also check the output of the Change Tracker Debug View on the console window to find the entities that are tracked and their states. Check the below image:
Notice the messages.
In this example we will see how change tracking works for insert, update and delete functionalities. We will execute the following code.
var dept = context.Department.Include(e => e.Employees).First(e => e.Name == "Development");
// Modify property values
dept.Name = ".NET Development";
// Insert a new Employee
dept.Employees.Add(
new Employee
{
Name = "Rock",
Designation = "VP",
DepartmentId = 1
});
// Mark an existing Employee as Deleted
var d = dept.Employees.Single(e => e.Designation == "Lead");
context.Remove(d);
context.ChangeTracker.DetectChanges();
Console.WriteLine(context.ChangeTracker.DebugView.LongView);
context.SaveChanges();
The above code does 3 things – updates the Department name, inserts a new Employee and deletes another Employee. When the SaveChanges method is called the following commands are executed which can be seen on the console window.
UPDATE [Department] SET [Name] = @p0
OUTPUT 1
WHERE [Id] = @p1;
DELETE FROM [Employee]
OUTPUT 1
WHERE [Id] = @p2;
INSERT INTO [Employee] ([DepartmentId], [Designation], [Name])
OUTPUT INSERTED.[Id]
VALUES (@p3, @p4, @p5);
Also check the output of the Change Tracker Debug View on the console window which is shown below.
Department {Id: 1} Modified
Id: 1 PK
Name: '.NET Development' Modified Originally 'Development'
Employees: [{Id: 1}, {Id: 2}, {Id: 3}, {Id: -2147482647}]
Employee {Id: -2147482647} Added
Id: -2147482647 PK Temporary
DepartmentId: 1 FK
Designation: 'VP'
Name: 'Rock'
Department: {Id: 1}
Employee {Id: 1} Unchanged
Id: 1 PK
DepartmentId: 1 FK
Designation: 'Junior'
Name: 'John'
Department: {Id: 1}
Employee {Id: 2} Unchanged
Id: 2 PK
DepartmentId: 1 FK
Designation: 'Manager'
Name: 'Rahul'
Department: {Id: 1}
Employee {Id: 3} Deleted
Id: 3 PK
DepartmentId: 1 FK
Designation: 'Lead'
Name: 'Alice'
Department: {Id: 1}
The messages states.
The 2 tables are updated as shown below.
The Department table.
Id | Name |
---|---|
1 | .NET Development |
The Employee table.
Id | Name | Designation | DepartmentId |
---|---|---|---|
1 | John | Trainee | 1 |
2 | Rahul | Manager | 1 |
4 | Rock | VP | 1 |
In this case we will see how change tracker works when entities are attached explicitly to a DbContext for tracking or when re-attaching entities that were queried from a different DbContext.
Inserting a new Entity is done with Add, AddRange, AddAsync, AddRangeAsync methods of EF Core. An entity must be tracked in the Added state to be inserted by SaveChanges. Let us insert a new deparment to the database. Check the below code.
context.Add(new Department { Name = "Testing" });
context.ChangeTracker.DetectChanges();
Console.WriteLine(context.ChangeTracker.DebugView.LongView);
context.SaveChanges();
Console.WriteLine(context.ChangeTracker.DebugView.LongView);
In the above code we added the change tracker debug view code at 2 places – before and after the SaveChanges method.
The message given by change tracker debug view is given below.
Department {Id: -2147482647} Added
Id: -2147482647 PK Temporary
Name: 'Testing'
Employees: []
Department {Id: 2} Unchanged
Id: 2 PK
Name: 'Testing'
Employees: []
First message shows that the context is tracking the new entity in the Added state. The second message shows that the entity is tracked in the Unchanged state after SaveChanges completes, since the entity now exists in the database:
Notice in the first message – Id: -2147482647 PK Temporary, this is a temporary key value that has been generated for the entity. This value is used by EF Core until SaveChanges is called, at which point real key values are read back from the database i.e. Id: 2 PK.
We can also insert entity along with it’s related entity using the Add method like shown below.
context.Add(
new Department
{
Name = "Testing",
Employees =
{
new Employee
{
Name = "Sharapova",
Designation = "VP",
}
}
});
Entities returned from queries are tracked in the Unchanged state, it means the entity has not been modified since it was queried. We know such entities as Disconnected entities. Example – a disconnected entity is returned from a web client in an HTTP request. To start tracking a disconnected entity we us EF Core Attach or AttachRange methods. In the below code we are inserting a department entity along with it’s related employee records using the attach method.
context.Attach(
new Department
{
Name = "Testing",
Employees =
{
new Employee
{
Name = "Ronaldo",
Designation = "MD"
}
}
});
context.ChangeTracker.DetectChanges();
Console.WriteLine(context.ChangeTracker.DebugView.LongView);
context.SaveChanges();
In this case the change tracker debug view code gave the following messages.
Department {Id: -2147482647} Added
Id: -2147482647 PK Temporary
Name: 'Testing'
Employees: [{Id: -2147482647}]
Employee {Id: -2147482647} Added
Id: -2147482647 PK Temporary
DepartmentId: -2147482647 FK Temporary
Designation: 'VP'
Name: 'Sharapova'
Department: {Id: -2147482647}
The tracker tracks the department entity in the Added state. The new Employee is also marked in the Added state. Once the SaveChanges method is called the new department and the new employee is added to the database.
The Update and UpdateRange methods of EF Core put the entity in the Modified state so that when SaveChanges method is called the entity is updated on the database.
In the below code the department 2 name is updated to “Testing”.
context.Update(new Department { Id = 2, Name = "Testing" });
context.ChangeTracker.DetectChanges();
Console.WriteLine(context.ChangeTracker.DebugView.LongView);
context.SaveChanges();
Inspecting the change tracker debug view messages shows that the context is tracking this entity in the Modified state.
Department {Id: 2} Modified
Id: 2 PK
Name: 'Testing' Modified
Employees: []
We can also update the entity along with it’s related entity as shown in the below code.
context.Update(
new Department
{
Id = 1,
Name = ".NET Development",
Employees =
{
new Employee
{
Name = "Rock",
Designation = "VP",
},
new Employee
{
Id = 1,
Name = "Sharapova",
Designation = "MD",
}
}
});
context.ChangeTracker.DetectChanges();
Console.WriteLine(context.ChangeTracker.DebugView.LongView);
context.SaveChanges();
Note that 2 related Employees are provided in the code. The Employee with no key value is detected as new and set to the Added state. The other entities are marked as Modified.
The change tracker debug view messages are given below.
Department {Id: 1} Modified
Id: 1 PK
Name: '.NET Development' Modified
Employees: [{Id: -2147482647}, {Id: 1}]
Employee {Id: -2147482647} Added
Id: -2147482647 PK Temporary
DepartmentId: 1 FK
Designation: 'VP'
Name: 'Rock'
Department: {Id: 1}
Employee {Id: 1} Modified
Id: 1 PK
DepartmentId: 1 FK Modified Originally 0
Designation: 'MD' Modified
Name: 'Sharapova' Modified
Department: {Id: 1}
The SaveChanges method causes updates for all the existing entities, while the new entity is inserted.
Executed DbCommand (54ms) [Parameters=[@p1='?' (DbType = Int32), @p0='?' (Size = 4000), @p5='?' (DbType = Int32), @p2='?' (DbType = Int32), @p3='?' (Size = 4000), @p4='?' (Size = 4000), @p6='?' (DbType = Int32), @p7='?' (Size = 4000), @p8='?' (Size = 4000)], CommandType='Text', CommandTimeout='30']
SET NOCOUNT ON;
UPDATE [Department] SET [Name] = @p0
OUTPUT 1
WHERE [Id] = @p1;
UPDATE [Employee] SET [DepartmentId] = @p2, [Designation] = @p3, [Name] = @p4
OUTPUT 1
WHERE [Id] = @p5;
INSERT INTO [Employee] ([DepartmentId], [Designation], [Name])
OUTPUT INSERTED.[Id]
VALUES (@p6, @p7, @p8);
The EF Core Remove and RemoveRange methods deletes the Entity from the database. These method make the Change Tracker marks the entity to be in Deleted state.
In the below code we are deleting Employee 1.
context.Remove(
new Employee { Id = 1 });
context.ChangeTracker.DetectChanges();
Console.WriteLine(context.ChangeTracker.DebugView.LongView);
context.SaveChanges();
Inspecting the change tracker debug view following this call shows that the context is tracking the Employee entity in the “Deleted” state:
Employee {Id: 1} Deleted
Id: 1 PK
DepartmentId: 0 FK
Designation: <null>
Name: <null>
Department: <null>
EF Core will execute the following SQL Command to delete this entity when SaveChanges method is called. You can see this command in the console window.
DELETE FROM [Employee]
OUTPUT 1
WHERE [Id] = @p0;
In the below code we first load Parent and it’s related entities i.e. Department 1 with it’s employees. We then marked it’s first employee to be deleted.
var dept = context.Department.Include(e => e.Employees).First(e => e.Name == "Development");
context.Remove(dept.Employees[0]);
context.ChangeTracker.DetectChanges();
Console.WriteLine(context.ChangeTracker.DebugView.LongView);
context.SaveChanges();
All entities are marked as “Unchanged”, except the Employee 1 which is marked as “Deleted” since the Remove method was called on it.
Department {Id: 1} Unchanged
Id: 1 PK
Name: 'Development'
Employees: [{Id: 1}, {Id: 2}, {Id: 3}]
Employee {Id: 1} Deleted
Id: 1 PK
DepartmentId: 1 FK
Designation: 'Trainee'
Name: 'John'
Department: {Id: 1}
Employee {Id: 2} Unchanged
Id: 2 PK
DepartmentId: 1 FK
Designation: 'Manager'
Name: 'Rahul'
Department: {Id: 1}
Employee {Id: 3} Unchanged
Id: 3 PK
DepartmentId: 1 FK
Designation: 'Lead'
Name: 'Alice'
Department: {Id: 1}
After SaveChanges completes, the deleted entity is detached from the DbContext since it no longer exists in the database. Other entities remain in the Unchanged state.
Department {Id: 1} Unchanged
Id: 1 PK
Name: 'Development'
Employees: [{Id: 2}, {Id: 3}]
Employee {Id: 2} Unchanged
Id: 2 PK
DepartmentId: 1 FK
Designation: 'Manager'
Name: 'Rahul'
Department: {Id: 1}
Employee {Id: 3} Unchanged
Id: 3 PK
DepartmentId: 1 FK
Designation: 'Lead'
Name: 'Alice'
Department: {Id: 1}
In the preceding examples we were deleting a dependent / child entity which is Employee. This is relatively straightforward since removal of a dependent/child entity does not have any impact on other entities. Now we will be deleting a principal / parent entity which is Department. Deleting principal entity leave a foreign key value on the child referencing a primary key value on the principla no longer exists. This is an invalid model state and results in a referential constraint error in most databases.
This invalid model state can be handled in two ways:
Here EF Core will delete the Child entities when the Parent is deleted. See the below code where we are deleting a Department.
var dept = context.Department.Include(e => e.Employees).First(e => e.Name == "Development");
context.Remove(dept);
context.ChangeTracker.DetectChanges();
Console.WriteLine(context.ChangeTracker.DebugView.LongView);
context.SaveChanges();
The change tracker debug view messages shows all the Employees of the department are also marked as Deleted.
Department {Id: 1} Deleted
Id: 1 PK
Name: 'Development'
Employees: [{Id: 1}, {Id: 2}, {Id: 3}]
Employee {Id: 1} Deleted
Id: 1 PK
DepartmentId: 1 FK
Designation: 'Trainee'
Name: 'John'
Department: {Id: 1}
Employee {Id: 2} Deleted
Id: 2 PK
DepartmentId: 1 FK
Designation: 'Manager'
Name: 'Rahul'
Department: {Id: 1}
Employee {Id: 3} Deleted
Id: 3 PK
DepartmentId: 1 FK
Designation: 'Lead'
Name: 'Alice'
Department: {Id: 1}
In this case is that all related Employees have also been marked as Deleted. Calling SaveChanges causes the Department and all related Employees to be deleted from the database. Check the console window for the delete sql commands.
DELETE FROM [Employee]
OUTPUT 1
WHERE [Id] = @p0;
DELETE FROM [Employee]
OUTPUT 1
WHERE [Id] = @p1;
DELETE FROM [Employee]
OUTPUT 1
WHERE [Id] = @p2;
DELETE FROM [Department]
OUTPUT 1
WHERE [Id] = @p3;
In this case the the child entities foreign key is set to Null.
The change tracker debug view messages shows FK key for all the Employees of the department are set as Null.
Department {Id: 1} Deleted
Id: 1 PK
Name: 'Development'
Employees: [{Id: 1}, {Id: 2}, {Id: 3}]
Employee {Id: 1} Unchanged
Id: 1 PK
DepartmentId: <null> FK
Designation: 'Trainee'
Name: 'John'
Department: {Id: 1}
Employee {Id: 2} Unchanged
Id: 2 PK
DepartmentId: <null> FK
Designation: 'Manager'
Name: 'Rahul'
Department: {Id: 1}
Employee {Id: 3} Unchanged
Id: 3 PK
DepartmentId: <null> FK
Designation: 'Lead'
Name: 'Alice'
Department: {Id: 1}
In this article we covered Entity Framework Core Change Tracker in complete details. We first introduced it’s working and how it keeps track of the loaded entities. I hope you understood it, if you have any confusion then let me know in the comments section.