EF 4.1 Code First - duplicate objects in an object graph throw an exception

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

"AcceptChanges cannot continue because the object key values ​​conflict with another object in the ObjectStateManager. Before invoking AcceptChanges, make sure the key values ​​are unique."

I am creating a three-tier application where the data access layer uses EF Code First and where the client calls the middle layer using WCF. Therefore, I cannot allow the context to track the state of the object when creating the object on the client.

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

For example, I have the following objects: Client Country Curreny

  • From the client, I create a new client instance. Then I make a service call to get the country instance and assign it to the customer. A copy of the country currency associated with it.
  • The user can then associate Currency with the customer. They can choose the same currency that are associated with the country.
  • I am making another service call to get this. Thus, at this stage we can have two separate copies of the same currency.

So, in the end, I get two instances of the same object in the object graph.

When saving an object (in my service) I need to tell EF that both Currency objects are not changed (if I do not do this, I get duplicates). The problem is that I got the exception above.

When saving, if I set the Currency instance on the Country instance to null, it solves the problem, but I feel the code is getting more dirty (because of this and other WCF EF-related workarounds that I need to implement).

Are there any suggestions on how to resolve this better?

Thanks so much for any help in advance. Here is 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 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(); } } } 
+7
source share
3 answers

I assume that you get an exception only if customer.Currency and customer.Country.Currency refer to the same currency, that is, they have the same identification key. The problem is that these 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 State ), an 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 client matches the same as the currency of the country before you load the currency, for example:

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

(Here I assume that Symbol is the key to the currency or the least unique in the database.) In addition, you avoid a single service / DB call if the currencies are the same.

Other options: Do not include the currency in the country’s request, if you can. Your decision to set customer.Country.Currency to null (not bad at all). Make the link to the two currencies equal in the last context before adding the client ( 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 client.

But all this is not exactly the “best way” to solve the problem, only in a different way - in my opinion.

+5
source

I think the problem is that you are setting the EntityState to Unchanged. The exception that you see occurs only if entity keys always exist AND the state of the object is not added.

See http://msdn.microsoft.com/en-us/library/bb896271.aspx

Last paragraph of considerations for attaching objects: "An InvalidOperationException occurs when the attached object has the same EntityKey as another object already present in the context of the object. This error does not occur if the object in the context with the same key but is in the state" Added "

So, the question is, why are you forcing the state to remain unchanged, and not leave it as an added one?

EDIT: Edited after viewing your post and your comment. Ultimately, the problem is that you say EF “Hey, add these Currency and Country objects with this client,” but two of these objects already exist.

You can use Attach instead of the Add method, but the client does not exist yet.

I suggest wrapping these calls in a transactional machine by calling SaveChanges immediately after creating the Client, than using Attach rather than Add. If you get errors, you can cancel the transaction if necessary. I don't have the right code, but am I making sense?

Something like:

  using (TransactionScope scope = new TransactionScope()) { // Now add a new customer Customer customer = new Customer(); customer.Name = "Customer1"; context1.SaveChange(); // 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.Attach(customer); // The following line will result in this exception: // AcceptChanges cannot continue because the object key values conflict with another // object in the ObjectStateManager. Make sure that the key values are unique before // calling AcceptChanges. context4.SaveChanges(); scope.Complete(); } 
0
source

I had the same problem in a windows service and it was solved by creating and deleting a DBContext in each insert / update / get call. I previously saved dbContext as a private variable in my repositories and reused it.

So far so good. YMMV. I can’t say that I understand exactly why this works - I have not yet delved into Code First. The magical functions of the unicorn are good, but I'm going to throw it away and pass in the TSQL code, as the magic makes it difficult to understand what is happening.

0
source

All Articles