Document overwriting issues are common in enterprise applications where multiple users interact with the same document at the same time. When there is no controlled concurrency, users may overwrite each other’s changes, delete active versions, or corrupt file metadata.
This is especially problematic in systems like:
Document Management Systems
Work Order or Job Card modules
ERP systems
HR document review
Financial approval workflows
Ticketing or case management systems
To solve this problem, modern platforms rely on document locking, leases, expiry, and safe conflict resolution.
This article explains how to build a Document Overwriting Protection System using Angular (front-end) and .NET 8 (back-end).
The solution is metadata-driven, resilient, and suitable for enterprise-scale multi-user environments.
What This System Provides
The final system supports:
Soft locks and hard locks
Lease-based editing (time-bound exclusive access)
Auto-expiry and auto-renewable locks
Distributed-safe locking (SQL-based or Redis-based)
Optimistic concurrency using version tokens
Conflict detection
Force-release and admin override
Real-time lock status indicators in Angular
Lock viewer to see who is editing what
Clean fallback behaviour for offline or crashed clients
This article goes step-by-step from the architecture to Angular UI interactions and .NET implementation.
System Architecture Overview
A Document Protection System uses three layers:
Client (Angular)
Requests lock, renews lock, checks status, and saves changes only if lock is valid.
Server (ASP.NET Core)
Manages lock creation, validation, expiry, release, and conflict detection.
Database (SQL Server / Redis / Hybrid)
Stores lock metadata and ensures atomicity.
Workflow Diagram (High-Level Architecture)
+---------------------+
| Angular Application |
+----------+----------+
|
Lock/Status API Calls
|
v
+----------------------------+
| ASP.NET Core Lock Manager |
+-----+----------+-----------+
| |
Metadata Store |
(SQL Server / Redis)| Save/Validate
| |
v v
+-----------------------------+
| Document Locking Repository |
+-----------------------------+
Flowchart: Document Editing with Protection
+------------------------+
| User opens document |
+-----------+------------+
|
v
+-----------+------------+
| Request lock from API |
+-----------+------------+
|
+-----------+------------+
| Lock granted? |
+------+----------------+
|Yes
v
+---------+----------+
| Enter edit mode |
+---------+----------+
|
v
+-----------+-------------+
| User edits & saves doc |
+-----------+-------------+
|
v
+-----------+-------------+
| Validate lock still live|
+------+------------------+
|Yes
v
+---------+-----------+
| Save document |
+---------+-----------+
|
v
+---------+-----------+
| Release lock |
+---------------------+
If No (lock not granted), the UI shows who is editing and until when.
Understanding Lock Types
There are three commonly used lock models:
1. Hard Lock
Only the locking user can edit. Others can only read.
2. Soft Lock
Other users can edit but will be warned.
3. Lease Lock
Exclusive access for a fixed duration (e.g., 5 minutes).
Auto-renewal possible.
Our system uses Lease Lock with controlled renewal.
Designing the Lock Metadata (Database Table)
A simple SQL Server table structure:
CREATE TABLE DocumentLocks (
DocumentId UNIQUEIDENTIFIER NOT NULL,
LockedBy NVARCHAR(200) NOT NULL,
LockedAt DATETIME2 NOT NULL,
LeaseExpiry DATETIME2 NOT NULL,
VersionToken ROWVERSION,
PRIMARY KEY (DocumentId)
);
Fields
DocumentId: Unique ID of the document
LockedBy: Email or UserId
LockedAt: Time lock was acquired
LeaseExpiry: Auto-expiry time
VersionToken: Used for optimistic concurrency
Backend Implementation (.NET 8)
ASP.NET Core Controller Structure
POST /api/lock/acquire
POST /api/lock/renew
POST /api/lock/release
GET /api/lock/status/{documentId}
Lock Acquisition Logic (.NET 8)
public async Task<LockResponse> AcquireLockAsync(Guid documentId, string userId)
{
var now = DateTime.UtcNow;
var existing = await _db.DocumentLocks.FindAsync(documentId);
if (existing != null && existing.LeaseExpiry > now)
{
if (existing.LockedBy == userId)
{
// same user — renew
existing.LeaseExpiry = now.AddMinutes(5);
await _db.SaveChangesAsync();
return LockResponse.Success(existing);
}
return LockResponse.Failed(existing.LockedBy, existing.LeaseExpiry);
}
// No active lock or expired lock
var newLock = new DocumentLock
{
DocumentId = documentId,
LockedBy = userId,
LockedAt = now,
LeaseExpiry = now.AddMinutes(5)
};
_db.DocumentLocks.Update(newLock);
await _db.SaveChangesAsync();
return LockResponse.Success(newLock);
}
Lock Renewal API
public async Task<bool> RenewLockAsync(Guid documentId, string userId)
{
var lockRecord = await _db.DocumentLocks.FindAsync(documentId);
if (lockRecord == null) return false;
if (lockRecord.LockedBy != userId) return false;
lockRecord.LeaseExpiry = DateTime.UtcNow.AddMinutes(5);
await _db.SaveChangesAsync();
return true;
}
Lock Release API
public async Task<bool> ReleaseLockAsync(Guid documentId, string userId)
{
var lockRecord = await _db.DocumentLocks.FindAsync(documentId);
if (lockRecord == null) return true;
if (lockRecord.LockedBy != userId)
return false;
_db.DocumentLocks.Remove(lockRecord);
await _db.SaveChangesAsync();
return true;
}
Status Check API
public async Task<LockStatusDto> GetStatusAsync(Guid documentId)
{
var lockRecord = await _db.DocumentLocks.FindAsync(documentId);
if (lockRecord == null)
return LockStatusDto.Free();
if (lockRecord.LeaseExpiry < DateTime.UtcNow)
return LockStatusDto.Free();
return LockStatusDto.Locked(lockRecord.LockedBy, lockRecord.LeaseExpiry);
}
Adding Optimistic Concurrency (Version Tokens)
When saving, always check token:
db.Entry(document).OriginalValues["VersionToken"] = incomingToken;
await db.SaveChangesAsync(); // will throw DbUpdateConcurrencyException
Angular will receive a conflict response and prompt the user.
Angular Front-End Implementation
Angular handles UI notifications, auto-renewal timer, and safe blocking.
Lock Service (Angular)
@Injectable({ providedIn: 'root' })
export class DocumentLockService {
constructor(private http: HttpClient) {}
acquire(documentId: string) {
return this.http.post('/api/lock/acquire', { documentId });
}
renew(documentId: string) {
return this.http.post('/api/lock/renew', { documentId });
}
release(documentId: string) {
return this.http.post('/api/lock/release', { documentId });
}
status(documentId: string) {
return this.http.get(`/api/lock/status/${documentId}`);
}
}
Auto-Renew Timer (Angular)
startAutoRenew(documentId: string) {
timer(0, 60000).pipe(
switchMap(() => this.lockService.renew(documentId))
).subscribe();
}
Renew every 1 minute to maintain a 5-minute lease.
Document Component Logic
ngOnInit() {
this.lockService.acquire(this.docId).subscribe(response => {
if (response.success) {
this.lockAcquired = true;
this.startAutoRenew(this.docId);
} else {
this.showLockedByMessage(response.lockedBy, response.expiry);
}
});
}
ngOnDestroy() {
if (this.lockAcquired) {
this.lockService.release(this.docId).subscribe();
}
}
UI Feedback Example (Angular HTML)
<div *ngIf="!lockAcquired" class="warning">
This document is currently being edited by {{ lockedBy }} until {{ expiry | date:'short' }}
</div>
<textarea [disabled]="!lockAcquired"></textarea>
Handling Crashed Browsers, Network Failure, or User Closing Tab
Use:
window.beforeunload
@HostListener('window:beforeunload')
beforeUnload() {
if (this.lockAcquired) {
navigator.sendBeacon('/api/lock/release', JSON.stringify({ documentId: this.docId }));
}
}
This ensures lock release even if the tab closes.
Adding a Lock Viewer (Admin)
Admins can view:
Current active locks
Expiry timestamps
Stale or expired locks
Useful SQL query:
SELECT * FROM DocumentLocks ORDER BY LockedAt DESC;
Handling Conflicts on Save
If another user saves in between the edit window, return:
409 Conflict
Angular code:
this.http.post('/api/document/save', data).subscribe({
error: err => {
if (err.status === 409) {
this.openConflictDialog(err.error);
}
}
});
Real-World Enhancements
1. Redis-based Distributed Locking
For multi-server load-balanced systems.
2. Lock Escalation
Convert soft lock → hard lock if the user is editing for long.
3. Idle Tracking
Release lock if the user is idle for more than N minutes.
4. Lock Transfer
Allow admin to transfer a lock from one user to another.
5. Dual-Layer Protection
Use both:
This ensures total protection.
6. Lock History
Store lock events:
Who locked
When
Duration
Who force-released
Best Practices
Keep lock duration short (3–5 minutes).
Implement regular auto-renew.
Store expiry server-side (never in client).
For high concurrency, use Redis locks.
Prevent lock poisoning by validating the user ID.
Build clear UI warnings and messages.
Release locks on navigation and tab close.
Use optimistic concurrency for document content.
Provide admin override for forced unlock.
Conclusion
A robust Document Overwriting Protection System is mandatory for enterprise-grade applications.
Using Angular on the front-end and .NET 8 on the back-end, you can build a system that supports:
This architecture is scalable, reliable, and easy to extend.