Question

I'm using Entity Framework 6 and I'm writing a Save method to update the details of an existing "Case", as follows:

public void Save(User user, Case @case)
{
    if (@case.Id == 0)
    {
        // deal with new case scenario
    }
    else
    {
        var existing = GetCase(@case.Id);

        // update existing's properties with those in @case

        context.Entity(existing).State = EntityState.Modified;
        context.Entry(existing.SubEntity1).CurrentValues.SetValues(@case.SubEntity1);
        context.Entry(existing.SubEntity2).CurrentValues.SetValues(@case.SubEntity2);
    }

    context.SaveChanges();
}

The Case type has a property named RaisedBy of type User, which derives from ASP.NET Identity's IdentityUser.

When the GetCase call is made, I use IQueryable.Include to load the user attached to the RaisedBy property at the same time. It appears that it loads the User as a proxy, a watch while debugging shows that the type is System.Data.Entity.DynamicProxies.User_ followed by a really long value that looks like a hash.

When context.SaveChanges() is called EF appears to try and re-write the user to the table resulting in a DbUpdateException with message:

Violation of PRIMARY KEY constraint 'PK_dbo.IdentityUsers'. Cannot insert duplicate key in object 'dbo.IdentityUsers'. The duplicate key value is (0799b4c2-a38e-430c-8031-1c27878f2f43). The statement has been terminated.

As far as I can see, the RaisedBy user is exactly the same as that loaded from the context, so I'm struggling to see how the problem has crept in.

Was it helpful?

Solution

Given the code that I omitted from the question, behind the comment

// update existing's properties with those in @case

I'm not surprised no one could answer.

The problem was that, in that block of omitted code:

existing.Events.Add(new Event
{
    CaseId = @case.Id,
    Author = user,
    Text = existing.GetChangeDescription(@case),
    DateTime = now,
    EventType = EventType.Update
});

I am setting up an association to the User object, Entity Framework will subsequently set it's state to added, which is fair enough as there is no trace of this user loaded in the context at this stage, and even if there was, I'd get a different "cannot track object of same type with same key" error. The best approach would be to either:

  • Load the user from the database using DbSet.Find, setting the passed in user to this loaded user
  • Don't set the navigation property, leaving it null and set the AuthorId on the Event object instead
  • Do something smart whereby the context is queried to see if the user already is loaded and if not attaches the passed user to the context with State = EntityState.Unchanged (similar to the first option only avoiding the database hit)

I have gone for option 1 because it fits best with my current architecture, however I'm looking at refactoring to make option 3 possible.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top