ASP.NET Core  

Building a Document Overwriting Protection System (Leases, Locks, Expiry) with Angular + .NET

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:

  1. Client (Angular)
    Requests lock, renews lock, checks status, and saves changes only if lock is valid.

  2. Server (ASP.NET Core)
    Manages lock creation, validation, expiry, release, and conflict detection.

  3. 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:

  • Lease lock

  • Version token

This ensures total protection.

6. Lock History

Store lock events:

  • Who locked

  • When

  • Duration

  • Who force-released

Best Practices

  1. Keep lock duration short (3–5 minutes).

  2. Implement regular auto-renew.

  3. Store expiry server-side (never in client).

  4. For high concurrency, use Redis locks.

  5. Prevent lock poisoning by validating the user ID.

  6. Build clear UI warnings and messages.

  7. Release locks on navigation and tab close.

  8. Use optimistic concurrency for document content.

  9. 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:

  • Lease-based locks

  • Expiry

  • Safe renewals

  • Optimistic concurrency

  • Clear UI notifications

  • Force unlock and admin tools

This architecture is scalable, reliable, and easy to extend.