Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

EF 4.1 Code First - duplicate entities in object graph causes exception

I am getting the following exception when attempting to save my entity:

"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."

I'm creating a 3 tiered application where the data access layer is using EF Code First, and where the client calls the middle tier using WCF. I am therefore unable able to let the context track the entity state when building up an entity on the client.

In some situations I am finding that the same entity is contained twice in the object graph. In this situation it fails when I try and set the entity state of the duplicate.

For example, I have the following entities: Customer Country Curreny

  1. From the client I create a new instance of a Customer. I then make a service call to get Country instance and assign it to the Customer. The Country instance has an associated Currency.
  2. The user can then associate a Currency with the customer. They may well choose the same Currency that's associated with the Country.
  3. I make another service call to get this. Thus at this stage we may have two separate instances of the same currency.

So what I end up with are two instance of the same entity in the object graph.

When then saving the entity (in my service) I need to tell EF that both Currency entities are not modified (if I don't do this I get duplicates). Problem is that I get the exception above.

On saving if I set the Currency instance on Country instance to null, it resolves the problem, but I feel like the code is becoming increasingly messy (due to this and other WCF related EF workarounds I'm having to put in place).

Are there any suggestions on how to resolve this in a nicer way?

Many thanks for any help in advance. Here's the code:

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();
        }
    }



}
like image 448
P2l Avatar asked Jun 08 '11 14:06

P2l


1 Answers

I guess you get the exception only if customer.Currency and customer.Country.Currency refer to the same currency, i.e. have the same identity key. The problem is that those two currency objects come from different object contexts, therefore they are different objects (ReferenceEquals(customer.Currency, customer.Country.Currency) is false). When you attach both to your last context (by setting the State) the exception occurs because they are two different objects with the same key.

Looking at your code, perhaps the easiest option would be to check if the currency you want to assign to the customer is the same as the country's currency before you even load the currency, something like:

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 assume here that Symbol is the key for currency or a least unique in the DB.) You would also avoid one service/DB call if the currencies are the same.

Other options would be: Don't include the currency in the country query, if you can. Your solution to set customer.Country.Currency to null (not bad at all). Make the references to the two currencies equal in the last context before you add the customer (if (customer.Country.Currency.Symbol == customer.Currency.Symbol) customer.Currency = customer.Country.Currency;). Reload the currencies in your last context and assign them to the customer.

But that's all not really a "nicer way" to solve the problem, only another way - in my opinion.

like image 137
Slauma Avatar answered Oct 01 '22 16:10

Slauma