2

I am using the latest version of entity framework (6.1.3) and i have the following class which allows a customers name to be changed:

public class CustomerService
{
    public void ChangeName()
    {
        using (TestModel ctx = new TestModel())
        {
            var customer = GetCustomer(ctx);

            //set new name
            customer.Name = "New Name";

            SaveCustomer(ctx, customer);
        }
    }

    private Customer GetCustomer(TestModel ctx)
    {
        //get customer by email
        var customer = ctx.Customers
                          .AsNoTracking()
                          .Include(n => n.Country) //Load Shipping Country
                          .Include(n => n.Country1) //Load Billing Country
                          .Where(n => n.Email == "test@test.com")
                          .Single();

        return customer;
    }

    private void SaveCustomer(TestModel ctx, Customer customer)
    {
        //save back
        ctx.Customers.Attach(customer); // getting error here
        ctx.Entry(customer).State = EntityState.Modified;
        ctx.SaveChanges();
    }
}

In the sql server database i have 2 tables:

  • Customer - Id, Name, ShippingCountryId (Foreign Key), BillingCountryId (Foreign Key)
  • Country - Id, Name

When i call the ChangeName method i get the following error:

Attaching an entity of type 'TestConsoleApp.customermodel.Country' 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.

I have been doing some debugging and found the following:

  • If i remove the AsNoTracking call then there is no error
  • If i remove one of the Includes (keeping AsNoTracking in) then there is no error

So it looks like the combination of AsNoTracking and 2 Includes that are of the same type causes the issue.

Can anyone suggest why i get this error and how i would resolve it? (At the same time keeping AsNoTracking and the 2 Includes in my code).

I reuse the GetCustomer method in other places thats why i want to keep AsNoTracking and the 2 Includes in.

Raja Nadar
  • 9,409
  • 2
  • 32
  • 41
