5

i´m having some troubles when it comes to modify and add child rows at the same time. I´m using the technique from the answer: stackoverflow.com/questions/5557829/....

The problem is in the following code:

public void EditReport(tbl_inspection inspection)
{
    foreach (var roll in inspection.tbl_inspection_roll)
    {                    
        container.tbl_inspection_roll.Attach(roll);
        container.ObjectStateManager.ChangeObjectState(roll, (roll.id_inspection_roll == 0) ? EntityState.Added : EntityState.Modified);
    }

    container.SaveChanges();
}

I always have at least 1 row to update. When I have 1 row to add, it works fine, the problem is when I try to add more than 1 row at the same time, shows the well-known error:

An object with the same key already exists in the ObjectStateManager. The ObjectStateManager cannot track multiple objects with the same key.

Feels like I´m missing something here...

Community
  • 1
  • 1
Gabriel
  • 887
  • 10
  • 22
  • 1) Check your tbl_inspection list to make sure that there are no duplicates. 2) In the debugger, check to see what the state of those records in the tbl_inspection list are *before* you attach them, and see if one of them is already considered attached. 3) Also in the debugger, check to see if any of the tbl_inspection records are also found in the local collection. That one's happened to me before... – Corey Adler Jan 03 '13 at 14:55
  • I checked all items that you described, and it seems fine, although when I try to add more than 1 row, both have id_inspection_roll equals zero. This implies that they are equal? Is there a way to differ them? Thanks a lot for helping me. – Gabriel Jan 03 '13 at 15:19

3 Answers3

1

I think that you need to split off the modified from the added. In the question that you linked to Ladislav had the following code as an example of this:

if (myEntity.Id != 0)
{
    context.MyEntities.Attach(myEntity);
    context.ObjectStateManager.ChangeObjectState(myEntity, EntityState.Modified);
}
else
{
    context.MyEntities.AddObject(myEntity);
}

context.SaveChanges();

I think that using Attach specifically, instead of using AddObject is what's causing the error.

EDIT: Try the following for the Attach part instead:

