0

I have a SPA ASP.NET WebAPI application that uses EF6. The application has two parent child entities that I am having a problem updating. The intent of the code is that when a user changes code on the front end then the Objectives and their details will be sent to the controller. The controller then check to see if any details have changed and processes these. Right now I have commented this part of the code out as I cannot even get the simplest update to work. Here is the error that I get:

{"Attaching an entity of type 'Entities.Models.Core.ObjectiveDetail' failed because another entity of the same type already has the same primary key value. This can happen when using the 'Attach' method or setting the state of an entity to 'Unchanged' or 'Modified' if any entities in the graph have conflicting key values. This may be because some entities are new and have not yet received database-generated key values. In this case use the 'Add' method or the 'Added' entity state to track the graph and then set the state of non-new entities to 'Unchanged' or 'Modified' as appropriate."}

Here are the two classes that I have:

public class Objective
{
    public Objective()
    {
        this.ObjectiveDetails = new HashSet<ObjectiveDetail>();
    }
    public int ObjectiveId { get; set; }
    public string Text { get; set; }
    public virtual ICollection<ObjectiveDetail> ObjectiveDetails { get; set; }
}

public partial class ObjectiveDetail
{
    public int ObjectiveDetailId { get; set; }
    public int ObjectiveId { get; set; }
    public string Text { get; set; }
    public virtual Objective Objective { get; set; }
}

My Controller looks like this:

    // PUT: api/Objective/5 Updating
    [ResponseType(typeof(void))]
    public async Task<IHttpActionResult> Put(int id, Objective objective)
    {
        try
        {
            // I get an exception when I uncomment the following line
            //
            //
            // var oldObj = db.ObjectiveDetails.Where(t => t.ObjectiveId == id).ToList();

            var newObj = objective.ObjectiveDetails.ToList();

            //var upd = newObj
            //    .Where(wb => oldObj
            //    .Any(db1 =>
            //       (db1.ObjectiveDetailId == wb.ObjectiveDetailId) &&
            //           (db1.Number != wb.Number || !db1.Text.Equals(wb.Text))))
            //           .ToList();
            //var add = newObj
            //    .Where(wb => oldObj
            //        .All(db2 => db2.ObjectiveDetailId != wb.ObjectiveDetailId))
            //    .ToList();
            //var del = oldObj
            //    .Where(db2 => newObj
            //        .All(wb => wb.ObjectiveDetailId != db2.ObjectiveDetailId))
            //    .ToList();
            //foreach (var objectiveDetail in upd)
            //{
            //    db.Entry(objectiveDetail).State = EntityState.Modified;
            //}
            //foreach (var objectiveDetail in add)
            //{
            //    db.ObjectiveDetails.Add(objectiveDetail);
            //}
            //del.ForEach(_obj => db.ObjectiveDetails.Remove(_obj));

            // I tried the following but it did not work
            //
            //db.Objectives.Attach(objective);
            //db.Entry(objective).State = EntityState.Modified;

            DbEntityEntry dbEntityEntry = db.Entry(objective);
            if (dbEntityEntry.State == EntityState.Detached)
            {
                db.Objectives.Attach(objective);
            }
            dbEntityEntry.State = EntityState.Modified;
            await db.SaveChangesAsync();
            return Ok(objective);
        }
        catch (Exception e)
        {
            return NotFound();
        }
    }

If I remove the line:

var oldObj = db.ObjectiveDetails.Where(t => t.ObjectiveId == id).ToList();

Then I am able to do an update to the database. If I add in this line (which I need for later) then I get the exception.

Can someone give me some advice on what I am doing wrong?

Samantha J T Star
  • 30,952
  • 84
  • 245
  • 427

1 Answers1

1

What you're doing with the commented line is loading the db entities in your DbContext. And then you try to attach the same entities to the context again, so you get a conflict.

You can try loading the old entities in a different context, and use them as reference, so that you avoid this conflict.

Other solution is to modify the already loaded entities (those of oldObj), which are already in your context, instead of trying to add the same entities again with a different state.

You can also detach or directly loaded detached, your oldObject, like explained here.

Ideally, you should make the change tracking in your SPA. In this way you don't need to load the old entities and compare them with the new ones. With your approach the app can accidentally overtwrite other people's changes.

This code:

var oldObj = db.ObjectiveDetails.Where(t => t.ObjectiveId == id).ToList();

loads all the objectiveDetails which are in the DB and attach them to the current context.

And this code, which attaches the updated objective details, tries to attach an object (the modified object) when there is already one with the same key (the one loaded in the previous line of code):

foreach (var objectiveDetail in upd)
{
   db.Entry(objectiveDetail).State = EntityState.Modified;
}

That's way you're getting a conflict on the context.

Community
  • 1
  • 1
JotaBe
  • 38,030
  • 8
  • 98
  • 117
  • I would really like to just modify but because of the objective details being part of the payload from the browser I previously had problems as EF did not seem to understand that some new objective details were added, some may be deleted and some modified. – Samantha J T Star May 05 '14 at 14:15
  • Them try the other option: load the old object in a different DbContext. They won't be in the same context, you'll avoid the conflict, and they'll be available for reference to get the states of the received entites. Ideally, the SPA should track the state of each entity, post only the aded/deelted/modified with it's state in an extra property, so that you don't need to load the old object to get the states. – JotaBe May 05 '14 at 14:20
  • I've added yet another option in my answer – JotaBe May 05 '14 at 14:23
  • Thanks for your links and suggestions. I found that .AsNoTracking() worked well. Now I am wondering how I can modify the oldObj. I have the old Objective graph and the new Objective graph. I notice that my context had Configuration.AutoDetectChangesEnabled = false; Maybe if I set this to true do you think an update would update not only the Objective but also the ObjectiveDetails that it contains ? – Samantha J T Star May 05 '14 at 14:30