开发者

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 buildi开发者_如何学Gong 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();
        }
    }



}


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.


I think the issue is because you are setting the EntityState to Unchanged. The exception you are seeing only happens if the entity keys always exist AND the entity state is not Added.

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

The last paragraph of Considerations for Attaching Objects is: "An InvalidOperationException occurs when an object being attached has the same EntityKey as a different object already present in the object context. This error does not occur if an object in the context with same key but is in the Added state."

So the question is, why are you forcing the state to Unchanged instead of leaving it as added?

EDIT: Edited after looking at your post again and your comment. Ultimately the problem is you are telling EF "Hey, add these Currency and Country objects with this Customer" but two of those objects already exist.

You can use the Attach instead of Add method, but the customer doesn't exist yet.

I suggest wrapping these calls in a transactionscope, calling SaveChanges right after creating the Customer, than using Attach rather then Add. If you get errors, you can roll back the transaction if necessary. I don't have a code sample handy, but does what I am saying make 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's key values conflict with another     
                // object in the ObjectStateManager. Make sure that the key values are unique before 
                // calling AcceptChanges.
                context4.SaveChanges();
                scope.Complete();
            }


I had this same problem in a Windows Service and solved it by creating and disposing the DBContext in every insert/update/get call. I was previously keeping the dbContext as a private variable in my repos and reusing it.

So far so good. YMMV. I can't say I understand exactly why it works - I haven't gone deep enough into Code First yet. The magic unicorn features are nice but I'm bordlerine about to throw it out and hand code the TSQL as the magic makes it hard to actually understand what is going on.

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