13

I pull a bunch of timesheet entries out of the database and use them to create an invoice. Once I save the invoice and have an Id I want to update the timesheet entries with the invoice Id. Is there a way to bulk update the entities without loading them one at a time?

void SaveInvoice(Invoice invoice, int[] timeEntryIds) {
    context.Invoices.Add(invoice);
    context.SaveChanges();

    // Is there anything like?
    context.TimeEntries
        .Where(te => timeEntryIds.Contains(te.Id))
        .Update(te => te.InvoiceId = invoice.Id);
}
Phuc Thai
  • 718
  • 7
  • 17
Adrian Brand
  • 20,384
  • 4
  • 39
  • 60

8 Answers8

17

Disclaimer: I'm the owner of the project Entity Framework Plus

Our library has a Batch Update feature which I believe is what you are looking for

This feature supports EF Core

// Is there anything like? YES!!!
context.TimeEntries
    .Where(te => timeEntryIds.Contains(te.Id))
    .Update(te => new TimeEntry() { InvoiceId = invoice.Id });

Wiki: EF Batch Update

EDIT: Answer comment

does it supports contains as in your example? I think this is coming from EF Core which is not supported feature in 3.1 version even

EF Core 3.x support contains: https://dotnetfiddle.net/DAdIO2

EDIT: Answer comment

this is great but this requires to have zero parameter public constructors for classes. which is not a great. Any way to get around this issue?

Anonymous type is supported starting from EF Core 3.x

context.TimeEntries
    .Where(te => timeEntryIds.Contains(te.Id))
    .Update(te => new { InvoiceId = invoice.Id });

Online example: https://dotnetfiddle.net/MAnPvw

Jonathan Magnan
  • 10,874
  • 2
  • 38
  • 60
  • this is great but this requires to have zero parameter public constructors for classes. which is not a great. Any way to get around this issue? – akd Apr 30 '21 at 09:09
15

As of EFCore 7.0 you will see the built-in BulkUpdate() and BulkDelete methods:

   context.Customers.Where(...).ExecuteDelete();
   context.Customers.Where(...).ExecuteUpdate(c => new Customer { Age = c.Age + 1 });
   context.Customers.Where(...).ExecuteUpdate(c => new { Age = c.Age + 1 });
   context.Customers.Where(...).ExecuteUpdate(c => c.SetProperty(b => b.Age, b => b.Age + 1));
Majid Shahabfar
  • 4,010
  • 2
  • 28
  • 36
  • 4
    A note to anyone who finds this - EFCore 7 isnt released yet and is expected to be released November 2022. – Jono Jun 22 '22 at 22:55
  • 1
    the API is ExecuteDelete()/ExecuteUpdate() in the final release version. – Rm558 Jan 03 '23 at 23:22
  • ExecuteDelete()/ExecuteUpdate() are not working with `InMemory` Please check this here https://stackoverflow.com/questions/74907256/ef-7-new-executedelete-and-executeupdate-methods-not-working-on-an-in-memory-d – Murali Murugesan Jan 24 '23 at 15:03
8

Are you after the performance of simplified syntax?

I would suggest to use direct SQL query,

 string query = "Update TimeEntries Set InvoiceId = <invoiceId> Where Id in (comma separated ids)";    
 context.Database.ExecuteSqlCommandAsync(query);

For comma separated ids you can do string.Join(',', timeEntryIds)

It depends on what you actually need. If you want to go with Linq, then you need to iterate through each object.

Dhanuka777
  • 8,331
  • 7
  • 70
  • 126
  • SQL Injection Attack !! Everybody using EF knows that they can fire SQL but it is not recommended. – Akash Kava May 19 '21 at 08:12
  • 1
    Microsofts own EF documentation recommends raw sql inserts for bulk updating for efficiency: https://learn.microsoft.com/en-us/ef/core/performance/efficient-updating – MDave Feb 21 '22 at 09:25
6

If TimeEntry has an association to Invoice (check the navigation properties), you can probably do something like this:

var timeEntries = context.TimeEntries.Where(t => timeEntryIds.Contains(te.Id)).ToArray();

