简体   繁体   中英

Entity Framework Navigation Property with Id and Object fail

When saving an Entity where the navigation property is defined dynamically cause problem.

Here is a reproduction of a more complex code.

namespace ConsoleAppEFAttaching
{
    public class MyContext : DbContext
    {
        public MyContext()
            : base("MyContextConnectionString")
        {
            base.Configuration.LazyLoadingEnabled = false;
            base.Configuration.AutoDetectChangesEnabled = false;
            base.Configuration.ProxyCreationEnabled = false;
        }

        protected override void OnModelCreating(DbModelBuilder modelBuilder)
        {
            base.OnModelCreating(modelBuilder);
            modelBuilder.Entity<Parent>();
            modelBuilder.Entity<Child>();
        }
    }
    public class Parent
    {
        public int Id { get; set; }
        public string NameParent { get; set; }

        public static Parent Create(int id)
        {
            return new Parent { Id = id };
        }
    }

    public class Child
    {
        private Parent theOnlyParent;
        public int Id { get; set; }
        public string NameChild { get; set; }

        public Parent TheOnlyParent {
            get { return Parent.Create(TheOnlyParentId); }
            set { TheOnlyParentId = value.Id; }
        }

        public int TheOnlyParentId { get; set; }
    }


    class Program
    {

        static void Main(string[] args)
        {
            Console.WriteLine("Start create database");
            Database.SetInitializer(new DropCreateDatabaseAlways<MyContext>());
            Console.WriteLine("Start adding Parent");
            var p1 = new Parent {NameParent = "Test Parent Name#1"};
            int parentCreatedId;
            Console.WriteLine("Context");
            using (var context = new MyContext())
            {
                context.Set<Parent>().Add(p1);
                context.SaveChanges();
                parentCreatedId = p1.Id;
            }
            Console.WriteLine("Start adding a child from a different context");
            var c1 = new Child { NameChild= "Child #1" };
            c1.TheOnlyParentId = parentCreatedId;
            c1.TheOnlyParent = new Parent {Id = parentCreatedId};
            Console.WriteLine("Context");
            using (var context = new MyContext())
            {
                Console.WriteLine("*Change State Child");
                context.Entry(c1).State = EntityState.Added; // !!! Error : Conflicting changes to the role 'Child_TheOnlyParent_Target' of the relationship 'Child_TheOnlyParent' have been detected.
                Console.WriteLine("*Change State Child->Parent Navigability Property");
                context.Entry(c1.TheOnlyParent).State = EntityState.Detached;
                Console.WriteLine("*Save Changes");
                context.SaveChanges();
            }
            Console.WriteLine("End");
            Console.ReadLine();
        }
    }
}

The problem is when changing the Entry State to Added. The error Conflicting changes to the role 'Child_TheOnlyParent_Target' of the relationship 'ConsoleAppEFAttaching.Child_TheOnlyParent' have been detected. raises.

If I put a Console.WriteLine inside the Child.TheOnlyParent property, I see that the method is set and get multiple times during the change of state. I though that the problem may be caused because of the returned object is not the same but even if I create this one once (instantiate only the first time and then return the same instance) it has the same problem.

If I do not use the Parent.Create in the Child.TheOnlyParent, it works. But, I want to use our logic (with the Create method) to define the class by only the id in the case that we want to limit Include for performance reason.

So my question is divided in two: Why does it calls multiple times the Getter and Setter during the change state and why do I have the conflicting changes to the role?

The getter and setter is called because of the context.Entry(c1) method call. What happens here is, when you call this method for a detached object the ChangeTracker attaches the whole object graph (the object and all its navigationproperties recursively) to the Context. That's why the getters are called.

The ChangeTracker also tries to fixup the navigation properties with already attached objects if they match. So if you have DbContext with a Parent.Id = 1 already attached to your context and you attach a Child with Child.ParentId = 1 and the Child.Parent navigation property = null after the context.Entry(c1) call the Child.Parent property is automatically filled. That's why the setters are called.

As you assumed, your problem is that you create a new Instance of the Parent object everytime you access the getter. For the EF that's basically like having multiple instances of an object with the same primary key which simply cannot be handled by the ChangeTracker. Changing your navigation and foreign key properties like this should work.

public Parent TheOnlyParent
{
    get
    {
        if (theOnlyParent == null) {
            theOnlyParent = Parent.Create(TheOnlyParentId);
        }
        return theOnlyParent;
    }
    set
    {
     If(theOnlyParent != value){
            theOnlyParent = value;
            if (value != null) {
                TheOnlyParentId = value.Id;
            }
        }
    }
}

