HelpdeskMVCVSAAugust 2026 · 9 min read

MVC Helpdesk Manager Architecture: VSA, SLA Engine, and Role-Based Access

TL;DR

MVC Helpdesk Manager is built on Vertical Slice Architecture with CQRS handlers, an SLA solve estimation engine that calculates deadlines in business hours, three-tier role-based access (Guest/Member/Admin), ticket ownership enforcement through authorization handlers, and a Vue 3 frontend. Every feature is a self-contained vertical slice under Features/Helpdesk/.

Architecture decisions shape everything that follows. A helpdesk system that couples ticket logic across controllers, services, and repositories becomes impossible to extend without breaking something else. MVC Helpdesk Manager uses Vertical Slice Architecture to keep every feature — ticket creation, SLA calculation, agent assignment, status transitions — isolated in its own folder with its own handler, request, and response. This article walks through the architecture layer by layer.

Vertical Slice Architecture: Features/Helpdesk/

The entire helpdesk module lives under Features/Helpdesk/. Each subfolder represents a use case: CreateTicket/, AssignAgent/, ChangeStatus/, GetDashboard/, and so on. Inside each folder you will find the command or query, the handler, the validator, and the view model or DTO — all co-located. There are no shared service layers, no generic repositories, and no cross-cutting abstractions that couple unrelated features together.

This structure means a developer can open Features/Helpdesk/CreateTicket/ and see every line of code that executes when a ticket is created: the request DTO, FluentValidation rules, the MediatR handler that persists the entity and triggers the SLA calculation, and the view model returned to the client. No hunting through a sprawling Services/ folder or tracing through base classes.

CQRS is applied through MediatR: commands mutate state, queries read state. The handlers use EF Core directly — no repository wrapper — keeping the data access explicit and the SQL predictable. Each handler opens its own DbContext scope, so adding a new feature never risks breaking an existing one through unintended side effects.

SLA Solve Estimation Calculation Engine

The SLA engine is one of the most critical pieces of the architecture. When a ticket is created or its status changes, the system calculates a SolveByEstimation timestamp based on the SLA policy assigned to the ticket's group. The calculation accounts for business hours, weekends, and holidays so that a ticket created at 4 PM on Friday with an 8-hour SLA does not show as overdue at midnight.

public static DateTime CalculateSolveBy(
    DateTime start,
    int resolutionHours,
    BusinessHoursConfig businessHours)
{
    var remaining = resolutionHours;
    var current = start;

    while (remaining > 0)
    {
        if (IsBusinessDay(current, businessHours.Holidays) &&
            current.TimeOfDay >= businessHours.StartTime &&
            current.TimeOfDay < businessHours.EndTime)
        {
            remaining--;
        }
        current = current.AddHours(1);
    }

    return current;
}

The SLA engine also handles pause and resume when a ticket moves to OnHold and back to InProgress. The elapsed time spent in OnHold is subtracted from the SLA clock, so agents are not penalized for waiting on third parties. An Admin can configure business hours per helpdesk team, allowing teams in different time zones to operate under their own schedules.

Visual indicators in the UI show green (well within SLA), amber (approaching deadline), and red (overdue) based on the remaining time. These are calculated server-side and pushed to the Vue 3 frontend so that the agent queue always reflects real-time SLA status.

Role-Based Access Control: Guest, Member, Admin

Three role levels govern every action in the system. Guest users can submit tickets through the self-service portal and view only their own tickets. Member agents can view tickets assigned to their helpdesk team, post internal notes, change ticket status, and communicate with submitters. Admin managers can configure SLA policies, create helpdesk teams, assign agents, override ticket assignments, and access the full reporting dashboard.

