What is the difference between _context.Entry(entity).State = EntityState.Modified and _context.Entity.Update(entity) in ASP.NET EF Core? For example:
[HttpPut]
public async Task<ActionResult<Student>> PutStudent(Student student)
{
**_context.Entry(student).State = EntityState.Modified;**
await _context.SaveChangesAsync();
return student;
}
[HttpPut]
public async Task<ActionResult<Student>> PutStudent(Student student)
{
**_context.Student.Update(student);**
await _context.SaveChangesAsync();
return student;
}
Setting an entity's state toModified will mark all of the entity's scalar properties as modified, meaning that SaveChanges will generate an update statement updating all mapped table fields except the key field(s).
Not asked, but a single property can also be marked as Modified:
_context.Entry(student).Property(s => s.Name).IsModified = true;
This will also set the entity's state as modified.
The Update method is quite different, see the docs:
Begins tracking the given entity (...)
For entity types with generated keys if an 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. This helps ensure new entities will be inserted, while existing entities will be updated. 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.
This can be very convenient in disconnected scenarios in which new and updated entities are attached to a context. EF will figure out which entities are Added and which are Modified.
Another difference is that the Update method also traverses nested entities. For example, if Exams is a collection in the Student class, updating a Student will also mark its Exams as Modified, or Added where their keys aren't set.
Not documented, but worth mentioning, if a Student and its Exams are attached as Unchanged then the Update method will only set the Student's state to Modified, not the Exams.
Related
I'm working on a form using EF Core in Blazor Server. I had a number of issues with entity tracking so I set all of my queries to AsNoTracking and designed my service to create a new instance of dbcontext for each query. I think this is appropriate as none of the returned values will be edited - only the form data that users enter and the id references to the queried fields, such as employee numbers, will be stored. For inserting data, I use this:
using var context = Factory.CreateDbContext();
context.SetupForm.Attach(model);
context.Entry(model).State = EntityState.Added;
await context.SaveChangesAsync();
I am attaching the data rather than adding it and then setting the form object state to added. This ensures EF Core doesn't attempt to insert the existing employee objects when it inserts the form data.
The trouble starts in a section of the form that can have as many items as the user wants. The select a couple of employees and type in relevant data. When they submit the form, they may have selected the same employee in multiple items. As these employees were selected from separate contexts, they are two separate instances with the same ID. Of course, EF Core doesn't like this and throws errors like this one:
The instance of entity type 'Principal' cannot be tracked because another instance with the key value '{EmployeeID: 1234}' is already being tracked. When attaching existing entities, ensure that only one entity instance with a given key value is attached.
I understand why this error is occurring but I need to be able to attach multiple entities in this way. How can I work around this issue?
One thing I could do is assign the foreign keys manually but that would be rigid and require updates whenever the model changes.
just try this
using var context = Factory.CreateDbContext();
context.Set<Principal>().Add(model);
//or maybe context.Principals.Add(model);
await context.SaveChangesAsync();
This seems to do the trick! What it does is mark any entity that lacks a key as added. Otherwise, the entity is ignored entirely.
using var context = Factory.CreateDbContext();
context.ChangeTracker.TrackGraph(model, node =>
{
if (!node.Entry.IsKeySet)
{
node.Entry.State = EntityState.Added;
}
});
await context.SaveChangesAsync();
None of the items that have a key will need to be inserted. Treating them as untracked then solves any issues with duplicates and only inserts the rows that need it.
More information: https://learn.microsoft.com/en-us/ef/core/change-tracking/identity-resolution#resolve-duplicates
I am using Entity Framework Core 5.0.3, .NET 5.0.103, ASP.NET Core 5 Blazor WebAssebmly. I have -
// Cập nhật
[HttpPut("{id}")]
public async Task<ActionResult> UpdateItem(int id, ProjectWorkCategory newItem)
{
var oldItem = await db.ProjectWorkCategories.FindAsync(id);
if (oldItem == null)
{
return NotFound();
}
// Mã loại công trình.
if (!string.IsNullOrEmpty(newItem.ProjectWorkCategoryCode))
{
oldItem.ProjectWorkCategoryCode = newItem.ProjectWorkCategoryCode;
}
// Tên loại công trình
if (!string.IsNullOrEmpty(newItem.ProjectWorkCategoryName))
{
oldItem.ProjectWorkCategoryName = newItem.ProjectWorkCategoryName;
}
// Diễn giải
if (!string.IsNullOrEmpty(newItem.Description))
{
oldItem.Description = newItem.Description;
}
// Trạng thái theo dõi.
if (newItem.ActiveStatus != oldItem.ActiveStatus)
{
oldItem.ActiveStatus = newItem.ActiveStatus;
}
// Người sửa.
if (!string.IsNullOrEmpty(newItem.ModifiedBy))
{
oldItem.ModifiedBy = newItem.ModifiedBy;
}
oldItem.ModifiedDate = DateTime.Now;
//db.ProjectWorkCategories.Add(oldItem);
db.Entry(oldItem).State = EntityState.Modified;
await db.SaveChangesAsync();
return NoContent();
}
What is the different between
db.ProjectWorkCategories.Add(oldItem);
await db.SaveChangesAsync();
versus
db.Entry(oldItem).State = EntityState.Modified;
await db.SaveChangesAsync();
db.ProjectWorkCategories.Add(oldItem);
await db.SaveChangesAsync();
the above code adds the entity to database. if 'oldItem' has Unique Id, then you will get exception trying to save it, otherwise it would save duplicate record.
db.Entry(oldItem).State = Microsoft.EntityFrameworkCore.EntityState.Modified;
await db.SaveChangesAsync();
this is the correct way to update an entity.
A record will be added to the database when you use the add method. But if you use the 'State = Microsoft.EntityFrameworkCore.EntityState.Modified', your existing record will be edited in the database.
This one is adding a new item, and as mentioned by #Navid it will through an error if it has a unique Id.
db.ProjectWorkCategories.Add(oldItem);
await db.SaveChangesAsync();
If you want to update you should be using Update method:
db.ProjectWorkCategories.Update(oldItem);
await db.SaveChangesAsync();
This one also can be used for updating an existing item.
db.Entry(oldItem).State = Microsoft.EntityFrameworkCore.EntityState.Modified;
await db.SaveChangesAsync();
Deep dive:
Updated to explain the points mentioned by #atiyar comment
There is a some differences between using Add, Update methods and changing the entity state EntityState.Modified.
When using (1)Add Update method, the framework will start to track this entity as EntityState.Added if it has no Id assigned, or will track it as EntityState.Modified if it has an Id.
DbContext.Update Method:
For entity types with generated keys if an entity has its primary key value set then it will be tracked in the EntityState.Modified state. If the primary key value is not set then it will be tracked in the EntityState.Added state.
(2) When using Update method the framework will track the entity and all its referenced entities with EntityState.Modified flag.
(2) I still think this is valid. Please see the explanation in this document: Where this method differs from explicitly setting the State property, is in the fact that the context will begin tracking any related entities (such as a collection of books in this example) in the Modified state, resulting in UPDATE statements being generated for each of them. If the related entity doesn't have a key value assigned, it will be marked as Added, and an INSERT statement will be generated.
When using EntityState.Modified the framework will track this entity as EntityState.Modified. But all referenced entities will be tracked by EntityState.Unchanged flag, and you may need to change the state of each referenced entity to Modified manually.
Another difference, when calling SaveChanges() after Update method, only changed values of the entity will be submitted to the DB. But when you call SaveChanges() after using EntityState.Modified all values will be submitted to the DB since all fields has been marked as Modified.
(3) That is my mistake, I used to use another library which we can use to send only modified fields for update, but this is not valid for EF Update method.
EDIT
With reference to your update method, you can use both Update or EntityState.Modified. The easy way is to use Update(entity). In some advanced cases when the entity state could be Detached (un-tracked) I use EntityState flags to check if the entity is attached (tracked) or not, then I do modify its state accordingly.
Finally, instead of avoiding the result of SaveChangesAsync() I recommend to check the value and return:
var success = await db.SaveChangesAsync();
if(success > 0)
// log or message for success
else
// log or message for failure
Entity Framework tracks an entity with a State value, which you can check by -
var state = db.Entry(myEntity).State;
And when you call SaveChanges(), EF tries to insert, update or delete an entity based on its State value.
What Add() and EntityState.Modified do :
The Add() method marks an entity as Added (sets the State value to EntityState.Added), whether the entity has a primary key value or not, and when you call SaveChanges() the next time, EF will try to insert that entity.
db.Entry(myEntity).State = EntityState.Modified; sets the State value of the entity explicitly to Modified, which basically tells EF that "this entity already exits in database, and has been modified". Therefore, on the next SaveChanges() call EF will try to update the entity with its new values.
What Add() and EntityState.Modified will do in your scenario :
You are trying to update an entity not to insert it. For update operation you are not supposed to use the Add() method.
You fetched an existing entity from the database (and it has its primary key value in it) with -
var oldItem = await db.ProjectWorkCategories.FindAsync(id);
At this point the entity will be in Unchanged state. Then as soon as you set a new value to any of its properties, the entity gets modified and will automatically go in Modified state. But then if you call -
db.ProjectWorkCategories.Add(oldItem);
the Add() method will put the entity in Added state. At this point calling SaveChanges() will throw an exception, because EF is trying to insert an entity which already exists in database (the primary key violation).
In your scenario, by setting -
db.Entry(oldItem).State = EntityState.Modified;
you are explicitly marking the entity as Modified, and on the next SaveChanges() call EF will update the entity with its new values. But the thing is, you don't even need the above line. As I mentioned earlier, fetching an existing entity and changing any of its property value will automatically put the entity in Modified state. Therefore, after setting the new values you can directly call -
await db.SaveChangesAsync();
and your entity will get updated as expected.
I am adding a new entity to the DbContext with a navigational property passed from the client but instead of EF recognising that the navigational property entity already exist, it tries to re-insert it which fails because you cannot have duplicate primary keys.
var profile = _mapper.Map<Profile>(profileDto);
profile.User.LockoutEnabled = true;
profile.User.Password = new PasswordHasher<User>().HashPassword(profile.User, "*********");
profile.Company = null; // Doing this works fine but otherwise it fails.
await _dataContext.Set<Profile>().AddAsync(profile);
await _dataContext.SaveChangesAsync();
profile.Company = await _dataContext.Set<Company>().FindAsync(profile.CompanyId);
return _mapper.Map<ProfileCreateDto>(profile);
Since nulling the Company entity or changing the client to pass only the CompanyId instead of the Company value works fine, I still want to understand why EF is try to re-insert an existing entity.
EF is trying to re-insert the Company entity because you are using the AddAsync method to insert your Profile entity.
The AddAsync method causes the entity in question (Profile) and all it's related entities (e.g. Company) present in the entity-graph to be marked as Added. An entity marked as Added implies - This is a new entity and it will get inserted on the next SaveChanges call. See details - DbSet<TEntity>.AddAsync().
As a general solution, in a disconnected scenario, when creating new entity with an entity-graph (with one or more related entities) use the Attach method instead.
The Attach method causes any entity to be marked as Added only if it doesn't have the primary-key value set. Otherwise, the entity is marked as Unchanged. An entity marked as Unchanged implies - This entity already exists in the database and it might get updated on the next SaveChanges call. See details - DbSet<TEntity>.Attach()
I hope that meets your curiosity.
I'm using Entity Framework Core together with the repository pattern. To help me out, I coded one base repository with the basic CRUD methods. The update method is as follows:
public void Update(TEntity entity)
{
var contextEntry = _context.Entry<TEntity>(entity);
if (contextEntry.State == EntityState.Dettached)
{
_context.Attach(entity);
}
contextEntry.State = EntityState.Modified;
_context.SaveChanges();
}
Given the BaseRepository class containing this method, I created one User repository inheriting from this
public class UserRepository : BaseRepository<User>, IUserRepository
{
}
And I've used this in the PUT method of one Web API coded with ASP.NET Core
[HttpPut("~/api/users/{id}")]
public IActionResult Put(int id, [FromBody] User user)
{
if (user == null || user.UserId != id)
{
return BadRequest();
}
userRepository.Update(user);
return new NoContentResult();
}
Now, when issuing a request, I get one error in the _context.Attach(entity) line. The exception says that it can't add the entity for tracking because there is already another entity with the same key being tracked.
When debugging I saw that contextEntry.State was set to Unchanged. Hence, it is obviously not equal to EntityState.Dettached. Still, the execution got inside the if statement and tried to attach the entity.
Something is quite wrong here. Is this a bug? Or am I doing something very wrong? I believe that I'm the one doing something very wrong with this update strategy, but I'm unsure about it. In that case, what is wrong with my approach?
EDIT: I updated the Update method to use just _context.Update(entity) and after _context.SaveChanges(). Still, the _context.Update(entity) throws one InvalidOperationException with this message:
Additional information: The instance of entity type 'User' 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 temporary key value will be created if no key is set (i.e. if the key property is assigned the default value for its type). 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.
You are getting same entity from database some where in the project that's why it give you error.
In Update method you just add the entity in the context that's why you get contextEntry.State Unchanged.
You can fix this problem in two ways.
You need to call the Detach method on same entity when you get it from database.
copy the values from entity which you received in Update method to the existing context entity and save that entity in database.
All the information is in the exception message... you already have another copy of the entity with that primary key attached.
I would recommend one of the following (preferred first):
Use a new Context for each action, don't have a long-lived repository/context
Use .Set<TEntity>.Find(object[] key) on your context using the Primary Key, in order to retrieve any entity you already have.
In your current update method, use the Set<TEntity>.Local.Find(..) to check if it already exists
I am trying to log changes to the database so that the user can see who changed what. I am using the DbEntityEntry to go through and log the DbPropertyEntity that have been modified. I am running into a problem when I want to log changes to a navigation property. I use the Reference() method to get reference to the navigation property, however unlike DbPropertyEntity, DbReferenceEntry does not have a OriginalValue only a CurrentValue attribute. How do you get the OriginalValue of a navigation property?
//Get the field that hold the id of the foreign key
var field = entry.Property(x => x.field);
//Check to see if the user changed the value
if (field.IsModified)
{
//Get the reference property associated with the field
var fieldRef = entry.Reference(x => x.fieldRef);
//Log the id change
Log(field.Name, field.CurrentValue, field.OriginalValue);
//Can't get the OriginalValue
Log(fieldRef.Name, fieldRef.CurrentValue, ???);
}
What exactly do you expect to log for the reference?
If you want to log changes in relation (= changes in foreign key) you should map the foreign key as separate property and it will be logged as normal field. If you don't map the foreign key you have an independent association. Support for independent associations in DbContext API is limited. The full support is only in ObjectContext API (you can use it from DbContext API) but it will still not solve your problem - independent association cannot be in modified state. It can be either in unchanged, added or deleted state. If you change the reference the old association is marked as deleted and new is added = there are two separate association objects tracked by the context.