private int theOnlyParentId;

public int TheOnlyParentId
{
    get
    {
        return theOnlyParentId;
    }
    set
    {
        if (theOnlyParentId != value) {
            theOnlyParentId = value;
            theOnlyParent = null;
        }
    }
}

I have several thing that I needed to changed to make it works.

First, we need to have the Child to return the object. The reason is that if someone set the navigability to Null that we can have the property Null and keeping at the same time the ID.

public class Child
{
    private Parent theOnlyParent;
    private int theOnlyParentId;
    public int Id { get; set; }
    public string NameChild { get; set; }
    [Required]
    public Parent TheOnlyParent
    {
        get
        {
            return theOnlyParent;
        }
        set
        {
            theOnlyParent = value;
            if (value != null)
                TheOnlyParentId = value.Id;
        }
    }

    public int TheOnlyParentId  
    {
        get { return theOnlyParentId; }
        set { 

            theOnlyParentId = value;
            theOnlyParent = Parent.Create(value);
        }
    }

}

The second thing is when working with the entity. I can set the TheOnlyParent to null and keep the Id OR I can use the Entry of the context and set to Unchanged. Both work now.

using (var context = new MyContext())
        {
            Console.WriteLine("*Change State Child");
            context.Entry(c1).State = EntityState.Added; // Conflicting changes to the role 'Child_TheOnlyParent_Target' of the relationship 'Child_TheOnlyParent' have been detected.
            Console.WriteLine("*Change State Child->Parent Navigability Property");
            context.Entry(c1.TheOnlyParent).State = EntityState.Unchanged; // We do not want to create but reuse
            Console.WriteLine("*Save Changes");
            context.SaveChanges();
        }

If someone want to try the whole solution, here is the complete code:

public class MyContext : DbContext
{
    public MyContext()
        : base("MyContextConnectionString")
    {
        base.Configuration.LazyLoadingEnabled = false;
        base.Configuration.AutoDetectChangesEnabled = false;
        base.Configuration.ProxyCreationEnabled = false;
    }

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);
        modelBuilder.Entity<Parent>();
        modelBuilder.Entity<Child>();
    }
}
public class Parent
{
    public int Id { get; set; }
    public string NameParent { get; set; }

    public static Parent Create(int id)
    {
        return new Parent { Id = id };
    }
}

public class Child
{
    private Parent theOnlyParent;
    private int theOnlyParentId;
    public int Id { get; set; }
    public string NameChild { get; set; }
    [Required]
    public Parent TheOnlyParent
    {
        get
        {
            return theOnlyParent;
        }
        set
        {
            theOnlyParent = value;
            if (value != null)
                TheOnlyParentId = value.Id;
        }
    }

    public int TheOnlyParentId  
    {
        get { return theOnlyParentId; }
        set { 

            theOnlyParentId = value;
            theOnlyParent = Parent.Create(value);
        }
    }

}


class Program
{

    static void Main(string[] args)
    {
        Console.WriteLine("Start create database");
        Database.SetInitializer(new DropCreateDatabaseAlways<MyContext>());
        Console.WriteLine("Start adding Parent");
        var p1 = new Parent {NameParent = "Test Parent Name#1"};
        int parentCreatedId;
        Console.WriteLine("Context");
        using (var context = new MyContext())
        {
            context.Set<Parent>().Add(p1);
            context.SaveChanges();
            parentCreatedId = p1.Id;
        }
        Console.WriteLine("Start adding a child from a different context");
        var c1 = new Child { NameChild= "Child #1" };
        c1.TheOnlyParentId = parentCreatedId;
        c1.TheOnlyParent = new Parent {Id = parentCreatedId};

        Console.WriteLine("Context");
        using (var context = new MyContext())
        {
            Console.WriteLine("*Change State Child");
            context.Entry(c1).State = EntityState.Added; // Conflicting changes to the role 'Child_TheOnlyParent_Target' of the relationship 'Child_TheOnlyParent' have been detected.
            Console.WriteLine("*Change State Child->Parent Navigability Property");
            context.Entry(c1.TheOnlyParent).State = EntityState.Unchanged; // We do not want to create but reuse
            Console.WriteLine("*Save Changes");
            context.SaveChanges();
        }
        Console.WriteLine("End");
        Console.ReadLine();
    }
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM