AssetMVCVSAAugust 2026 · 8 min read

MVC Asset Manager Architecture: VSA, Lifecycle Enforcement, and Depreciation Engine

TL;DR

MVC Asset Manager is built on Vertical Slice Architecture with CQRS handlers per feature. It enforces asset assignment rules (no double assignment), calculates depreciation using multiple methods, guards lifecycle transitions, and exposes Minimal API endpoints consumed by a Vue 3 dashboard with QR/barcode integration.

Architecture decisions in an asset management system have real consequences: a poorly designed assignment rule can let two employees claim the same laptop, and a missing lifecycle guard can corrupt audit data. MVC Asset Manager addresses these risks with Vertical Slice Architecture (VSA), explicit CQRS handlers, and a rules engine that enforces business invariants at the handler level — before any database write occurs. This article walks through each architectural layer in detail.

Vertical Slice Architecture: Feature-First Organization

MVC Asset Manager organizes code around features, not technical layers. Instead of top-level Controllers/, Services/, and Models/ folders, each asset operation lives inside its own vertical slice under Features/Asset/:

Features/
  Asset/
    AssignAsset/
      AssignAssetCommand.cs
      AssignAssetHandler.cs
      AssignAssetEndpoint.cs
    CreateAsset/
      CreateAssetCommand.cs
      CreateAssetHandler.cs
      CreateAssetEndpoint.cs
    UpdateAssetStatus/
      UpdateAssetStatusCommand.cs
      UpdateAssetStatusHandler.cs
      UpdateAssetStatusEndpoint.cs
    GetAssetById/
      GetAssetByIdQuery.cs
      GetAssetByIdHandler.cs
      GetAssetByIdEndpoint.cs

Each slice is self-contained: the command or query DTO, the handler with business logic, and the Minimal API endpoint that wires it to HTTP. This structure keeps related code together, reduces merge conflicts, and makes it trivial to locate every piece of a feature — from request shape to database write — in a single folder.

CQRS Handlers for Asset Operations

Every asset operation flows through a CQRS handler. Commands (mutations) and queries (reads) are separated, with MediatR dispatching each request to its handler. The handler owns the business logic, validation, and persistence in one place. This pattern eliminates service-layer sprawl and keeps business rules close to the data they govern.

Handlers receive a request DTO, validate preconditions, execute domain logic, and return a Result or Result<T> — a discriminated union that carries either success data or a list of errors. No exceptions for control flow; every failure path is explicit and traceable.

Asset Assignment Rules: No Double Assignment

The most critical business rule in asset management is that one asset cannot be assigned to two employees at the same time. MVC Asset Manager enforces this at the handler level with a guard clause that checks for any active (unreturned) assignment before allowing a new one:

public async Task<Result> Handle(AssignAssetCommand request, CancellationToken ct)
{
    var asset = await _context.Assets
        .Include(a => a.Assignments)
        .FirstOrDefaultAsync(a => a.Id == request.AssetId, ct);

    if (asset is null)
        return Result.Failure("Asset not found.");

    if (asset.Status != AssetStatus.Ready)
        return Result.Failure("Only assets in Ready status can be assigned.");

    var activeAssignment = asset.Assignments
        .Any(a => a.ReturnDate == null);
    if (activeAssignment)
        return Result.Failure("Asset is already assigned to an employee. Return it first.");

    asset.Status = AssetStatus.Assigned;
    asset.Assignments.Add(new AssetAssignment
    {
        EmployeeId = request.EmployeeId,
        AssignmentDate = DateTime.UtcNow,
        Notes = request.Notes
    });

    await _context.SaveChangesAsync(ct);
    return Result.Success();
}

The check a.ReturnDate == null is the invariant: as long as an assignment has no return date, the asset is considered actively assigned. This single guard prevents every double-assignment scenario — whether from concurrent requests, UI bugs, or API misuse.

Depreciation Calculation Engine

MVC Asset Manager supports three depreciation methods, each implemented as a separate calculation strategy. The engine computes monthly depreciation and updates accumulated depreciation and current book value for every asset on a scheduled run:

