Insertion order of multiple records in Entity Framework

Viewed 13440

I'm having trouble with EF reordering my inserts when I try and add an entity with multiple children all at once. I've got a 3 level structure with one-to-many relationships between each (Outer 1--* Item 1--* SubItem). If I try and insert a new Outer with Items and Subitems, the Items which contain SubItems end up being inserted first.

Sample Code (.NET 4.5, EF 5.0.0-rc):

public class Outer
{
    public int OuterId { get; set; }
    public virtual IList<Item> Items { get; set; }
}

public class Item
{
    public int OuterId { get; set; }
    [ForeignKey("OuterId")]
    public virtual Outer Outer { get; set; }

    public int ItemId { get; set; }
    public int Number { get; set; }

    public virtual IList<SubItem> SubItems { get; set; }
}

public class SubItem
{
    public int SubItemId { get; set; }

    [ForeignKey("ItemId")]
    public virtual Item Item { get; set; }
    public int ItemId { get; set; }
}

public class MyContext : DbContext
{
    public DbSet<Outer> Outers { get; set; }
    public DbSet<Item> Items { get; set; }
    public DbSet<SubItem> SubItems { get; set; }
}

class Program
{
    static void Main(string[] args)
    {
        Database.SetInitializer(new DropCreateDatabaseAlways<MyContext>());
        MyContext context = new MyContext();

        // Add an Outer object, with 3 Items, the middle one having a subitem
        Outer outer1 = new Outer { Items = new List<Item>() };
        context.Outers.Add(outer1);
        outer1.Items.Add(new Item { Number = 1, SubItems = new List<SubItem>() });
        outer1.Items.Add(new Item { Number = 2, SubItems = new List<SubItem>(new SubItem[] { new SubItem() }) });
        outer1.Items.Add(new Item { Number = 3, SubItems = new List<SubItem>() });

        context.SaveChanges();

        // Print the order these have ended up in
        foreach (Item item in context.Items)
        {
            Console.WriteLine("{0}\t{1}", item.ItemId, item.Number);
        }
        // Produces output:
        // 1       2
        // 2       1
        // 3       3
    }
}

I'm aware of this answer by Alex James which states that inserts may need to be reordered in order to satisfy relational constraints, but that is not the issue here. His answer also mentions that they can't track the order of items in order-preserving structures such as Lists.

What I'd like to know is how I can get these inserts to be ordered. While I can rely on sorting my inserted items by a field other than the PK, it's a lot more efficient if I can rely on the PK order. I don't really want to have to use multiple SaveChanges calls to accomplish this.

I'm using EF5 RC, but judging by the other unanswered questions around, this has been around for some time!

7 Answers

Another way of doing this, without database round trip after each entry added (heavily dependent on the application logic though) is via combination of entity state changes.

In my case - hierarchy of nodes - I had to persist root nodes first, then rest of the hierarchy in order to automatic path calculations to work.

So I had a root nodes, without parent ID provided and child nodes with parent ID provided.

EF Core randomly (or through complex and intelligent logic - as you prefer :) randomly scheduled nodes for insertion, breaking path calculation procedure.

So I went with overriding SaveChanges method of the context and inspecting entities from the set for which I need to maintain certain order of inserts - detaching any child nodes first, then saving changes, and attaching child nodes and saving changes again.

// select child nodes first - these entites should be added last
List<EntityEntry<NodePathEntity>> addedNonRoots = this.ChangeTracker.Entries<NodePathEntity>().Where(e => e.State == EntityState.Added && e.Entity.NodeParentId.HasValue == true).ToList();