var r = new tbl_inspection_roll { id_inspection_roll = roll.id_inspection_roll };
container.tbl_inspection_roll.Attach(r);
container.Entry(r).CurrentValues.SetValues(roll);
Corey Adler
  • 15,897
  • 18
  • 66
  • 80
  • still not working :( ... i think i have to add some reference or something like that... i tried to set the tbl_inspection_rollReference.EntityKey but still doesn't work... – Gabriel Jan 03 '13 at 15:35
  • Which line is it failing on? The Attach? – Corey Adler Jan 03 '13 at 15:51
  • Before you attach it, does it say that the entity is detached, or that it's already been attached? – Corey Adler Jan 03 '13 at 17:06
  • Did you find that record in the local collection? If it's found on the local collection then it will bomb because it already knows to track that record. If it's not there, then I have a different solution. – Corey Adler Jan 03 '13 at 18:53
  • No no, it originally comes from the html form, then i create the entire object using stubs and pass to the function that i posted... i make some queries on these entities before, but i use MergeOption.NoTracking... – Gabriel Jan 03 '13 at 19:00
  • +1 for the effort... your answer is correct, but doesn't solve the problem... see my answer... – Gabriel Jan 04 '13 at 13:36
  • yes, but fires the error anyway because more than 1 child have the same primary key: 0. When I pass the View Model object and create a stub not attached to the parent object, it works. – Gabriel Jan 04 '13 at 14:38
  • You can't use the Attach method for brand new records. You have to use the Add() method. – Corey Adler Jan 04 '13 at 14:42
  • i didn't know it too, but turns out, it works... you just have to chance the object's state to Added – Gabriel Jan 04 '13 at 14:51
1

The problem here is because two or more child stubs have the same key: 0. Once you try to Attach the first object, it fires the error.

The method will have to be redesigned, using some kind of DTO (I think it's not correct to pass the ViewModel object to the Domain Model layer, thats why I was using stubs). Or calling a function to Add/Modify direct from the Controller.

EDIT:

Here's the code:

public void EditReport(Inspection obj)
{
    var inspection = new tbl_inspection
    {
        id_inspection = obj.ID,
        code = obj.Code       
    };

    foreach (var roll in obj.Rolls)
    {                    
        var rollStub = new tbl_inspection_roll
        {
            id_inspection_roll = roll.ID,
            id_inspection = obj.ID,
            description = roll.Description
        };

        container.tbl_inspection_roll.Attach(roll);
        container.ObjectStateManager.ChangeObjectState(roll, (roll.id_inspection_roll == 0) ? EntityState.Added : EntityState.Modified);
    }

    container.tbl_inspection.Attach(inspection);
    container.ObjectStateManager.ChangeObjectState(inspection, EntityState.Modified);

    container.SaveChanges();
}

Any better solutions are welcomed...

Gabriel
  • 887
  • 10
  • 22
  • So EF doesn't balk at multiple rolls with Id = 0 at container.tbl_inspection_roll.Attach(roll)? – Raymond Saltrelli Jan 04 '13 at 15:22
  • No... It just fires the error if you have a complete filled stub entity with a parent and their child objects... it doesn't make sense to me either... but works this way... now I have a Design problem... what kind of object should I pass to this function... and on which layer should I keep it... – Gabriel Jan 04 '13 at 15:30
  • 1
    I don't know if this will work for you but I have DTOs that my controller takes as parameters and returns. The controller is responsible for converting the DTOs to stub entities and passes them into methods called on a repository object. The repository object handles all of the attaching/adding/detaching to/from my DbContext. I'm going to replace the code from my answer which exists in my repository class with the code from your and see how it works out. – Raymond Saltrelli Jan 04 '13 at 15:38
  • I was using View Model instead of DTO, and it was in a project separated from the MVC Application... then my controller created the stub and pass to the method on the repository. But once I have to pass the object directly to the repository, I don't know which approach to use... Do you keep your DTOs in the MVC Application as well? – Gabriel Jan 04 '13 at 16:45
  • 1
    I have an MVC application project that also contains my entities and a portable class library project containing my DTOs. The DTO project is portable because it is used by both my MVC application and my Silverlight client application. Using the same DTO library both server- and client-side makes JSON (de)serialization easier. The controllers deserialize incoming JSON into DTOs, convert the DTOs into stub entities, pass the stub entities into the repository, convert resulting non-stub entities back into DTOs then serialize the DTOs to JSON in the response. – Raymond Saltrelli Jan 04 '13 at 17:24
  • I like your approach bit I'm having trouble with navigation properties. I attempt to attach my child objects individually before attaching my parent object but the child objects have a navigation property back to the parent which is still giving me grief. Need to find a way to avoid this circular reference. – Raymond Saltrelli Jan 04 '13 at 17:28
0

If I understand you problem correctly, you are trying to save an object graph containing a ParentObject and related ChildObjects and you're getting the exception because Attach() is not able to reconcile Id collisions among the ChildObjects in the database and the attached ParentObject.

Below is a work around I've used in the past. It's not pretty but it gets the job done.

public ParentObject Upsert(ParentObject newParentObject)
{
    // Check for an existing ParentObject.
    ParentObject exisitingParentObject = container.ParentObjects.SingleOrDefault(o => o.Id == newParentObject.Id);

    // If not an existing ParentObject
    if (exisitingParentObject == null)
    {
        // Add a new ParentObject
        exisitingParentObject = container.ParentObjects.Add(container.ParentObjects.Create());
    }

    // Update the properties of the existing ParentObject with the values of the new ParentObject
    var parentEntry = container.Entry(exisitingParentObject);
    parentEntry.CurrentValues.SetValues(newParentObject);

    // Remove ChildObjects that are in the existing but not in the new.
    for (int i = exisitingParentObject.ChildObjects.Count() - 1; i >= 0; i--)
    {
        ChildObject exisitingChildObject = exisitingParentObject.ChildObjects[i];
        if (!newParentObject.ChildObjects.Any(o => o.Id == exisitingChildObject.Id))
        {
            container.ChildObjects.Remove(exisitingChildObject);
        }
    }

    // Upsert new ChildObjects
    foreach (ChildObject newChildObject in newParentObject.ChildObjects)
    {
        // Check for an existing ChildObject
        ChildObject exisitingChildObject = exisitingParentObject.SingleOrDefault(o => o.Id == newChildObject.Id);

        // If not an existing ChildObject
        if (exisitingChildObject == null)
        {
            // Add a new ChildObject
            exisitingChildObject = exisitingParentObject.ChildObjects.Add(container.ChildObject.Create());
        }

        var childEntry = container.Entry(exisitingChildObject);
        childEntry.CurrentValues.SetValues(newChildObject);
    }

    container.SaveChanges();

    return exisitingParentObject;
}
Raymond Saltrelli
  • 4,071
  • 2
  • 33
  • 52
  • It is. I'm not thrilled about having to do this but I know it works. If anyone else has a cleaner way to do it I'd love to hear about it too. – Raymond Saltrelli Jan 04 '13 at 14:26
  • 1
    A stub entity http://blogs.msdn.com/b/alexj/archive/2009/06/19/tip-26-how-to-avoid-database-queries-using-stub-entities.aspx – Gabriel Jan 04 '13 at 14:47
  • I don't know the context in which you are calling Attach() but I'm calling it from the Put action in an ApiController so my entity is inherently a stub. How are you making it not a stub and once it is not a stub why do you then need to call Attach() at all? – Raymond Saltrelli Jan 04 '13 at 14:56
  • I'm using mvc... so I create the stub object in the controller action and pass to the function that I posted... once its Detached, I have to Attach it... – Gabriel Jan 04 '13 at 15:03