Reputation: 8902
We are using EF Core 6 and currently seeing one of the API in PROD is throwing a DbUpdateConcurrencyException
occasionally. As of now we do not have logic (concurrency check / RowVersion) implemented to manage the optimistic concurrency. I want to reproduce the same error while debugging, so I followed these steps
SaveChanges()
from the codeSurprisingly, this did not throw any error while debugging. I'm wondering why these concurrent updates are causing an exception in PROD, but not while debugging?
Upvotes: 1
Views: 822
Reputation: 34653
If the table doesn't have a concurrency token set then you won't receive update concurrency errors (I.e. stale data) but you will receive delete concurrency errors relating to deletes. These are the errors that report "Database operation expected to affect 1 row(s) but actually affected 0 row(s)"
You can reproduce this by editing an existing entity, then in the DB, delete that entity, then call SaveChanges
. It can be confusing getting this error because it can occur when you don't have code that you think would ever delete a row.
If you have a system that does delete rows where there are multiple active sessions going on, it is something you will need to handle. If you are getting this when rows should not have been deleted, a common bug that can result in issues like this is setting collection navigation properties.
For instance given an Blog entity that has a list of Tags associated to it and you have a screen that adds or removes the relevant Tags on the blog. Code like this:
var blog = _context.Blogs
.Include(x => x.Tags)
.Single(x => x.BlogId == blogId);
blog.Tags = updatedTags;
... is bad. Setters would be used on singular navigation properties but should never be used on collections. Add and remove relevant tags from the collection individually as EF's change tracker is tied to the Tags collection set up when the Blog entry was read.
Other culprits to check are things like unintentional edits with warnings that may have been missed that may have tainted the DbContext.
For instance you might have a line of code somewhere doing a check like:
if (blog.Status = Statuses.Draft)
instead of:
if (blog.Status == Statuses.Draft)
The compiler will generate a warning about a potential unintended assignment in the first statement with a squiggle, but if your solution already has dozens of compiler warnings that you are accustomed to ignore, this may be missed where you don't intend to update a Blog and when saving something unrelated it tries updating a blog or other such entity you aren't expecting leading you on a wild goose chase. Useful steps to help avoid this:
AsNoTracking()
or projection (select anonymous types or DTO/ViewModels) in queries rather that reading tracked entities. You'll still have a "bug" with the unintended assignment to resolve, but it won't poison the DbContext.Tracking down one of these tainted entities with a debugger can be a bit tricky but if you do happen to break on an exception with a reproducible example, look at the change tracker to inspect what entities the DbContext is trying to update. You may spot an entity you didn't expect to see. As for production you might have some luck with a profiler capturing SQL statements on the database then going through everything doing an update and finding something that resulted in no rows being updated.
Upvotes: 3