I ran into an interesting EF Core issue while working with PostgreSQL. The task was simple: add a new message to a chat. Instead, I got a concurrency exception that made EF think I was updating a row that didn't exist.
The Setup
Chat is the conversation container; Message are the individual messages linked via ChatId.
public class Chat
{
public int Id { get; set; }
public string Name { get; set; }
public ICollection<Message> Messages { get; set; }
}
public class Message
{
public int Id { get; set; }
public int ChatId { get; set; }
public string Text { get; set; }
public DateTime CreatedAt { get; set; }
public Chat Chat { get; set; }
}
The "Obvious" Code That Failed
I loaded the chat and appended to its navigation property:
using var db = new AppDbContext(options);
var chat = db.Chats.Find(1);
var message = new Message
{
ChatId = 1,
Text = "Hello from EF!",
CreatedAt = DateTime.UtcNow
};
chat.Messages.Add(message);
db.SaveChanges();
Instead of inserting, EF Core threw:
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.
Why was EF expecting to affect exactly one row on an update when I was trying to insert?
Turning On SQL Logging
To see what EF Core was doing, I enabled SQL logging.
Option A β Inside DbContext
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
public class AppDbContext : DbContext
{
private static readonly ILoggerFactory _loggerFactory =
LoggerFactory.Create(builder =>
{
builder.AddConsole().SetMinimumLevel(LogLevel.Information);
});
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder
.UseLoggerFactory(_loggerFactory)
.LogTo(Console.WriteLine,
new[] { DbLoggerCategory.Database.Command.Name },
LogLevel.Information)
.EnableSensitiveDataLogging()
.EnableDetailedErrors()
.UseNpgsql("Host=localhost;Database=app;Username=app;Password=pass");
}
public DbSet<Chat> Chats => Set<Chat>();
public DbSet<Message> Messages => Set<Message>();
}
Note: EnableSensitiveDataLogging() prints parameter values. Great for debugging; turn it off in production.
Option B β ASP.NET Core via appsettings.json
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Information",
"Microsoft.EntityFrameworkCore.Database.Command": "Information"
}
}
}
What the Logs Looked Like
β
Good Path (adding via DbSet)
When I changed to db.Messages.Add(message), EF produced a normal INSERT:
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (34ms) [Parameters=[@p0='1', @p1='Hello from EF!' (Size = 4000), @p2='2025-08-09T02:11:33.1234567Z'], CommandType='Text', CommandTimeout='30']
INSERT INTO "Messages" ("ChatId", "Text", "CreatedAt")
VALUES (@p0, @p1, @p2)
RETURNING "Id";
β Problem Path (adding via navigation)
With chat.Messages.Add(message), EF tried to update a row that didn't exist:
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (12ms) [Parameters=[@p0='1', @p1='Hello from EF!', @p2='2025-08-09T02:12:10.9876543Z', @p3='42'], CommandType='Text', CommandTimeout='30']
UPDATE "Messages" SET "ChatId" = @p0, "Text" = @p1, "CreatedAt" = @p2
WHERE "Id" = @p3;
warn: Microsoft.EntityFrameworkCore.Update[10000]
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.
The WHERE "Id" = @p3 didn't match any row (because it didn't exist), so 0 rows were affected and EF raised the concurrency exception.
Why This Happens
EF Core decides whether an entity is new (Added) or existing (Modified) based on the primary key value:
- Default key value (
0forint, empty forGuid) →Added(will insert). - Non-default key value →
Modified(will update).
When adding via a navigation property, EF tries to infer state. Depending on how the parent was tracked or how the key is configured, EF may treat the child as existing and attempt an update.
The Fixes
1) Add via the DbSet (safest)
db.Messages.Add(message);
db.SaveChanges();
2) If you prefer navigation, force the state
chat.Messages.Add(message);
db.Entry(message).State = EntityState.Added;
db.SaveChanges();
Lesson Learned
Adding related entities in EF Core isn't always as automatic as it looks. Navigation property additions only work if EF infers the state correctly. If it guesses wrong, you get a concurrency exception. My rule now:
When in doubt, add via the
DbSetfor new entities. If using navigation properties, explicitly set the state.