Entity Framework 6 - Multiple lookup inserts vs An object with the same key already exists in the ObjectStateManager

asp.net-mvc asp.net-mvc-5 c# entity-framework entity-framework-6

Question

I am apparently having a real devil of a time understanding Entity Framework 6 which I am using with ASP.NET MVC 5.

The core of the matter is that I have a really quite simple data model that is typical of any real world situation where I have various business objects that have other business objects as properties (and of course they child objects may in turn have other child business objects) and also various types of lookup/type data (Country, State/Province, LanguageType, StatusType etc.) and I cannot figure out how to save/update it properly.

I keep going back and forth between two error states:

1) I either run into the situation where saving a parent business object results in unwanted duplicate values being inserted into my lookup/type tables (for example saving a business object that has been assigned an existing LanguageType of 'English' will result in another LanguageType for 'English' being inserted into the LanguageType table), or

2) I use some of the suggestions I've seen here and elsewhere on the net (e.g. Saving Entity causes duplicate insert into lookup data, Prevent Entity Framework to Insert Values for Navigational Properties ) to solve issue 1 and then find myself fighting against this same issue: An object with the same key already exists in the ObjectStateManager. The ObjectStateManager cannot track multiple objects with the same key .

I will now provide a few code snippets to help build the picture of what I am trying to do and what I am using to do it. First, an example of the entities involved:



    public class Customer : BaseEntity
    {
        public string Name { get; set; }
        [LocalizedDisplayName("Contacts")]

        public virtual List Contacts { get; set; }
    }

    public class Contact : BaseEntity
    {
        [Required]
        public string FirstName { get; set; }

        [Required]
        public string LastName { get; set; }

        public int? LanguageTypeID { get; set; }

        [Required]
        [ForeignKey("LanguageTypeID")]
        public virtual LanguageType Language { get; set; }
    }

    public class LanguageType : Lookup
    {
        [LocalizedDisplayName("CultureName")]
        public string CultureName { get; set; }
    }

    public class Lookup : BaseEntity
    {
        public string DisplayName { get; set; }

        public int DisplayOrder { get; set; }

        public string Name { get; set; }

        public string Description { get; set; }
    }

    public class BaseEntity
    {
        public int ID { get; set; }
        public DateTime? CreatedOn { get; set; }
        public DateTime? UpdatedOn { get; set; }
        public DateTime? DeletedOn { get; set; }
        public bool Deleted { get; set; }
        public bool Active { get; set; }
        public ApplicationUser CreatedByUser { get; set; }
        public ApplicationUser UpdatedByUser { get; set; }
    }


In my controller, I have some code like the following:



    foreach(Contact contact in lstContacts)
    {
        customer.Contacts.Add(contact);
    }
    if (ModelState.IsValid)
    {
        repository.Add(customer);
    }


Let us suppose that each of the contacts has the same LanguageType of 'English' assigned (and in this example it is the fact that I am trying to save multiple contacts that have the same LanguageType that triggers the ObjectStateManager error). Initially, the repository.Add() code just did a context.SaveChanges() which did not work as expected, so now it looks something like this (Entity variable is a Customer):



    try
    {
        if(Entity.Contacts != null)
        {
            foreach(Contact contact in Entity.Contacts)
            {
                var entry = this.context.Entry(contact.Language);
                var key = contact.Language.ID;

                if (entry.State == EntityState.Detached)
                {
                    var currentEntry = this.context.LanguageTypes.Local.SingleOrDefault(l => l.ID == key);
                    if (currentEntry != null)
                    {
                        var attachedEntry = this.context.Entry(currentEntry);
                        //attachedEntry.CurrentValues.SetValues(entityToUpdate);
                        attachedEntry.State = EntityState.Unchanged;
                    }
                    else
                    {
                        this.context.LanguageTypes.Attach(contact.Language);
                        entry.State = EntityState.Unchanged;
                    }
                }
            }
        }
        context.Customers.Add(Entity);
        context.SaveChanges();
    }
    catch(Exception ex)
    {
        throw;
    }


Is it fundamentally wrong to expect this to have worked? How am I supposed to save and example like this? I have similar problems saving similar object graphs. When I look at tutorials and examples for EF, they are all simple and they all just call SaveChanges() after doing something very similar to what I am doing here.

I've just recently been using the ORM capabilities of ColdFusion (which is hibernate under the covers) and there are would simply load the LanguageType entity, assign it to the Contact entity, save the Contact entity, assign it to the Customer and then save the Customer.