// select root nodes second - these entities should be added first
List<EntityEntry<NodePathEntity>> addedRoots = this.ChangeTracker.Entries<NodePathEntity>().Where(e => e.State == EntityState.Added && e.Entity.NodeParentId.HasValue == false).ToList();

        if (!Xkc.Utilities.IsCollectionEmptyOrNull(addedRoots))
        {
            if (!Xkc.Utilities.IsCollectionEmptyOrNull(addedNonRoots))
            {
                // detach child nodes, so they will be ignored on SaveChanges call
                // no database inserts will be generated for them
                addedNonRoots.ForEach(e => e.State = EntityState.Detached);

                // run SaveChanges - since root nodes are still there, 
                // in ADDED state, inserts will be executed for these entities
                int detachedRowCount = base.SaveChanges();

                // re-attach child nodes to the context
                addedNonRoots.ForEach(e => e.State = EntityState.Added);

                // run SaveChanges second time, child nodes are saved
                return base.SaveChanges() + detachedRowCount;
            }
        }

This approach does not let you preserve order of individual entities, but if you can categorize entities in those that must be inserted first, and those than can be inserted later - this hack may help.

I've found a way to do it. It just thought I'd let you know:

using (var dbContextTransaction = dbContext.Database.BeginTransaction())
{
  dbContext.SomeTables1.Add(object1);
  dbContext.SaveChanges();

  dbContext.SomeTables1.Add(object2);
  dbContext.SaveChanges();

  dbContextTransaction.Commit();
}

multiple Add() before a save or AddRange() before a save does not preserve order. Also when you are reading the collection it is not guaranteed to return the results in the same order they were originally added. You need to add some property to your entities and when you query use OrderBy() to ensure they come back in the order you want.

It's not professional. But, I solved my problem with this method.

PublicMenu.cs file:

public class PublicMenu
    {
        public int Id { get; set; }

        public int? Order { get; set; }
        
        public int? ParentMenuId { get; set; }

        [ForeignKey("ParentMenuId")]
        public virtual PublicMenu Parent { get; set; }

        public virtual ICollection<PublicMenu> Children { get; set; }

        public string Controller { get; set; }

        public string Action { get; set; }

        public string PictureUrl { get; set; }

        public bool Enabled { get; set; }
}

PublicMenus.json file

[
    {
        "Order": 1,
        "Controller": "Home",
        "Action": "Index",
        "PictureUrl": "",
        "Enabled": true
    },
    {
        "Order": 2,
        "Controller": "Home",
        "Action": "Services",
        "PictureUrl": "",
        "Enabled": true
    },
    {
        "Order": 3,
        "Controller": "Home",
        "Action": "Portfolio",
        "PictureUrl": "",
        "Enabled": true
    },
    {
        "Order": 4,
        "Controller": "Home",
        "Action": "About",
        "PictureUrl": "",
        "Enabled": true
    },
    {
        "Order": 5,
        "Controller": "Home",
        "Action": "Contact",
        "PictureUrl": "",
        "Enabled": true
    }
]

DataContextSeed file

    public class DataContextSeed
        {
            public static async Task SeedAsync(ICMSContext context, ILoggerFactory loggerFactory)
            {
                try
                {
                       if (!context.PublicMenus.Any())
                        {
                            var publicMenusData = File.ReadAllText("../Infrastructure/Data/SeedData/PublicMenus.json");
                            var publicMenus = JsonSerializer.Deserialize<List<PublicMenu>>(publicMenusData);
                            foreach (var item in publicMenus)
                            {
                                context.PublicMenus.Add(item);
                                await context.SaveChangesAsync();
                            }
                       }
                }
                catch (Exception ex)
                {
                    var logger = loggerFactory.CreateLogger<ICMSContextSeed>();
                    logger.LogError(ex.Message);
                }
            }
   }

Crap!

The question is asked 9 years ago, and Microsoft still not doing anything with it.

In EF6, I found if I use

for(int i=0; i<somearray.Length; i++)
db.Table.Add(new Item {Name = somearray[i]});

It did in order, but if I use

foreach(var item in somearry)
db.Table.Add(new Item {Name = item})

then not in order.

When I used EF core 3.1, it is always not in order of the above two scenario.

Related