EntityFrameworkCore inserting existing related items

2.8k views Asked by At

Consider

 public class Item
 {
    public int Id{get;set;}
    public string Name{get;set;}
    public List<ItemTag> ItemTags{get;set;}
 }

 public class ItemTag
 {
    public int Id{get;set;}
    public int Name{get;set;}
 }

I now use entity framework core to add an ItemTag to Item. This works just fine.

Now I add a second ItemTag to the same Item. When saving, the entire object is passed, including the existing related ItemTags. EF then tries to insert the existing ItemTag which fails with an exception of 'Cannot Insert a value into an Identity Column...'

So how do I prevent the existing object from getting Inserted?

My workaround is to loop through the ItemTags, and set any that have an Id to EntityState.Unchanged to force it not to save it. But it seems that such a workaround should not be required.

This is the code that save the item:

//Get the current item, so that only updated fields are saved to the database.
var item = await this.DbContext.Items.SingleAsync(a => a.Id == itemInput.Id);
item.UpdatedBy = this._applicationUserProvider.CurrentAppUserId;
item.Updated = DateTimeOffset.UtcNow;

//Use Automapper to map fields.
this._mapper.Map(itemInput, item);

//Workaround for issue.
foreach (var itemTag in item.ItemTags)
{
    var entry = this.DbContext.Entry(itemTag);
    if (itemTag.Id > 0)
    {
        entry.State = EntityState.Unchanged;
    }
}

await this.SaveChangesAsync();
2

There are 2 answers

1
Tseng On

AutoMapper isn't suitable for backing back to persistence or domain models. The author of AutoMapper stated.

This also doesn't work well in the way EF (Core) does change tracking.

Change tracking is done, based on the entities that are loaded form the database. When you change an entities value its state changes from Unchanged to Modified and will result in an update method.

When you load an entity w/o the items relation (using eager loading), then when you add an entity it will me marked as Added, resulting in an insert when saved.

You also have the possibility to Attach and Detach entities.

From the linked blog post:

Rowan Miller summarized the new behavior in a GitHub issue (bit.ly/295goxw):

Add: Adds every reachable entity that isn’t already tracked.

Attach: Attaches every reachable entity, except where a reachable entity has a store-generated key and no key value is assigned; these will be marked as added.

Update: Same as Attach, but entities are marked as modified.

Remove: Same as Attach, and then mark the root as deleted. Because cascade delete now happens on SaveChanges, this allows cascade rules to flow to entities later on.

So you'll have to attach your items (if you only want to add new ones w/o a primary key) or call Update/UpdateRange on the DbSet to update the ones with a key and add the ones without.

Alternatively handle it yourself by setting/changing its tracked state as you already did. But keep in mind, Unchanged won't update the passed entities.

1
Edward On

For inserting the new ItemTag, you could insert to ItemTag table directly without reflecting the Item.

Define your model like below:

public class ItemTag
{
    public int Id { get; set; }
    public int Name { get; set; }
    public int ItemId { get; set; }
    public virtual Item Item { get; set; }
}

Try to set the ItemId for ItemTag to insert a new ItemTag for Item.

If you prefer to insert Item with ItemTag, you could try JsonPatchDocument which describe how to operate the model.

Try to refer JSON Patch With ASP.net Core .

For JsonPatchDocument , you need to generate the JsonPatchDocument<Item> at client side. try to refer ASP Core PatchDocument returning Invalid Input.