EF Core rowversion vs JPA @version: How Concurrency Tokens Differ
A developer moving from Spring Boot to EF Core explains how SQL Server's rowversion differs from JPA's @version field. The value comes from a database-wide counter, so it is not a per-row version number. This matters for .NET developers who need to set up optimistic concurrency correctly.
Key points
- SQL Server rowversion is an 8-byte binary value generated from a database-wide counter.
- A row's rowversion changes only when that row itself is inserted or updated.
- Mapping with .IsRowVersion() makes EF Core include the original token in the UPDATE condition.
- If zero rows match, EF Core throws DbUpdateConcurrencyException for the application to handle.
- Hibernate's @version is a per-entity number incremented by the ORM, so the two should not be treated as identical.
The article compares how JPA/Hibernate's @version and EF Core's rowversion property protect against lost updates. In Hibernate, the ORM increments a numeric version per entity. In SQL Server, rowversion is an 8-byte binary value taken from a database-wide counter, and it gets a new value whenever a row with that column is inserted or updated. The author shows that changing one row does not alter the stored value of another row, which is the key point that surprised them.
In practice, EF Core adds the original token to the WHERE clause of the UPDATE. If the value has changed in the meantime, zero rows are affected and EF Core throws DbUpdateConcurrencyException, so the application can resolve the conflict instead of overwriting data. The piece concludes that both approaches implement optimistic concurrency, but a developer should not treat the database-generated token as if it were a per-record version counter.
Real-world use
- An admin panel where several users edit the same product record.
- An ASP.NET Core API on SQL Server that must prevent lost writes on order or stock updates.
- Teams migrating from Spring Boot to .NET who need to re-map their concurrency rules.
Why it matters
Treating rowversion as a per-row version number leads to wrong assumptions, since the stored value also moves when other rows change. Conflict handling code should be written with that behavior in mind.
Related stories
- Designing out double charges and overselling in an ASP.NET Core POSDEV Community: #aspnetcore6 d ago
- SQL Server Msg 1934: why QUOTED_IDENTIFIER errors happen and how to fix themDEV Community: #dotnetOct 23
- Sending leads to Odoo without losing one: a transactional outbox in ASP.NET CoreDEV Community: #aspnetcore2 d ago