When you peel back the UI of MVC Roster Manager, you find a modern .NET architecture designed for maintainability and performance. This article walks through every architectural layer: how Vertical Slice Architecture keeps roster features self-contained, how CQRS separates reads from writes, how Minimal APIs serve a Vue 3 frontend, and how Hangfire handles scheduled work like auto-publishing rosters at midnight. If you are evaluating whether this codebase fits your team's standards, this deep dive answers the question.
Vertical Slice Architecture: Features, Not Layers
Instead of spreading roster logic across Controllers, Services, and Repositories folders, MVC Roster Manager groups everything a feature needs into a single folder under Features/Roster/. Each slice contains its command, its handler, its validator, its DTOs, and its endpoint registration — all co-located. When a developer needs to add a new roster operation, they create one folder and touch nothing else. This eliminates the cross-cutting ripple effects that plague traditional layered architectures, especially in a domain as interconnected as shift scheduling where roster creation touches employees, shifts, leaves, and notifications.
Features/
└── Roster/
├── CreateRoster/
│ ├── CreateRosterCommand.cs
│ ├── CreateRosterHandler.cs
│ ├── CreateRosterValidator.cs
│ └── CreateRosterEndpoint.cs
├── GetRosterById/
│ ├── GetRosterByIdQuery.cs
│ ├── GetRosterByIdHandler.cs
│ └── GetRosterByIdEndpoint.cs
├── SwapShift/
│ ├── SwapShiftCommand.cs
│ ├── SwapShiftHandler.cs
│ ├── SwapShiftValidator.cs
│ └── SwapShiftEndpoint.cs
└── PublishRoster/
├── PublishRosterCommand.cs
├── PublishRosterHandler.cs
└── PublishRosterEndpoint.cs
CQRS: Separating Commands from Queries
Every roster operation is either a command (it changes state) or a query (it returns data). Commands like CreateRosterCommand and SwapShiftCommand go through handlers that validate input, enforce business rules, persist changes via EF Core, and dispatch domain events. Queries like GetRosterByIdQuery bypass validation and domain logic entirely, projecting directly from the database using AsNoTracking() for maximum read performance. This separation means the write side can evolve with complex business rules while the read side stays fast and simple — critical when a manager dashboard needs to render roster coverage across 50 locations in under a second.
public sealed class SwapShiftHandler : IRequestHandler<SwapShiftCommand, Result>
{
private readonly AppDbContext _db;
private readonly ICurrentUser _currentUser;
private readonly INotificationService _notifications;
public async Task<Result> Handle(SwapShiftCommand request, CancellationToken ct)
{
var assignment = await _db.RosterAssignments
.Include(a => a.Roster)
.FirstOrDefaultAsync(a => a.Id == request.AssignmentId, ct);
if (assignment is null) return Result.Failure("Assignment not found.");
if (assignment.Roster.Status != RosterStatus.Confirmed)
return Result.Failure("Only confirmed rosters allow swaps.");
var swapRequest = new ShiftSwapRequest
{
RosterAssignmentId = assignment.Id,
RequestedById = _currentUser.UserId,
TargetEmployeeId = request.TargetEmployeeId,
Reason = request.Reason,
Status = SwapStatus.Pending
};
_db.ShiftSwapRequests.Add(swapRequest);
await _db.SaveChangesAsync(ct);
await _notifications.SendSwapRequestToManagerAsync(swapRequest, ct);
return Result.Success();
}
}
Minimal API Endpoints for Vue 3 Consumption
While the MVC side handles server-rendered pages for managers, the employee self-service portal runs on Vue 3 and communicates through Minimal API endpoints. Each VSA slice exposes its own endpoint via an extension method, keeping the API surface co-located with its handler. Endpoints are grouped under /api/roster and secured with role-based authorization — employees can only query their own assignments, while managers can create and modify rosters.
public static class GetMyShiftsEndpoint
{
public static void MapGetMyShifts(this IEndpointRouteBuilder app)
{
app.MapGet("/api/roster/my-shifts", async (
[FromQuery] DateTime from,
[FromQuery] DateTime to,
IMediator mediator,
CancellationToken ct) =>
{
var query = new GetMyShiftsQuery(from, to);
var result = await mediator.Send(query, ct);
return Results.Ok(result);
})
.RequireAuthorization("Employee")
.Produces<List<ShiftDto>>();
}
}
Vue 3 + DataTables Frontend
The employee self-service portal is a Vue 3 single-page application that consumes the Minimal API endpoints. Shift lists use DataTables with server-side processing for pagination, sorting, and filtering — essential when an employee has hundreds of historical shifts. The swap request form is a reactive Vue component with inline validation and real-time coworker availability search. State management uses Vue's Composition API with ref and reactive, avoiding the overhead of Vuex or Pinia for this focused module.
<script setup>
import { ref, onMounted } from 'vue';
import DataTable from 'datatables.net-vue3';
import { getMyShifts } from '../api/roster';
const shifts = ref([]);
const loading = ref(true);
onMounted(async () => {
const data = await getMyShifts({ from: '2026-08-01', to: '2026-08-31' });
shifts.value = data;
loading.value = false;
});
</script>
<template>
<DataTable :data="shifts" :columns="columns" class="display">
<thead>
<tr>
<th>Date</th>
<th>Shift</th>
<th>Status</th>
<th>Actions</th>
</tr>
</thead>
</DataTable>
</template>
Authorization: Admin, Manager, and Employee Roles
MVC Roster Manager enforces three role tiers. Admins have full access to all rosters, locations, and system configuration. Managers can create, edit, confirm, and publish rosters for their assigned locations, approve or reject swap requests, and view attendance reports. Employees can view their own shifts, submit swap requests, clock in and out, and update availability preferences. Authorization is enforced at both the MVC controller level with [Authorize(Roles)] attributes and at the Minimal API level with .RequireAuthorization() policies. Location-scoped authorization uses a custom policy that checks the manager's assigned location against the roster's location.
Hangfire Background Jobs
Several roster operations are time-sensitive and run as Hangfire recurring jobs. The roster auto-publish job runs nightly at midnight and automatically publishes any confirmed roster whose start date is today. The shift reminder job runs every evening and emails employees their shifts for the next day. The attendance reconciliation job runs weekly and flags discrepancies between clock-in records and published assignments. All jobs are configured in Program.cs with the Hangfire dashboard protected behind the Admin role for operational visibility.
Multi-Database Support
MVC Roster Manager uses EF Core with provider-agnostic configuration, supporting SQL Server, PostgreSQL, and MySQL out of the box. The connection string and provider are selected at deployment time through appsettings.json, and all migrations are generated with the chosen provider. The data access layer avoids provider-specific SQL, relying on EF Core's LINQ translation to generate optimal queries for each database engine. This means a team running SQL Server in production can develop locally against PostgreSQL without code changes.
Key Takeaways
- Vertical Slice Architecture keeps roster features self-contained in
Features/Roster/folders - CQRS separates write-heavy roster commands from read-heavy dashboard queries for optimal performance
- Minimal API endpoints serve a Vue 3 + DataTables frontend with server-side pagination
- Three-tier role authorization (Admin, Manager, Employee) with location-scoped policies
- Hangfire recurring jobs handle auto-publish, reminders, and attendance reconciliation
- EF Core multi-database support for SQL Server, PostgreSQL, and MySQL without code changes
FAQ
Why VSA over Clean Architecture? VSA groups code by feature, not by layer. In a roster system where create-roster touches employees, shifts, leaves, and notifications, having all that logic in one folder is easier to reason about than spreading it across four Clean Architecture layers. VSA also reduces the friction of adding new features — you create one folder, not files in five projects.
How does Vue 3 communicate with the backend? Vue 3 calls Minimal API endpoints under /api/roster/ using fetch. Each endpoint is defined in its own VSA slice and secured with role-based authorization policies. The API returns JSON DTOs that Vue components bind to reactively.
How are background jobs configured? Hangfire is registered in Program.cs with AddHangfire and AddHangfireServer. Recurring jobs are scheduled in a JobScheduler class that runs at startup. The Hangfire dashboard is mounted at /hangfire and protected with the Admin role. Storage uses the same database as the application.
Can I swap Vue 3 for another frontend? Yes. The Minimal API layer is completely decoupled from the frontend. You can replace Vue 3 with React, Angular, or even Blazor WASM — the API contracts remain the same.