Skip to main content

How to Update a Row with EF Core and SaveChangesAsync

· 5 min read
Jagdish Kumawat
Founder @ Dewiride

To update a row with EF Core, load it, change the property, and call SaveChangesAsync. There's no Update call in that, and EF Core sends an UPDATE with only the columns that changed. If nothing changed, it sends nothing at all. This guide shows that in the real SQL, and compares it with the Update method.

Prerequisites​

  • A .NET C# project with EF Core. The examples use EF Core 10, Oracle's MySql.EntityFrameworkCore, MySQL 9.7, and a categories table. SQL logging is on, as in How to See the SQL That EF Core Sends.

Step 1: Load, Change, Save​

src/Dewiride.Inventory.Api/Features/Categories/CategoryEndpoints.cs (excerpt)
var category = await db.Categories
.FirstOrDefaultAsync(c => c.Id == id, cancellationToken);

if (category is null)
{
return TypedResults.NotFound();
}

category.Name = request.Name.Trim();

await db.SaveChangesAsync(cancellationToken);

The query has no Select, so it returns the entity, and the change tracker keeps a copy of its values. EF Core's Basic SaveChanges page says: "EF automatically detects changes made to an existing entity that is tracked by the context." So changing the property is all you do.

Step 2: Read the SQL​

Renaming a category to Wall chargers:

API log
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (6ms) [Parameters=[@id='?' (DbType = Int32)], CommandType='Text', CommandTimeout='30']
SELECT `c`.`Id`, `c`.`CreatedAtUtc`, `c`.`Name`
FROM `categories` AS `c`
WHERE `c`.`Id` = @id
LIMIT 1
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (4ms) [Parameters=[@p1='?' (DbType = Int32), @p0='?' (Size = 100)], CommandType='Text', CommandTimeout='30']
UPDATE `categories` SET `Name` = @p0
WHERE `Id` = @p1;
SELECT ROW_COUNT();

A terminal with the API log: a SELECT that loads the three columns of the row, followed by an UPDATE that sets only Name, and SELECT ROW_COUNT.

The SELECT read all three columns, and the UPDATE wrote one. CreatedAtUtc isn't there, so nothing can overwrite it by accident. SELECT ROW_COUNT() is EF Core checking that one row changed. When it reports zero, EF Core throws DbUpdateConcurrencyException.

Step 3: Save With No Changes​

Send the same name again:

API log
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (1ms) [Parameters=[@id='?' (DbType = Int32)], CommandType='Text', CommandTimeout='30']
SELECT `c`.`Id`, `c`.`CreatedAtUtc`, `c`.`Name`
FROM `categories` AS `c`
WHERE `c`.`Id` = @id
LIMIT 1

Only the SELECT. EF Core compared the values, found no difference, and had nothing to send.

Step 4: Compare with the Update Method​

DbSet.Update is for an object EF Core isn't tracking, and it marks every property as changed:

A temporary endpoint
var updated = new Category { Id = id, Name = "Renamed without loading", CreatedAtUtc = new DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc) };
db.Categories.Update(updated);
await db.SaveChangesAsync();
API log
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (2ms) [Parameters=[@p2='?' (DbType = Int32), @p0='?' (DbType = DateTime), @p1='?' (Size = 100)], CommandType='Text', CommandTimeout='30']
UPDATE `categories` SET `CreatedAtUtc` = @p0, `Name` = @p1
WHERE `Id` = @p2;
SELECT ROW_COUNT();

It saves a SELECT, but it writes every column, so you must supply every value, including CreatedAtUtc, and whatever you put there replaces what's stored. It also gives you no 404, and throws for an ID that doesn't exist. Calling it after loading the same row fails outright.

ApproachQueriesColumns writtenMissing row
Load, change, saveSELECT + UPDATEOnly the changed onesYou can answer 404
Update on a new objectUPDATEAll of themDbUpdateConcurrencyException
ExecuteUpdateAsyncUPDATEThe ones you nameReturns 0

Common Mistakes​

MistakeWhat happensDo this instead
Calling Update on a loaded entityEvery column is marked changedJust save
Loading with AsNoTracking, then changing itSaveChangesAsync sends nothingLoad with tracking
Forgetting SaveChangesAsyncThe change stays in memoryCall it
Two people editing the same rowThe second silently overwrites the firstUse a concurrency token

FAQ​

How does EF Core know what changed?​

It keeps a copy of the values from when it loaded the row, and compares them when you save. How to See What EF Core Will Save shows that comparison.

Can I update several rows in one save?​

Yes. Change several tracked objects, then call SaveChangesAsync once. All the changes go in one transaction.

Conclusion​

Load the row, change its properties, and call SaveChangesAsync. EF Core sends an UPDATE with only the changed columns, or nothing if nothing changed, and there's no Update method to call. Use DbSet.Update only for objects you didn't load, knowing it writes every column.

Additional Resources​

Stay Updated

Subscribe to our newsletter for the latest tutorials, tech insights, and developer news.

By subscribing, you agree to our privacy policy. Unsubscribe at any time.