In my mind, this is the most basic of situations and I cannot believe that it has caused me so much pain - I hate to say it, but using plain old ADO.NET (or heaven forbid, ColdFusion which I really don't enjoy) would have been MUCH simpler. So I am missing SOMETHING. I apparently have a key flaw in my understanding/approach to EF and If somebody could help me to make this work as expected and help me to figure out just where my misunderstanding lies, I would greatly appreciate it. I have spend too many hours and hours on this and it is a waste of time - I have/will have countless examples just like this one in the code I am building so I need to adjust my thinking with respect to EF right now so I can be productive and do approach things in the expected way.

Your help will mean so much and I thank you for it!

1
0
5/23/2017 12:11:11 PM

Accepted Answer

Let's consider the following object graph in which a teacher instance is the root object,

Teacher --[has many]--> courses

Teacher --[Has One]--> Department

In entity framework's DbContext, each instance of an object has a State indicating whether the object is Added, Modified, Removed or Unchanged. What happens apparently is the following :

Creating the root object for the first time

In this case, in addition to the newly created root object Teacher, ALL the child objects in the graph will have the State Added as well even if they're already created. The solution for this problem is to include the foreign key property for each child element and use it instead, i.e. Teacher.DepartmentId = 3 for example.

Updating the root object and one of its child elements' properties

Suppose you fetch a teacher object from the db, and you change the Teacher.Name property as well as the Teacher.Department.Name property; in this case, only the teacher root object will have the State marked as Modified, the department's State on the other hand remains Unchanged and the modification won't be persisted into DB; Silently without any warning.


EDIT 1

I used your classes as follows and I don't have a problem with persisting the objects :

public class Customer : BaseEntity
{
    public string Name { get; set; }
    public virtual List<Contact> Contacts { get; set; }
}

public class Contact : BaseEntity
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public int? LanguageTypeID { get; set; }
    public Customer Customer { get; set; }
    [ForeignKey("LanguageTypeID")]
    public LanguageType Language { get; set; }
}

public class LanguageType : Lookup
{
    public string CultureName { get; set; }
}

public class Lookup : BaseEntity
{
    public string DisplayName { get; set; }
    public int DisplayOrder { get; set; }
    public string Name { get; set; }
    public string Description { get; set; }
}

public class BaseEntity
{
    public int ID { get; set; }
    public DateTime? CreatedOn { get; set; }
    public DateTime? UpdatedOn { get; set; }
    public DateTime? DeletedOn { get; set; }
    public bool Deleted { get; set; }
    public bool Active { get; set; }
    public ApplicationUser CreatedByUser { get; set; }
    public ApplicationUser UpdatedByUser { get; set; }
}

public class ApplicationUser
{
    public int ID { get; set; }
    public string UserName { get; set; }
    public string Password { get; set; }
}

And used the following Context :

public class Context : DbContext
{
    public Context() : base("name=CS") { }

    public DbSet<Customer> Customers { get; set; }
    public DbSet<Contact> Contacts { get; set; }
    public DbSet<LanguageType> LanguageTypes { get; set; }
    public DbSet<ApplicationUser> ApplicationUsers { get; set; }

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        //I'm generating the database using those entities you defined;
        //Here we're demanding not add 's' to the end of table names
        modelBuilder.Conventions.Remove<PluralizingTableNameConvention>();
    }
}

Then I created a unit tests class with the following :

[TestMethod]
public void TestMethod1()
{
    //our context
    var ctx = new Infrastructure.EF.Context();

    //our language types
    var languageType1 = new LanguageType { ID = 1, Name = "French" };
    var languageType2 = new LanguageType { ID = 2, Name = "English" };

    ctx.LanguageTypes.AddRange(new LanguageType[] { languageType1, languageType2 });

    //persist our language types into db before we continue.
    ctx.SaveChanges();

    //now we're about to start a new unit of work

    var customer = new Customer
    {
        ID = 1,
        Name = "C1",
        Contacts = new List<Contact>() //To avoid null exception
    };

    //notice that we're assigning the id of the language type and not
    //an object.
    var Contacts = new List<Contact>(new Contact[] { 
         new Contact{ID=1, Customer = customer, LanguageTypeID=1},
         new Contact{ID=2, Customer = customer, LanguageTypeID=2}
        });

    customer.Contacts.AddRange(Contacts);

    //adding the customer here will mark the whole object graph as 'Added'
    ctx.Customers.Add(customer);

    //The customer & contacts are persisted, and in the DB, the language
    //types are not redundant.
    ctx.SaveChanges();
}

It all worked smoothly without any problems.

0
3/27/2014 4:29:30 PM

Popular Answer

As far as i know there is no build in support for reattaching modified graphs (like the SaveOrUpdate method of nHibernate). Perhaps this or this can help you.



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