foreach(var timeEntry in timeEntries)
    invoice.TimeEntries.Add(timeEntry);

context.Invoices.Add(invoice);

//save the entire context and takes care of the ids
context.SaveChanges();
DesertFox
  • 768
  • 1
  • 4
  • 6
  • Instead of doing this I added the TimeEntries to the invoice before I saved it. invoice.TimeEntries = context.TimeEntries.Where(te => timeEntryIds.Contains(te.Id)).ToArray() – Adrian Brand Apr 21 '17 at 02:11
4

The IQueryable.ToQueryString method introduced in Entity Framework Core 5.0 may help with this scenario. This method will generate SQL that can be included in a raw SQL query to perform a bulk update of records identified by that query.

For example:

void SaveInvoice(Invoice invoice, int[] timeEntryIds) {
    context.Invoices.Add(invoice);
    context.SaveChanges();

    var query = context.TimeEntries
        .Where(te => timeEntryIds.Contains(te.Id))
        .Select(te => te.Id);

    var sql = $"UPDATE TimeEntries SET InvoiceId = {{0}} WHERE Id IN ({query.ToQueryString()})";

    context.Database.ExecuteSqlRaw(sql, invoice.Id);
}

The major drawback of this approach is that you end up with raw SQL appearing in your code. However I don't know of any reasonable way to avoid that with current Entity Framework Core capabilities - you're stuck with this caveat, or the caveats of other answers posted here such as:

  • Introducing a dependency on another library such as Entity Framework Plus or ELinq.
  • Using DbContext.SaveChanges() which will involve the execution of multiple SQL queries to retrieve and update records one at a time rather than doing a bulk update.
chrisg
  • 1,117
  • 8
  • 8
2

In entity framework core , you can do with update range method. you can see some samples usage here .

using (var context = new YourContext())
{
     context.UpdateRange(yourModifiedEntities);

     // or the followings are also valid
     //context.UpdateRange(yourModifiedEntity1, yourModifiedEntity2, yourModifiedEntity3);
    //context.YourEntity.UpdateRange(yourModifiedEntities);
    //context.YourEntity.UpdateRange(yourModifiedEntity1, yourModifiedEntity2,yourModifiedEntity3);

    context.SaveChanges();
  }
nzrytmn
  • 6,193
  • 1
  • 41
  • 38
  • Be aware that while EF improves performance by executing an UPDATE statement per entity, it doesn't run a single UPDATE statement that updates all entities at once, like @Dhanuka777's example above. The latter is much, much faster. – Sandor Drieënhuizen Apr 19 '19 at 11:09
  • @SandorDrieënhuizen I don't understand how executing an UPDATE statement per entity is faster than a single UPDATE. So you say running update 10.000 times is faster than running it once? Come on. – schlingel Apr 29 '20 at 12:00
  • 1
    @schlingel: Even though you're only calling a single `UpdateRange` command, EF generates an SQL UPDATE statement for each entity under the hood. Thus, you're still effectively doing lots of separate update queries which is much slower than a single combined UPDATE query. – Sandor Drieënhuizen May 05 '20 at 14:08
2

Bulk update supported with EF 7:

context
.TimeEntries
.Where(te => timeEntryIds.Contains(te.Id))
.ExecuteUpdate(s => s.SetProperty(
    i => te.InvoiceId,
    i => invoice.Id));

Also there is async version for this method ExecuteUpdateAsync.

Saad Qais
  • 592
  • 5
  • 8
1

in EF Core 7, use ExecuteUpdate(), what's new

var multipleRows = TableA.Where(t=>t.Id < 99);

multipleRows.ExecuteUpdate(t=> 
    t.SetProperty(
        r => r.Salary,
        r => r.Salary * 2));
    
//SQL already sent to database, do not run below
//SaveChanges(); 

SQL being generated by EF

UPDATE [t]
SET [t].[Salary] = [t].[Salary] * 2
FROM [TableA] AS [t]
WHERE [t].[ID] < 99
Rm558
  • 4,621
  • 3
  • 38
  • 43