| name | handle-optimistic-concurrency |
| description | Enable and handle ABP optimistic concurrency control so concurrent updates to the same record are detected and rejected. USE FOR: enabling concurrency with IHasConcurrencyStamp/ConcurrencyStamp, deriving from aggregate root base classes to get it for free, flowing the stamp through output and update DTOs, handling AbpDbConcurrencyException when two users update the same record. DO NOT USE FOR: EF-Core-specific mapping/change tracking (use ef-core-integration), MongoDB provider setup (use mongodb-integration), unit-of-work flush/transaction control (use manage-units-of-work), general validation/error surfacing (use handle-validation-and-errors).
|
| license | MIT |
Handle Optimistic Concurrency in ABP
ABP's concurrency check uses optimistic concurrency control: users can attempt to update the same record without being blocked up front. When one update wins, the others are rejected because the record has changed underneath them and must be re-read before retrying.
When to Use
- You need to detect and reject concurrent updates to the same record instead of silently overwriting.
- You want to enable concurrency control on an entity via
IHasConcurrencyStamp / ConcurrencyStamp.
- You want concurrency for free by deriving from an aggregate root base class.
- You need to flow the stamp from the read DTO back through the update DTO so a stale client is rejected.
- You need to handle or surface
AbpDbConcurrencyException.
When Not to Use
- EF-Core-specific mapping or change tracking — use ef-core-integration.
- MongoDB provider setup — use mongodb-integration (this skill only notes provider-specific stamp behavior).
- Unit-of-work flush / transaction control — use manage-units-of-work.
- General validation and user-facing error handling — use handle-validation-and-errors.
How it works
IHasConcurrencyStamp
Implement this interface (directly or indirectly) to enable concurrency control on an entity:
public interface IHasConcurrencyStamp
{
string ConcurrencyStamp { get; set; }
}
How ABP uses the stamp:
- On create, behavior is provider-specific. With EF Core,
AbpDbContext runs SetConcurrencyStampIfNull on added entities, so a null ConcurrencyStamp is filled in automatically. With MongoDB, insert does not set the stamp — the aggregate root base classes work only because their constructor generates one. A plain Entity<TKey>, IHasConcurrencyStamp on MongoDB must initialize ConcurrencyStamp itself.
- On update, ABP compares the entity's current
ConcurrencyStamp against the value stored in the database. If they match, it saves and ABP itself generates a new stamp (Guid.NewGuid().ToString("N") in framework code — not by the database). If they mismatch, it throws AbpDbConcurrencyException.
Get it via a base class
The aggregate root base classes already implement IHasConcurrencyStamp, so deriving from any of these gives you concurrency control without implementing it yourself:
AggregateRoot, AggregateRoot<TKey>
CreationAuditedAggregateRoot, CreationAuditedAggregateRoot<TKey>
AuditedAggregateRoot, AuditedAggregateRoot<TKey>
FullAuditedAggregateRoot, FullAuditedAggregateRoot<TKey>
public class Book : FullAuditedAggregateRoot<Guid>
{
public string Name { get; set; }
}
If your entity derives from a plain Entity<TKey>, implement the interface explicitly. On MongoDB, initialize the stamp yourself (EF Core fills a null stamp on insert, but MongoDB does not):
public class Book : Entity<Guid>, IHasConcurrencyStamp
{
public string ConcurrencyStamp { get; set; } = Guid.NewGuid().ToString("N");
}
Flow the stamp through DTOs
The stamp only protects an update if the client sends back the stamp it last read. So expose it on the output DTO and require it on the update DTO:
public class BookDto : EntityDto<Guid>, IHasConcurrencyStamp
{
public string ConcurrencyStamp { get; set; }
}
public class UpdateBookDto : IHasConcurrencyStamp
{
public string ConcurrencyStamp { get; set; }
}
In the application service's update method, copy the incoming stamp onto the entity before saving:
public class BookAppService : ApplicationService, IBookAppService
{
public virtual async Task<BookDto> UpdateAsync(Guid id, UpdateBookDto input)
{
var book = await BookRepository.GetAsync(id);
book.ConcurrencyStamp = input.ConcurrencyStamp;
await BookRepository.UpdateAsync(book, autoSave: true);
return ObjectMapper.Map<Book, BookDto>(book);
}
}
Because the client received ConcurrencyStamp in BookDto and sent the same value in UpdateBookDto, a second user editing a stale copy will send an old stamp, the comparison mismatches, and AbpDbConcurrencyException is thrown.
Save changes to get the new stamp
The stamp is validated and the next one is generated by ABP framework code (Guid.NewGuid().ToString("N")), not by the database engine. When exactly the entity picks up the regenerated stamp is provider-specific. With EF Core, UpdateConcurrencyStamp runs at SaveChanges time, so inside a unit of work you must trigger a flush to read the new value — pass autoSave: true to UpdateAsync/InsertAsync (as above) or call CurrentUnitOfWork.SaveChangesAsync(); otherwise the entity holds the old stamp until the UOW completes. With MongoDB, UpdateAsync regenerates the stamp on the entity right before ReplaceOneAsync, independent of autoSave.
Handling AbpDbConcurrencyException
AbpDbConcurrencyException lives in Volo.Abp.Data and derives from AbpException. You have two options:
- Let ABP handle it — if you don't catch it, ABP surfaces a user-friendly error message automatically. This is the common case; often you don't write any catch at all.
- Handle it yourself — catch it to run custom recovery, e.g. tell the user their copy is stale and reload:
try
{
await _bookAppService.UpdateAsync(id, input);
}
catch (AbpDbConcurrencyException)
{
}
Since resolving a conflict requires the caller to fetch the latest data and re-apply changes, the typical UX is: catch (or surface ABP's default message), reload the current record with its fresh ConcurrencyStamp, and let the user retry.
Validation
- Confirm the entity implements
IHasConcurrencyStamp (directly or via an aggregate root base class) and that both the output and update DTOs carry ConcurrencyStamp.
- Read a record, then update the same record twice with the original stamp: the first update succeeds, the second throws
AbpDbConcurrencyException.
- With EF Core, after
UpdateAsync(book, autoSave: true) verify the returned DTO's ConcurrencyStamp differs from the one sent in — proving the stamp regenerated.
Common Pitfalls
- MongoDB does not fill a null stamp on insert. A plain
Entity<TKey>, IHasConcurrencyStamp on MongoDB must initialize ConcurrencyStamp itself; EF Core fills it via SetConcurrencyStampIfNull, MongoDB does not.
- Not flushing under EF Core leaves the old stamp.
UpdateConcurrencyStamp runs at SaveChanges time, so inside a UOW the entity keeps the old stamp until you pass autoSave: true or call CurrentUnitOfWork.SaveChangesAsync().
- Forgetting to copy
input.ConcurrencyStamp onto the entity before saving defeats the check — a stale client would silently overwrite the winner.
- The new stamp comes from ABP framework code, not the database (
Guid.NewGuid().ToString("N")) — don't expect a DB-generated rowversion.