public class TicketAuthorizationHandler
    : AuthorizationHandler<TicketOwnerRequirement, Ticket>
{
    protected override Task HandleRequirementAsync(
        AuthorizationHandlerContext context,
        TicketOwnerRequirement requirement,
        Ticket ticket)
    {
        if (context.User.IsInRole("Admin"))
        {
            context.Succeed(requirement);
            return Task.CompletedTask;
        }

        if (context.User.IsInRole("Member") &&
            ticket.AssignedAgentId == GetUserId(context.User))
        {
            context.Succeed(requirement);
            return Task.CompletedTask;
        }

        if (context.User.IsInRole("Guest") &&
            ticket.SubmitterId == GetUserId(context.User))
        {
            context.Succeed(requirement);
            return Task.CompletedTask;
        }

        return Task.CompletedTask;
    }
}

Authorization is enforced at the handler level, not scattered across controller actions. Each MediatR handler declares its authorization requirement, and the pipeline validates it before any business logic executes. This keeps authorization rules centralized and testable without mocking HTTP contexts.

Ticket Ownership Enforcement

Only the assigned agent can modify a ticket's status or post internal notes. This ownership model prevents agents from accidentally working on each other's tickets and provides clear accountability. The ownership check runs as an authorization handler that compares the current user's ID against the ticket's AssignedAgentId.

public class ChangeTicketStatusHandler
    : IRequestHandler<ChangeTicketStatusCommand, Result>
{
    public async Task<Result> Handle(
        ChangeTicketStatusCommand command,
        CancellationToken cancellationToken)
    {
        var ticket = await _context.Tickets
            .FindAsync(new object[] { command.TicketId }, cancellationToken);

        if (ticket == null)
            return Result.Failure("Ticket not found.");

        var authResult = await _authService
            .AuthorizeAsync(_currentUser, ticket, "TicketOwner");
        if (!authResult.Succeeded)
            return Result.Failure("Only the assigned agent can modify this ticket.");

        ticket.Status = command.NewStatus;
        await _context.SaveChangesAsync(cancellationToken);

        return Result.Success();
    }
}

Ticket Lifecycle State Machine with Guards

The ticket lifecycle is modeled as a state machine with explicit transition guards. Not every status transition is valid: a Solved ticket cannot jump back to New without being reopened, and an OnHold ticket must return to InProgress before it can be Solved. These guards are implemented in the domain layer so they are enforced regardless of which handler or endpoint triggers the transition.

Valid transitions are: New→InProgress, New→OnHold (if the agent needs more info immediately), InProgress→OnHold, InProgress→Solved, OnHold→InProgress. Each transition also triggers side effects: SLA timer pause/resume, notification dispatch, and audit log entries. The state machine is explicit in code rather than buried in if-else chains across multiple handlers.

Vue 3 Frontend with Real-Time Updates

The agent dashboard and ticket detail views are built with Vue 3 for a reactive, component-driven UI. The frontend communicates with the ASP.NET Core MVC backend through REST API endpoints exposed by each vertical slice. Vue components are organized by feature — TicketQueue.vue, TicketDetail.vue, SlaIndicator.vue — mirroring the backend folder structure.

Real-time SLA countdown timers update in the browser without polling, using a lightweight SignalR connection that pushes status changes and SLA updates to connected agents. When one agent picks up a ticket, other agents see it disappear from the unassigned queue immediately, preventing duplicate work.

FAQ

How is SLA solve time calculated? The SLA engine iterates forward from the ticket's first response time in one-hour increments, skipping non-business hours, weekends, and configured holidays. The resulting timestamp is stored as SolveByEstimation and displayed as a live countdown in the agent dashboard.

How does ticket ownership enforcement work? An authorization handler checks whether the current user is the ticket's assigned agent, an Admin, or the original submitter. Only the assigned agent and Admin can modify ticket status; submitters can only view and comment. The check runs inside the MediatR handler before any mutation occurs.

What are the three role levels? Guest (ticket submitter, self-service portal only), Member (helpdesk agent, can manage tickets assigned to them and their team), and Admin (full access: SLA configuration, team management, agent assignment, reporting, and ticket override).

Want a helpdesk system with VSA, SLA engine, and RBAC built in?

MVC EDevKit Basic ships with Vertical Slice Architecture, CQRS handlers, and role-based access control — ready to extend. $21.

View MVC EDevKit Details