Exception in EF 4.1 Code First due to duplicate entities in the object graph

ef-code-first entity-framework

Question

When I try to save my entity, I receive the following exception:

"Because the object's key values disagree with those of another object in the ObjectStateManager, AcceptChanges cannot proceed. Before invoking AcceptChanges, make sure the key values are distinct."

A three-tiered application I'm building uses EF Code First for the data access layer and WCF for client calls to the middle tier. As a result, while creating an entity on the client, I am unable to let the context follow the entity state.

I sometimes discover that the same item appears twice in the object graph. When I attempt to change the duplicate's entity status in this case, it fails.

I have the following entities, for instance: Customer\sCountry\sCurreny

  1. I create a new instance of a Customer from the client. Once I have Country instance, I contact a service to assign it to the customer. A Currency instance is linked to the Country instance.
  2. The user may then link the customer with a currency. They could decide to use the same currency as the nation does.
  3. To acquire this, I dial another service number. As a result, we can currently have two different instances of the same currency.

So, in the object graph, I end up with two instances of the same thing.

I need to inform EF that both Currency entities have not been updated before storing the entity (in my service); otherwise, I get duplicates. Problem is, I keep getting the aforementioned exception.

The issue is fixed on saving if I change the Currency instance on the Country instance to null, but I feel like the code is becoming messier (as a result of this and other WCF-related EF workarounds I'm having to do).

Are there any recommendations for a more amicable method to address this?

Thank you in advance for any assistance. Here is the key:

using System;
using System.Collections.Generic;
using System.Data.Entity.ModelConfiguration;
using System.ComponentModel.DataAnnotations;
using System.Data.Entity;
using System.Linq;

namespace OneToManyWithDefault
{

    public class Customer
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public Country Country { get; set; }
        public Currency Currency { get; set; }
        public byte[] TimeStamp { get; set; }
    }

    public class Country
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public Currency Currency { get; set; }
        public byte[] TimeStamp { get; set; }
    }

    public class Currency
    {
        public int Id { get; set; }
        public string Symbol { get; set; }
        public byte[] TimeStamp { get; set; }
    }


    public class MyContext
        : DbContext
    {
        public DbSet<Customer> Customers { get; set; }
        public DbSet<Currency> Currency { get; set; }
        public DbSet<Country> Country { get; set; }

        public MyContext(string connectionString)
            : base(connectionString)
        {
            Configuration.LazyLoadingEnabled = false;
            Configuration.ProxyCreationEnabled = false;
        }

        protected override void OnModelCreating(DbModelBuilder modelBuilder)
        {
            modelBuilder.Configurations.Add(new CustomerConfiguration());
            modelBuilder.Configurations.Add(new CountryConfiguration());
            modelBuilder.Configurations.Add(new CurrencyConfiguration());
            base.OnModelCreating(modelBuilder);
        }
    }

    public class CustomerConfiguration
        : EntityTypeConfiguration<Customer>
    {
        public CustomerConfiguration()
            : base()
        {
            HasKey(p => p.Id);
            Property(p => p.Id)
                .HasColumnName("Id")
                .HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity)
                .IsRequired();
            Property(p => p.TimeStamp)
                .HasColumnName("TimeStamp")
                .IsRowVersion();

            ToTable("Customers");
        }
    }

    public class CountryConfiguration
        : EntityTypeConfiguration<Country>
    {
        public CountryConfiguration()
            : base()
        {
            HasKey(p => p.Id);
            Property(p => p.Id)
                .HasColumnName("Id")
                .HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity)
                .IsRequired();
            Property(p => p.TimeStamp)
                .HasColumnName("TimeStamp")
                .IsRowVersion();

            ToTable("Countries");
        }
    }

    public class CurrencyConfiguration
        : EntityTypeConfiguration<Currency>
    {
        public CurrencyConfiguration()
            : base()
        {
            HasKey(p => p.Id);
            Property(p => p.Id)
                .HasColumnName("Id")
                .HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity)
                .IsRequired();
            Property(p => p.TimeStamp)
                .HasColumnName("TimeStamp")
                .IsRowVersion();

            ToTable("Currencies");
        }
    }

    class Program
    {
        private const string ConnectionString =
            @"Server=.\sql2005;Database=DuplicateEntities;integrated security=SSPI;";

        static void Main(string[] args)
        {
            // Seed the database
            MyContext context1 = new MyContext(ConnectionString);

            Currency currency = new Currency();
            currency.Symbol = "GBP";
            context1.Currency.Add(currency);

            Currency currency2 = new Currency();
            currency2.Symbol = "USD";
            context1.Currency.Add(currency2);

            Country country = new Country();
            country.Name = "UK";
            country.Currency = currency;
            context1.Country.Add(country);

            context1.SaveChanges();

            // Now add a new customer
            Customer customer = new Customer();
            customer.Name = "Customer1";

            // Assign a country to the customer
            // Create a new context (to simulate making service calls over WCF)
            MyContext context2 = new MyContext(ConnectionString);
            var countries = from c in context2.Country.Include(c => c.Currency) where c.Name == "UK" select c;
            customer.Country = countries.First();

            // Assign a currency to the customer
            // Again create a new context (to simulate making service calls over WCF)
            MyContext context3 = new MyContext(ConnectionString);
            customer.Currency = context3.Currency.First(e => e.Symbol == "GBP");

            // Again create a new context (to simulate making service calls over WCF)
            MyContext context4 = new MyContext(ConnectionString);
            context4.Customers.Add(customer);

            // Uncommenting the following line prevents the exception raised below
            //customer.Country.Currency = null;

            context4.Entry(customer.Country).State = System.Data.EntityState.Unchanged;
            context4.Entry(customer.Currency).State = System.Data.EntityState.Unchanged;

            // The following line will result in this exception:
            // AcceptChanges cannot continue because the object's key values conflict with another     
            // object in the ObjectStateManager. Make sure that the key values are unique before 
            // calling AcceptChanges.
            context4.Entry(customer.Country.Currency).State = System.Data.EntityState.Unchanged;
            context4.SaveChanges();

            Console.WriteLine("Done.");
            Console.ReadLine();
        }
    }



}
1
7
6/8/2011 2:50:17 PM

