Project management API — Kanban boards, sprint planning, real-time collaboration via SignalR. Deployed on Railway with Azure SQL Database.
ASP.NET Core 8 · EF Core 8 (writes) + Dapper (reads) · SignalR · MediatR · Hangfire · SQL Server on Azure
Clean Architecture with 4 layers. Domain has zero external dependencies.
TaskFlow.API → Controllers, JWT auth, SignalR hub mapping, Swagger
TaskFlow.Application → CQRS handlers (MediatR), FluentValidation pipeline, DTOs
TaskFlow.Infrastructure → AppDbContext (EF Core + Fluent API), Dapper queries,
BoardHub (SignalR), Hangfire jobs, MailKit
TaskFlow.Domain → Entities, Enums, Domain Events (INotification) — no deps
Key decisions:
- CQRS via MediatR — separate Command/Query handlers, ValidationBehavior in pipeline
- Domain Events —
TaskMovedEventis published afterMoveTaskCommand, handled by three handlers in parallel: SignalR broadcast, ActivityLog write, @mention parsing - EF Core for writes with Fluent API configuration, Dapper for analytical queries (GetBoard JOIN, BurnDown aggregation)
- Policy-based authorization —
WorkspaceAuthHandlerchecks user role within a specific workspace on each request - JWT + refresh token rotation — refresh token stored in DB, revoked via
/auth/refresh
| Kanban | Columns + tasks with Order field for drag-and-drop, MoveTaskCommand updates ColumnId + Order |
| Real-time | BoardHub groups connections by board:{id}, broadcasts events after MoveTask/CreateTask/Comment |
| Sprints | Lifecycle: Planning → Active → Completed, burn-down chart via Dapper (points per day) |
| RBAC | 4 roles (Owner/Admin/Member/Viewer) per workspace, enforced via IAuthorizationRequirement |
| GitHub webhook | PR merged with #TASK-{guid} → automatically moves task to Done column, HMAC-SHA256 signature verification |
| @mentions | Regex parsing in comments → Hangfire fire-and-forget → email via MailKit |
| Overdue alerts | Hangfire recurring job daily at 08:00, Dapper query for overdue tasks |
| Activity log | Every Domain Event is written to ActivityLogs table via INotificationHandler |
SQL Server on Azure SQL Database. Schema via EF Core Fluent API:
- Composite PK on
WorkspaceMembers (WorkspaceId, UserId) - Index on
(ColumnId, Order)for fast task ordering - Index on
(WorkspaceId, OccurredAt)for activity log queries DeleteBehavior.Cascadeacross hierarchy Workspace → Projects → Boards → Columns → Tasks- Enums stored as
intvia.HasConversion<int>()
dotnet test tests/TaskFlow.UnitTests9 unit tests — CreateWorkspaceHandler, MoveTaskHandler, CreateTaskHandler using xUnit + Moq + InMemory DB.
Validated:
- DB persistence
- Domain event publishing (
mediatorMock.Verify) - Guard clauses (task not found → exception)
# SQL Server in Docker
docker-compose up db -d
# Migrations
dotnet ef database update \
--project src/TaskFlow.Infrastructure \
--startup-project src/TaskFlow.API
# API
dotnet run --project src/TaskFlow.API
# Swagger: https://localhost:{port}/swaggerappsettings.Development.json — requires connection string and Jwt section (see appsettings.json as template).
POST /auth/register — register
POST /auth/login — JWT + refresh token
POST /auth/refresh — refresh token rotation
POST /workspaces — create workspace [Authorize]
POST /workspaces/{id}/members — invite member [Admin+]
POST /projects/{workspaceId}/boards — create board [Admin+]
GET /projects/{workspaceId}/boards/{boardId} — board with columns and tasks [Member+]
POST /boards/{boardId}/tasks — create task [Member+]
PATCH /boards/{boardId}/tasks/{taskId}/move — move task [Member+]
POST /projects/{projectId}/sprints — create sprint [Admin+]
POST /projects/{projectId}/sprints/{id}/start — start sprint [Admin+]
GET /projects/{projectId}/sprints/{id}/burndown — burn-down data [Member+]
POST /tasks/{taskId}/comments — add comment [Member+]
POST /webhooks/github — GitHub PR webhook
wss://.../hubs/board?access_token={jwt}
→ JoinBoard(boardId)
Events:
TaskMovedTaskCreatedCommentAdded