Handling data concurrency in a web application is a critical aspect to ensure data integrity, especially when dealing with multiple users making simultaneous edits. This example, built on Dotnet Core 6 and Entity Framework Core 6, demonstrates an effective implementation of Optimistic Concurrency Control.
In a web application, the challenge arises when multiple users attempt to edit the same data concurrently. This scenario can lead to data overwrites, a common issue when several users access identical data simultaneously. The risk is especially evident when one user is editing a record, and another user concurrently saves changes, inadvertently overwriting the modifications made by the first user.
To address this challenge, implementing a concurrency control mechanism becomes imperative. This mechanism prevents simultaneous edits by different users and can be achieved through techniques like using a version number or a timestamp to track changes made to a record. When a user attempts to save modifications, the application checks whether the record has been altered by another user since it was last loaded. If modifications are detected, the application prompts the user to reload the record and reapply the changes, ensuring data consistency.
The primary goal of this project is to establish a system that ensures accurate and non-repetitive numbering of documents, all while incorporating Optimistic Concurrency Control. The challenge becomes apparent when two users simultaneously receive the latest document number. If one processes it faster than the other, the last document number held by the slower user becomes outdated. To counter this, the system employs a version number to track changes made to the record. When attempting to save modifications, the application checks if the record has been altered by another user since it was last loaded. If modifications are identified, the application notifies the user with an error message, prompting them to reload the record and reapply the changes.
Now, let's delve into the key components and the step-by-step implementation of this solution:
The first step involves creating an interface, ICheckConcurrency
, ensuring that domain models implement Concurrency Control.
This interface includes a RowVersion
property, acting as a unique identifier for concurrency checks.
public interface ICheckConcurrency
{
Guid RowVersion { get; set; }
}
The DocumentNumber
domain model implements the ICheckConcurrency
interface, signaling that it participates in concurrency control. The RowVersion
field uses the ConcurrencyCheck
DataAnnotation, instructing the ORM to perform Concurrency Control.
public class DocumentNumber : ICheckConcurrency, IAudit
{
// ... Other properties and methods ...
[ConcurrencyCheck]
public Guid RowVersion { get; set; }
// ... Other properties and methods ...
}
Within the ApplicationDbContext
class, we override the SaveChangesAsync
method to ensure that, during the save operation, the RowVersion
for entities implementing ICheckConcurrency
receives a different value.
public class ApplicationDbContext : DbContext
{
// ... Other DbContext configurations ...
public override Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
{
ApplyConcurrencyUpdates();
ApplyAuditUpdates();
return base.SaveChangesAsync(cancellationToken);
}
private void ApplyConcurrencyUpdates()
{
var entities = ChangeTracker.Entries<ICheckConcurrency>()
.Where(e => e.State is EntityState.Modified or EntityState.Added);
foreach (var entityEntry in entities)
{
entityEntry.Entity.RowVersion = Guid.NewGuid();
}
}
private void ApplyAuditUpdates()
{
// ... Apply updates for audit fields ...
}
}
The GenerateDocNumberAsync
method outlines the intricate process of generating a document number while handling Concurrency Control. The key aspect is the incorporation of a retry mechanism to address concurrency issues and reattempt the operation.
public async ValueTask<string> GenerateDocNumberAsync(
DocumentType documentType,
string prefix,
string code,
CancellationToken cancellationToken)
{
int maxRetryCount = 3;
int retryCount = 0;
bool success = false;
var currentYear = DateTime.Now.Year;
DocumentNumber? lastDocumentNumber = null;
while (retryCount < maxRetryCount && !success)
{
try
{
if (lastDocumentNumber != null)
{
// Detach the tracked entity
_context.Entry(lastDocumentNumber).State = EntityState.Detached;
lastDocumentNumber = null;
}
// 1. Get the last documentNumber by document type, code, and current year
lastDocumentNumber = await _context.DocumentNumbers
.Where(d => d.DocumentType == documentType && d.Code == code && d.Year == currentYear)
.OrderByDescending(d => d.Id)
.FirstOrDefaultAsync(cancellationToken)
.ConfigureAwait(false);
// 2. Create a new documentNumber or update the last documentNumber
long sequenceNumber = 1;
if (lastDocumentNumber == null)
{
// Create a new documentNumber
lastDocumentNumber =
new DocumentNumber(documentType: documentType, year: currentYear, code: code);
lastDocumentNumber.SetSequenceNumber(sequenceNumber);
await _context.AddAsync(lastDocumentNumber, cancellationToken);
}
else
{
// Update the last documentNumber
sequenceNumber = lastDocumentNumber.SequenceNumber + 1;
lastDocumentNumber.SetSequenceNumber(sequenceNumber);
// Mark the SequenceNumber property as modified
_context.Entry(lastDocumentNumber).Property(d => d.SequenceNumber).IsModified = true;
}
// 3. Save the documentNumber
await _context.SaveChangesAsync(cancellationToken);
success = true;
// Format the document number: prefix(45) + budgetCode(2728501) + sequenceNumber(00001) = 45272850100001
return GetFormattedDocumentNumber(lastDocumentNumber);
}
// 4. Handle concurrency exception and retry from step 1
catch (DbUpdateConcurrencyException e)
{
retryCount++;
Console.WriteLine(e);
}
catch (Exception e)
{
Console.WriteLine(e);
// Handle other exceptions and break the loop
break;
}
}
// Maximum retry count reached or save operation failed, handle the failure case here
throw new Exception("Failed to generate document number.");
}
In conclusion, this comprehensive approach ensures a robust solution for managing data concurrency, providing a secure and reliable system for generating document numbers in a web application.