Accepted Answer

I assume you only qualify for the exemption ifcustomer.Currency and customer.Country.Currency have the same identification key and correspond to the same currency. The issue is that the two money objects are distinct things since they originate from separate object contexts (ReferenceEquals(customer.Currency, customer.Country.Currency) is false When you link both of them to your previous context (by setting theState ), the fact that they are two distinct objects with the same key results in the exception.

Looking at your code, it seems that the simplest solution is to determine, before you even load the currency, if the currency you wish to assign to the customer is the same as the country's currency, as in:

if (customer.Country.Currency.Symbol == "GBP")
    customer.Currency = customer.Country.Currency;
    // currencies refer now to same object, avoiding the exception
else
{
    MyContext context3 = new MyContext(ConnectionString);
    customer.Currency = context3.Currency.First(e => e.Symbol == "GBP");
}

(I'm assuming thatSymbol is the currency key or at least one of a kind in the DB.) If the currencies are the same, one service/DB call would also be avoided.

Other solutions include, if possible, excluding the currency from the country inquiry. Your approach to settingcustomer.Country.Currency to null (Not at all awful) Prior to adding the customer, ensure that references to the two currencies are equivalent in the previous context (if (customer.Country.Currency.Symbol == customer.Currency.Symbol) customer.Currency = customer.Country.Currency; ). Reload the previous context's currencies, then assign them to the customer.

But in my perspective, that's just another approach to tackle the issue and not really a "nicer way" to do it.

5
6/8/2011 8:15:25 PM

Popular Answer

The identical issue occurred to me in a Windows Service, and I was able to fix it by establishing and destroying the DBContext with each request for an insert, update, or get. Previously, I reused the dbContext by storing it as a private variable in my repositories.

All is OK thus far. YMMV. I can't answer with certainty why it works since I haven't studied Code First in depth enough. The magic unicorn features are lovely, but I'm on the verge of abandoning them in favor of hand-coding the TSQL since the magic makes it difficult to truly comprehend what is happening.



Related Questions





Related

Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow
Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow