0

Note that this is NOT Bulk Update, every record will have its own unique update value.

I have a user-ordered list of items (Id, Name, Position) where user can freely reorder the items. Now when user submits a new ordered list, I would like to update all of their Positions. I have a list of Ids matching with their Positions (note that I do not have any other information like Name and do not want to lose it).

How do I make EF Core to submit a single round trip with many UPDATE statement without having to fetch them first?

Below are the approaches and their limitations:

  • This one sends 1 request for each item:
ctx.Products
   .Where(q => q.Id == item.Id)
   .ExecuteUpdate(q => q.SetProperty(p => p.Position, _ => item.Position))
var idList = new int[] { 1, 2, 3, 4 };

using (var db = new SomeDatabaseContext())
{
    var friends = db.Friends.Where(f => idList.Contains(f.ID)).ToList();

    friends.ForEach(a => a.msgSentBy = '1234');
    db.SaveChanges();
}
  • My approach by modifying the Change Tracker manually results in data loss (after running this, all Name becomes ""):
    var posList = new (int, int)[]
    {
        (1, 2),
        (2, 1),
        (3, 1),
    };

    var ctx = new TestContext();

    foreach (var (id, pos) in posList)
    {
        var entity = new DbItem
        {
            Id = id,
            Name = "", // Required property
            Position = pos,
        };

        var entry = ctx.Attach(entity);
        entry.State = EntityState.Modified;
        entry.Property(q => q.Position).IsModified = true;
    }

    await ctx.SaveChangesAsync();
  • I also tried another approach using Attach instead, however it results in exception:
// The surrounding code is similar to the above code
var entity = new DbItem
{
    Id = id,
    Name = "", // Required property
    Position = -1, // To make sure it will be changed
};

var entry = ctx.Attach(entity);
entity.Position = pos;

Unhandled exception. Microsoft.EntityFrameworkCore.DbUpdateConcurrencyException: The database operation was expected to affect 1 row(s), but actually affected 0 row(s); data may have been modified or deleted since entities were loaded. See http://go.microsoft.com/fwlink/?LinkId=527962 for information on understanding and handling optimistic concurrency exceptions.

I am using EF Core 7.

Luke Vo
  • 17,859
  • 21
  • 105
  • 181

1 Answers1

1

If you are sure that the data is present in database (or ok in case of failure for missing ones) you can try leveraging the ability to attach entities and set state:

List<Products> entities = ...
var ctx = ...;
ctx.AttachRange(entities);
foreach (var ent in entities)
{
    ctx.Entry(ent).State = EntityState.Modified;
}

ctx.SaveChanges();

My approach by modifying the Change Tracker manually results in data loss

Do not set state, update the value:

foreach (var (id, pos) in posList)
{
    var entity = new DbItem
    {
        Id = id,
        Position = pos - 1 
    };

    var entry = ctx.Attach(entity);
    entity.Position = pos;
}
Guru Stron
  • 102,774
  • 10
  • 95
  • 132
  • Hi I forgot to mention in my question is that I don't actually have the values of the entities. I am making an edit. In my case, manually attaching it would result in losing value of other fields – Luke Vo Jul 18 '23 at 08:51
  • @LukeVo you can attach "empty" entity (i.e. with only Id set) and then set the values for needed fields (though there are some edge cases for default values you need to keep in mind) without explicitly setting `EntityState`. – Guru Stron Jul 18 '23 at 08:52
  • @LukeVo also note that it is up to the EF provider to group the send command (for example SQLite does not). If in your case that does not work you will need to use 3rd party tool (like [EFCore.BulkExtensions](https://github.com/borisdj/EFCore.BulkExtensions)). See the update. – Guru Stron Jul 18 '23 at 08:54
  • I tried that one, see my updated code in my question, it results in Exception. I do not know why though, it should have been 3 rows affected but the exception said 1. – Luke Vo Jul 18 '23 at 08:58
  • I also checked your linked 3rd party tool, I am not sure which method is helping. Could you clarify? Thank you. – Luke Vo Jul 18 '23 at 09:13
  • @LukeVo was not able to repro. – Guru Stron Jul 18 '23 at 09:17
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/254533/discussion-between-guru-stron-and-luke-vo). – Guru Stron Jul 18 '23 at 09:18
  • 1
    Oh so I found out, if I remove the `required` from `Name`, and initialize the `DbItem` wihtout setting `Name`, it goes through. Guess I will report this bug to EF Core. – Luke Vo Jul 18 '23 at 09:19
  • 1
    Weird now that I am trying to submit a report and try to reproduce it, there is no exception anymore even when I add the `required` back. That's so strange but I am glad it works now. – Luke Vo Jul 18 '23 at 10:08