I am trying out EntityFrameworkCore. I looked at the documentation, but couldn't find a way to easily update a complex entity that is related to another entity.
Here is a simple example. I have 2 classes - Company & Employee.
public class Company
{
public int Id { get; set; }
public string Name { get; set; }
}
public class Employee
{
public int Id { get; set; }
public string Name { get; set; }
public Company Company { get; set; }
}
Company is a simple class, and Employee is only slightly complex, as it contains a property with reference to the Company class.
In my action method, which takes in the updated entity, I could first look up the existing entity by id, and then set each property on it before I call SaveChanges.
[HttpPut]
public IActionResult Update(int id, [FromBody]Employee updatedEmployee)
{
if (updatedEmployee == null || updatedEmployee.Id != id)
return BadRequest();
var existingEmployee = _dbContext.Employees
.FirstOrDefault(m => m.Id == id);
if (existingEmployee == null)
return NotFound();
existingEmployee.Name = updatedEmployee.Name;
if (updatedEmployee.Company == null)
existingEmployee.Company = null; //as this is not a PATCH
else
{
var existingCompany = _dbContext.Companies.FirstOrDefault(m =>
m.Id == updatedEmployee.Company.Id);
existingEmployee.Company = existingCompany;
}
_dbContext.SaveChanges();
return NoContent();
}
With this sample data, I make an HTTP PUT call on Employees/3.
{
"id": 3,
"name": "Road Runner",
"company":
{
"id": 1
}
}
And that works.
But, I hope to avoid having to set each property this way. Is there a way I could replace the existing entity with the new one, with a simple call such as this?
_dbContext.Entry(existingEmployee).Context.Update(updatedEmployee);
When I try this, it gives this error:
System.InvalidOperationException: The instance of entity type 'Employee' cannot be tracked because another instance of this type with the same key is already being tracked. When adding new entities, for most key types a unique tem porary key value will be created if no key is set (i.e. if the key property is assigned the default value for its t ype). If you are explicitly setting key values for new entities, ensure they do not collide with existing entities or temporary values generated for other new entities. When attaching existing entities, ensure that only one entity instance with a given key value is attached to the context.
I can avoid this error if I retrieve the existing entity without tracking it.
var existingEmployee = _dbContext.Employees.AsNoTracking()
.FirstOrDefault(m => m.Id == id);
And this works for simple entities, but if this entity has references to other entities, this causes an UPDATE statement for each of those referenced entities as well, which is not within the scope of the current entity update. The documentation for the Update
method says that as well:
// Begins tracking the given entity, and any other reachable entities that are not already being tracked, in the Microsoft.EntityFrameworkCore.EntityState.Modified state such that they will be updated in the database when Microsoft.EntityFrameworkCore.DbContext.SaveChanges is called.
In this case, when I update the Employee entity, my Company entity changes from
{
"id": 1,
"name": "Acme Products"
}
to
{
"id": 1,
"name": null
}
How can I avoid the updates on the related entities?
Based on the inputs in the comments and the accepted answer, this is what I ended up with:
Updated Employee class to include a property for CompanyId in addition to having a navigational property for Company. I don't like doing this as there are 2 ways in which the company id is contained within Employee, but this is what works best with EF.
public class Employee
{
public int Id { get; set; }
public string Name { get; set; }
public int CompanyId { get; set; }
public Company Company { get; set; }
}
And now my Update
simply becomes:
[HttpPut]
public IActionResult Update(int id, [FromBody]Employee updatedEmployee)
{
if (updatedEmployee == null || updatedEmployee.Id != id)
return BadRequest();
var existingEmployeeCount = _dbContext.Employees.Count(m => m.Id == id);
if (existingEmployeeCount != 1)
return NotFound();
_dbContext.Update(updatedEmployee);
_dbContext.SaveChanges();
return NoContent();
}
Based on documentation of Update
Ref:Update
Begins tracking the given entity in the Modified state such that it will be updated in the database when SaveChanges() is called. All properties of the entity will be marked as modified. To mark only some properties as modified, use Attach(Object) to begin tracking the entity in the Unchanged state and then use the returned EntityEntry to mark the desired properties as modified. A recursive search of the navigation properties will be performed to find reachable entities that are not already being tracked by the context. These entities will also begin to be tracked by the context. If a reachable entity has its primary key value set then it will be tracked in the Modified state. If the primary key value is not set then it will be tracked in the Added state. An entity is considered to have its primary key value set if the primary key property is set to anything other than the CLR default for the property type.
In your case, you have updatedEmployee.Company
navigation property filled in. So when you call context.Update(updatedEmployee)
it will recursively search through all navigations. Since the entity represented by updatedEmployee.Company
has PK property set, EF will add it as modified entity. A point to notice here is Company
entity has only PK property filled in not others. (i.e. Name is null). Therefore while EF determines that Company with id=1 has been modified to have Name=null and issues appropriate update statement.
When you are updating navigation by yourself, then you are actually finding the company from server (with all properties populated) and attaching that to existingEmployee.Company
Therefore it works since there are no changes in Company, only changes in existingEmployee
.
In summary, if you want to use Update
while having a navigation property filled in then you need to make sure that entity represented by navigation has all data and not just PK property value.
Now if you have only Company.Id
available to you and cannot get other properties filled in updatedEmployee
then for relationship fixup you should use foreign key property (which needs PK(or AK) values only) instead of navigation (which requires a full entity).
As said in question comments:
You should add CompanyId
property to Employee
class. Employee
is still non-poco (complex) entity due to navigation present.
public class Employee
{
public int Id { get; set; }
public string Name { get; set; }
public int? CompanyId {get; set; }
public Company Company { get; set; }
}
During update action pass updatedEmployee
in following structure. (See this is the same amount of data, just structured bit differently.)
{
"Id": 3,
"Name": "Road Runner",
"CompanyId": 1,
"Company": null //optional
}
Then in your action, you can just call context.Update(updatedEmployee)
and it will save employee but not modify the company.
Due to Employee
being complex class, you can still use the navigation. If you have loaded employee with eager loading (Include
) then employee.Company
will have relevant Company entity value.
Notes:
_dbContext.Entry(<any entity>).Context
gives you _dbContext
only so you can write just _dbContext.Update(updatedEmployee)
directly.AsNoTracking
, if you load the entity in the context, then you cannot call Update
with updatedEmployee
. At that point you need to modify each property manually because you need to apply changes to the entity being tracked by EF. Update
function gives EF telling, this is modified entity, start tracking it and do necessary things at SaveChanges
. So AsNoTracking
is right to use in this case. Further, if the purpose of retrieving entity from server for you is to check existence of employee only, then you can query _dbContext.Employees.Count(m => m.Id == id);
and compare return value to 1. This fetches lesser data from the server and avoids materializing the entity.CompanyId
if you don't add it to CLR class then EF creates one for you in background as shadow property. There will be database column to store value of FK property. Either you define property for it or EF will.