public static decimal CalculateMonthlyDepreciation(
    DepreciationMethod method, decimal cost, decimal salvage, int usefulLifeMonths)
{
    return method switch
    {
        DepreciationMethod.StraightLine =>
            (cost - salvage) / usefulLifeMonths,

        DepreciationMethod.DecliningBalance =>
            (cost - salvage) * (2m / usefulLifeMonths),

        DepreciationMethod.SumOfYearsDigits =>
        {
            var sumOfYears = usefulLifeMonths * (usefulLifeMonths + 1) / 2;
            var remainingLife = usefulLifeMonths; // tracked per period
            return (cost - salvage) * remainingLife / sumOfYears;
        },

        _ => throw new ArgumentOutOfRangeException(nameof(method))
    };
}

The depreciation run is idempotent: it calculates the amount for each period and records it in a DepreciationEntry table with a period identifier, so re-running the same period never double-counts. Accumulated depreciation and book value on the asset record are updated atomically within the same transaction.

Lifecycle Stage Enforcement: Valid Transitions

Not all status transitions are valid. An asset in Repair cannot jump directly to Assigned — it must first return to Ready. A quarantined asset cannot be assigned until quarantine is cleared. These rules are enforced by a transition guard that checks the current status against a permitted-next-states map:

private static readonly Dictionary<AssetStatus, AssetStatus[]> AllowedTransitions = new()
{
    [AssetStatus.Ready]      = new[] { AssetStatus.Assigned, AssetStatus.Quarantine, AssetStatus.Missing },
    [AssetStatus.Assigned]   = new[] { AssetStatus.Ready, AssetStatus.Repair, AssetStatus.Missing },
    [AssetStatus.Repair]     = new[] { AssetStatus.Ready, AssetStatus.Quarantine },
    [AssetStatus.Quarantine] = new[] { AssetStatus.Ready, AssetStatus.Missing },
    [AssetStatus.Missing]    = new[] { AssetStatus.Ready }
};

public static bool IsValidTransition(AssetStatus current, AssetStatus next)
{
    return AllowedTransitions.TryGetValue(current, out var allowed)
        && allowed.Contains(next);
}

This guard runs inside every status-change handler. If the transition is invalid, the handler returns a descriptive error before any database work occurs. The transition map is centralized — adding a new status or changing allowed paths means updating one dictionary, not hunting through scattered if-statements.

Minimal API Endpoints

All asset operations are exposed through Minimal API endpoints defined alongside their handlers. Each endpoint calls MediatR to dispatch the command or query and maps the result to an HTTP response:

  • GET /api/assets/{id} — fetch a single asset with full assignment history
  • POST /api/assets — create a new asset from a model
  • POST /api/assets/{id}/assign — assign an asset to an employee
  • POST /api/assets/{id}/return — return an asset to inventory
  • PATCH /api/assets/{id}/status — transition asset to a new lifecycle stage
  • GET /api/assets/report/depreciation — depreciation schedule report

Endpoints are grouped under /api/assets with authorization policies applied per group, ensuring that only authorized roles can mutate asset state.

Vue 3 Frontend: Asset Dashboard

The frontend is built with Vue 3 and consumes the Minimal API endpoints. The asset dashboard provides a real-time view of all assets with filtering by status, department, and branch. Key frontend features include:

  • Asset grid with sortable columns (tag, model, status, assigned employee, department)
  • Lifecycle stage filter that highlights assets by current state with color-coded badges
  • QR and barcode integration — scan an asset tag to pull up its full history and current assignment
  • Assignment modal with employee search, validation feedback, and confirmation
  • Depreciation report view with per-asset schedules and aggregate summaries by department

The Vue 3 app is bundled with Vite and served as static files from the ASP.NET Core backend, keeping deployment simple while delivering a reactive SPA experience for asset management workflows.

Building an asset management system with clean architecture?

MVC EDevKit Basic provides the VSA foundation, CQRS patterns, and Minimal API setup used in this architecture. $21.

View MVC EDevKit Details

FAQ

How does the no-double-assignment rule work? The assignment handler checks for any existing assignment with a null ReturnDate before creating a new one. Since only one active (unreturned) assignment can exist per asset, the database constraint plus the handler guard guarantee one employee per asset at any time.

What depreciation methods are supported? Three methods are built in: straight-line (even distribution over useful life), declining balance (accelerated, double-rate), and sum-of-years-digits (weighted toward early periods). Each method is implemented as a pure calculation function with no side effects.

How are lifecycle transitions enforced? A centralized allowed-transitions dictionary maps every status to its valid next states. Every status-change handler calls IsValidTransition before proceeding. Invalid transitions return a descriptive error and never touch the database.