user1786107
  • 2,921
  • 5
  • 24
  • 35
  • You might have a look at my answer on [ASP.NET MVC - Attaching an entity of type 'MODELNAME' failed because another entity of the same type already has the same primary key value](http://stackoverflow.com/questions/23201907/asp-net-mvc-attaching-an-entity-of-type-modelname-failed-because-another-ent/39557606#39557606). – Murat Yıldız Sep 18 '16 at 12:29

3 Answers3

3

It has to do with reference navigation properties and AsNotracking.

The line ...

ctx.Entry(customer).State = EntityState.Modified;

... marks the customer as Modified, but also attaches both countries to the context as Unchanged. In this case, both countries are identical. Except, they aren't...

Normally, when an object is materialized by EF using Include, EF creates only one instance of each entity in the object graph. The context cache is an identity map: each entity occurs only once. So when both countries are the same, one Country entity will be found in the cache.

With AsNoTracking however, EF doesn't file anything into its cache. Now two instances of the same country are created. Hence the exception when they are attached.

Gert Arnold
  • 105,341
  • 31
  • 202
  • 291
  • Ok this makes sense. Its not great that EF can't handle this. Do you have any suggestions on how to get around this problem? – user1786107 Feb 27 '16 at 13:54
  • I wouldn't use `AsNoTracking`. I don't think you gain much by it. It may help performance when larger volumes of data are queried. – Gert Arnold Feb 27 '16 at 16:30
  • *It's not great that EF can't handle this.* Well, to handle this, *some* sort of tracking of materialized objects is required. `AsNoTracking` is litterally *no* tracking, i.e. *no* way whatsoever to handle duplicates. – Gert Arnold Feb 28 '16 at 10:51
  • I don't know if any of you sees this, but I have a similar problem, an I _need_ to attach the record on save, because I can not fetch it with tracking. What I don't understand is why EF can't recognize that both country fields point to the same Id, and only attach the country once. I have even tried to override GetHashCode and implement Equatable to help EF detect that a record is already attached, but it does not work. – Göran Roseen Oct 06 '17 at 11:32
  • Even if anyone sees this, nobody can answer it without sufficient background information. In other words: ask a new question. – Gert Arnold Oct 06 '17 at 11:42
0

you don't need this method.

private void SaveCustomer(TestModel ctx, Customer customer)
{
    //save back
    ctx.Customers.Attach(customer); //GET ERROR HERE
    ctx.Entry(customer).State = EntityState.Modified;
    ctx.SaveChanges();
}

just do

    using (TestModel ctx = new TestModel())
    {
        var customer = GetCustomer(ctx);

        //set new name
        customer.Name = "New Name";

        ctx.SaveChanges();
    }

basically, your retrieved customer is not an isolated entity. it comes from ctx and is already tracked by EF. you don't need to attach it once more.

you attach an entity, only if you created it anew in your code, but with a primary key manually.

e.g.

// where 1023 is the key of an existing entity in the DB
var customer = new Customer { Id = 1023, Name = "test" };
ctx.Customers.Attach(customer)

this is not the case with your code. your entity comes from an EF query via ctx, and is already tracked by EF.

and the problem has nothing to do with multiple includes.

Raja Nadar
  • 9,409
  • 2
  • 32
  • 41
  • It has to be attached, remember i am using AsNoTracking. I tried the above and the databse did not update. – user1786107 Feb 27 '16 at 10:56
  • oh okay. in that case, just delete the attach statement. ctx.Entry(customer).State = EntityState.Modified; should be good enough – Raja Nadar Feb 27 '16 at 11:25
0

So it looks like the combination of AsNoTracking and 2 Includes that are of the same type causes the issue.

That is the problem. When you attach the customer with:

ctx.Customers.Attach(customer);

It's trying to attach the same country twice. The shipping country and the billing country are the same because they have the same key.

From https://msdn.microsoft.com/library/bb896271(v=vs.100).aspx:

If the object being attached has related objects, those objects are also attached to the object context.

So the customer is attached, then .Country is attached, then .Country1 is attached. Country and Country1 are the same, thus the exception, because, also from https://msdn.microsoft.com/library/bb896271(v=vs.100).aspx, when you are attaching entities:

If more than one entity of a particular type has the same key value, the Entity Framework will throw an exception.

To solve this problem, go into SaveCustomer and change Attach to Add, like this:

private void SaveCustomer(TestModel ctx, Customer customer)
{
    //save back
    ctx.Customers.Add(customer); // no more error!
    ctx.Entry(customer).State = EntityState.Modified;
    // Prevent countries from being added to database (they already exist in db)
    ctx.Entry(customer.Country).State = EntityState.Detatched;
    ctx.Entry(customer.Country1).State = EntityState.Detatched;
    ctx.SaveChanges();
}

Yes, you are calling Add, which is typically used to add new data that doesn't yet exist in the database, which will result in an INSERT to the database. However, since we're going in and manually setting the EntityState to Modified, there will be an UPDATE statement instead of an INSERT statement sent to the database.

I wrote a small test app with some unit tests to demonstrate this more fully, which can be found at https://github.com/codethug/EF.Experiments/blob/master/EF.Test/AttachingMultiple.cs

CodeThug
  • 3,054
  • 1
  • 21
  • 17
  • You're basically repeating what I answered and offering a solution that dates back to the `ObjectContext` API *and* is not what the OP wants (objects in an Added state is quite different than just attached). Not using `AsNoTracking` is all that's needed here. – Gert Arnold Mar 02 '16 at 19:17
  • @GertArnold 1. The entities do not end up in an added state - my solution specifically sets the state to `Modified`. 2. the `AddObject` method is available on both the `ObjectContext` API and the `DBContext` API. 3. The OP specifically states that removing `AsNoTracking` is not an option. – CodeThug Mar 02 '16 at 21:52
  • OK, you're indeed trying to repair the state (although `Unmodified` doesn't exist). But your final two lines throw exactly the same exception. You *can't* attach two different instances of the same entity as `Unchanged`, not even if you take the elaborate detour through adding them first (which, by the way, you can do without the old API method `AddObject`). The OP wanted to adhere to `AsNoTracking`, but that was before realizing the mechanism behind it. This simply can't be done right with `AsNoTracking`. – Gert Arnold Mar 02 '16 at 22:15
  • @GertArnold You are correct - I was wrong. The AddObject method no longer exists in DbContext. Rather, the Add method must be used. I'll update my answer with better information. – CodeThug Mar 03 '16 at 03:51
  • After `ctx.Entry(customer).State = EntityState.Modified;` you have two `Country` objects in an `Added` state. Are you sure these are not inserted? I'm pretty sure they are. – Gert Arnold Mar 03 '16 at 10:29
  • Good catch. I'll update the answer and the test in the linked sample project – CodeThug Mar 03 '16 at 18:55
  • 1
    Well, I think the OP includes the countries for a reason. By detaching them you effectively exclude them (the `Country` properties are `null` after detaching). I admire your tenacity though, and I must say this operation helped me gain more insight. But in the end I still think the conclusion is that simply removing `AsNoTracking` outweighs any elaborate work-around to deal with duplicate entries. – Gert Arnold Mar 03 '16 at 19:40
  • I agree. Removing `AsNoTracking` would be far simpler and more in line with how EF is typically used. But for some reason the OP wants to use it. Thanks for your feedback - it helped to improve this answer significantly (I had all kinds of problems with my original answer!) – CodeThug Mar 04 '16 at 01:38