dotnet / efcore

EF Core is a modern object-database mapper for .NET. It supports LINQ queries, change tracking, updates, and schema migrations.
https://docs.microsoft.com/ef/
MIT License
13.72k stars 3.17k forks source link

Attach, FixUp Relations and Insert scenario with Identity Not Working #5751

Closed popcatalin81 closed 2 years ago

popcatalin81 commented 8 years ago

Consider the following scenario:

A graph of entities is sent over the wire in serialized form without relationships, just scalar and foreign key properties (Typical case for serializes that do not support cyclic references: IE: Microsoft Bond) in order to be inserted.

The graph is attached to the context in the Added state. Then Entity Framework performs the Relationship FixUp.

The issue

Expected: Entity Framework to insert the graph obeying the Identity specification. Actual: Exception: Cannot insert explicit value for identity column in table 'Node' when IDENTITY_INSERT is set to OFF

Repro application:

class Program
    {
        static void Main(string[] args)
        {
            //Simulate deserialising a list of entities
            var nodes = new[]
            {
                new Node {Id = -1, Name = "Root"},
                new Node {Id = -2, Name = "Child1", ParentId = -1},
                new Node {Id = -3, Name = "Child2", ParentId = -1},
                new Node {Id = -4, Name = "Child3", ParentId = -2},
                new Node {Id = -5, Name = "Child4", ParentId = -3},
                new Node {Id = -6, Name = "Child5", ParentId = -3},
            };

            using (var db = new TreeContext())
            {
                db.Node.AddRange(nodes);

                //Assert Fixup works
                Debug.Assert(nodes[4].Parent != null && nodes[4].Parent.Id == -3);

                try
                {
                    db.SaveChanges();
                    // Exception: Cannot insert explicit value for identity column in table 'Node' when IDENTITY_INSERT is set to OFF
                }
                catch (Exception ex)
                {
                    Console.WriteLine("Insert failed!");
                }
                // UGLY Workaround !!!
                foreach (var entityEntry in db.ChangeTracker.Entries().ToArray())
                {
                    entityEntry.State = EntityState.Detached;
                    entityEntry.Property("Id").CurrentValue = 0;
                    entityEntry.State = EntityState.Added;
                }

                //Assert Fixup works
                Debug.Assert(nodes[4].Parent != null && nodes[4].Parent == nodes[2]);

                try
                {
                    db.SaveChanges();
                }
                catch (Exception ex)
                {
                    Console.WriteLine(ex.ToString());
                }
            }
        }
    }

    public class TreeContext : DbContext
    {
        public DbSet<Node> Node { get; set; }

        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            optionsBuilder.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=ConsoleApp.TreeDb;Trusted_Connection=True;");
        }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<Node>(entity =>
            {
                entity.HasKey(e => e.Id);
                entity.Property(e => e.Id).UseSqlServerIdentityColumn();

                entity
                    .HasOne(e => e.Parent)
                    .WithMany(e => e.Children)
                    .HasForeignKey(e => e.ParentId)
                    .IsRequired(false);
            });
        }
    }

    public class Node
    {
        public Node()
        {
            Children = new List<Node>();
        }

        public int Id { get; set; }
        public string Name { get; set; }
        public int? ParentId { get; set; }
        public virtual Node Parent { get; set; }
        public virtual ICollection<Node> Children { get; set; }
    }

The workaround to detach the graph, set the id to 0 is quite ugly for this scenario.

The better approach would be for the API to be explicit whether a to perform the insert using the temporary key or not.

Further technical details

EF Core version: 1.0.0-rc2-final Operating system: Windows 10 Visual Studio version: VS 2015

Other details about my project setup:

ajcvickers commented 8 years ago

@popcatalin81 You can tell EF that values are temporary, but we don't have any nice top-level API for this yet, so you have to drop down to our lower-level "internal" APIs:

using (var db = new TreeContext())
{
    db.Node.AddRange(nodes);

    var idProperty = db.Model.FindEntityType(typeof(Node)).FindProperty("Id");
    foreach (var node in nodes)
    {
        db.Entry(node).GetInfrastructure().MarkAsTemporary(idProperty);
    }

    //Assert Fixup works
    Debug.Assert(nodes[4].Parent != null && nodes[4].Parent.Id == -3);

    db.SaveChanges();
}

Keep in mind that these internal APIs may change between releases, so use only at your own risk. We will add top-level public API to do this in a future release.

popcatalin81 commented 8 years ago

Thanks @ajcvickers, this is great until the feature is surfaced.