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 relationsh
What I'd like to know is how I can get these inserts to be ordered.
You cannot. Order of database commands is EF's internal behavior. If you want to control the order of commands don't use tools which abstract you from low level database interactions - use SQL directly.
Edit based on comment:
Yes it is low level interaction because you are putting expectations on the order of SQL commands when working with abstraction you don't have under your control. At high level you are getting something different because you are using expectations which don't work with that abstraction. If you want to have control over order of SQL commands you must either force EF by saving items one by one (=> multiple SaveChanges
and TransactionScope
) or write SQL yourselves. Otherwise use separate column for ordering.
Btw. EF doesn't save the entity as you see it. It has its own change tracker holding references to all your attached instances. References are held in multiple Dictionary
instances and dictionary doesn't preserve insertion order. If these collections are used for generating SQL commands (and I guess they are) no order can be guaranteed.
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);
}
}
}
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();
}
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.
Tables in the database are sets. That means that the order is not guaranteed. I assume in your example that you want the results ordered by "Number". If that is what you want, what are you going to do if that number changes and it doesn't reflect the order in the database anymore?
If you really want to have the rows inserted in a specific order, multiple SaveChanges are your best bet.
The reason nobody wants to call SaveChanges multiple times is because this feels exactly how it is: a dirty hack.
Since a primary key is a technical concept, it shouldn't make any functional sense to order your results on this key anyway. You can order the results by a specific field and use a database index for this. You probably won't see the difference in speed.
Making the ordering explicit has other benefits as well: it is easier to understand for people who have to maintain it. Otherwise that person has to know that ordering on primary key is important and gives the correct results, because in an other (completely) unrelated section of your application, it accidentally is the same order as the number field.