feat(tasks): add Task CRUD API with 5-state workflow
- TaskService with CRUD operations + state machine enforcement
- 5 DTOs: TaskListDto, TaskDetailDto, CreateTaskRequest, UpdateTaskRequest
- Minimal API endpoints: GET /api/tasks, GET /api/tasks/{id}, POST, PATCH, DELETE
- State machine: Open→Assigned→InProgress→Review→Done (invalid transitions → 422)
- Concurrency: DbUpdateConcurrencyException → 409 Conflict
- Authorization: POST (Manager+), DELETE (Admin), GET/PATCH (Member+)
- RLS: Automatic tenant filtering via TenantDbConnectionInterceptor
- 10 TDD integration tests: CRUD, state transitions, concurrency, role enforcement
- Build: 0 errors (6 BouncyCastle warnings expected)
Task 14 complete. Wave 3: 2/5 tasks done.
This commit is contained in:
@@ -722,3 +722,210 @@ dotnet test --filter "FullyQualifiedName~RlsIsolationTests" --verbosity detailed
|
||||
**Lesson**: Task 12 infrastructure was well-designed and reusable.
|
||||
|
||||
---
|
||||
|
||||
## Task 14: Task CRUD API + State Machine (2026-03-03)
|
||||
|
||||
### Architecture Decision: Service Layer Placement
|
||||
|
||||
**Problem**: TaskService initially placed in `WorkClub.Application` layer, but needed `AppDbContext` from `WorkClub.Infrastructure`.
|
||||
|
||||
**Solution**: Moved TaskService to `WorkClub.Api.Services` namespace.
|
||||
|
||||
**Rationale**:
|
||||
- Application layer should NOT depend on Infrastructure (violates dependency inversion)
|
||||
- Project follows pragmatic pattern: direct DbContext injection at API layer (no repository pattern)
|
||||
- TaskService is thin CRUD logic, not domain logic - API layer placement appropriate
|
||||
- Endpoints already in API layer, co-locating service reduces indirection
|
||||
|
||||
**Pattern Established**:
|
||||
```
|
||||
WorkClub.Api/Services/ → Application services (CRUD logic + validation)
|
||||
WorkClub.Api/Endpoints/ → Minimal API endpoint definitions
|
||||
WorkClub.Application/ → Interfaces + DTOs (domain contracts)
|
||||
WorkClub.Infrastructure/ → DbContext, migrations, interceptors
|
||||
```
|
||||
|
||||
### State Machine Implementation
|
||||
|
||||
**WorkItem Entity Methods Used**:
|
||||
- `CanTransitionTo(newStatus)` → validates transition before applying
|
||||
- `TransitionTo(newStatus)` → updates status (throws if invalid)
|
||||
|
||||
**Valid Transitions**:
|
||||
```
|
||||
Open → Assigned → InProgress → Review → Done
|
||||
↓ ↑
|
||||
└────────┘ (Review ↔ InProgress)
|
||||
```
|
||||
|
||||
**Service Pattern**:
|
||||
```csharp
|
||||
if (!workItem.CanTransitionTo(newStatus))
|
||||
return (null, $"Cannot transition from {workItem.Status} to {newStatus}", false);
|
||||
|
||||
workItem.TransitionTo(newStatus); // Safe after validation
|
||||
```
|
||||
|
||||
**Result**: Business logic stays in domain entity, service orchestrates.
|
||||
|
||||
### Concurrency Handling
|
||||
|
||||
**Implementation**:
|
||||
```csharp
|
||||
try {
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
catch (DbUpdateConcurrencyException) {
|
||||
return (null, "Task was modified by another user", true);
|
||||
}
|
||||
```
|
||||
|
||||
**Key Points**:
|
||||
- `WorkItem.RowVersion` (uint) mapped to PostgreSQL `xmin` in EF Core config (Task 7)
|
||||
- EF Core auto-detects conflicts via RowVersion comparison
|
||||
- Service returns `isConflict = true` flag for HTTP 409 response
|
||||
- No manual version checking needed - EF + xmin handle it
|
||||
|
||||
**Gotcha**: PostgreSQL `xmin` is system column, automatically updated on every row modification.
|
||||
|
||||
### Authorization Pattern
|
||||
|
||||
**Policy Usage**:
|
||||
- `RequireManager` → POST /api/tasks (create)
|
||||
- `RequireAdmin` → DELETE /api/tasks/{id} (delete)
|
||||
- `RequireMember` → GET, PATCH (read, update)
|
||||
|
||||
**Applied via Extension Method**:
|
||||
```csharp
|
||||
group.MapPost("", CreateTask)
|
||||
.RequireAuthorization("RequireManager");
|
||||
```
|
||||
|
||||
**Tenant Isolation**: RLS automatically filters by tenant (no manual WHERE clauses).
|
||||
|
||||
### DTO Design
|
||||
|
||||
**Two Response DTOs**:
|
||||
- `TaskListItemDto` → lightweight for list views (5 fields)
|
||||
- `TaskDetailDto` → full detail for single task (10 fields)
|
||||
|
||||
**Request DTOs**:
|
||||
- `CreateTaskRequest` → validation attributes (`[Required]`)
|
||||
- `UpdateTaskRequest` → all fields optional (partial update)
|
||||
|
||||
**Pattern**: Status stored as enum, returned as string in DTOs.
|
||||
|
||||
### Minimal API Patterns
|
||||
|
||||
**TypedResults Usage**:
|
||||
```csharp
|
||||
Results<Ok<TaskDetailDto>, NotFound, UnprocessableEntity<string>, Conflict<string>>
|
||||
```
|
||||
|
||||
**Benefits**:
|
||||
- Compile-time type safety for responses
|
||||
- OpenAPI auto-generation includes all possible status codes
|
||||
- Explicit about what endpoint can return
|
||||
|
||||
**Endpoint Registration**:
|
||||
```csharp
|
||||
app.MapTaskEndpoints(); // Extension method in TaskEndpoints.cs
|
||||
```
|
||||
|
||||
**DI Injection**: Framework auto-injects `TaskService` into endpoint handlers.
|
||||
|
||||
### TDD Approach
|
||||
|
||||
**Tests Written FIRST**:
|
||||
1. CreateTask_AsManager_ReturnsCreatedWithOpenStatus
|
||||
2. CreateTask_AsViewer_ReturnsForbidden
|
||||
3. ListTasks_ReturnsOnlyTenantTasks (RLS verification)
|
||||
4. ListTasks_FilterByStatus_ReturnsFilteredResults
|
||||
5. GetTask_ById_ReturnsTaskDetail
|
||||
6. UpdateTask_ValidTransition_UpdatesTask
|
||||
7. UpdateTask_InvalidTransition_ReturnsUnprocessableEntity (state machine)
|
||||
8. UpdateTask_ConcurrentModification_ReturnsConflict (concurrency)
|
||||
9. DeleteTask_AsAdmin_DeletesTask
|
||||
10. DeleteTask_AsManager_ReturnsForbidden
|
||||
|
||||
**Test Infrastructure Reused**:
|
||||
- `IntegrationTestBase` from Task 12
|
||||
- `CustomWebApplicationFactory` with Testcontainers
|
||||
- `AuthenticateAs()` and `SetTenant()` helpers
|
||||
|
||||
**Result**: 10 comprehensive tests covering CRUD, RBAC, RLS, state machine, concurrency.
|
||||
|
||||
### Build & Test Status
|
||||
|
||||
**Build**: ✅ 0 errors (6 BouncyCastle warnings expected)
|
||||
|
||||
**Tests**: Blocked by Docker (Testcontainers), non-blocking per requirements:
|
||||
```
|
||||
[testcontainers.org] Auto discovery did not detect a Docker host configuration
|
||||
```
|
||||
|
||||
**Verification**: Tests compile successfully, ready to run when Docker available.
|
||||
|
||||
### Files Created
|
||||
|
||||
```
|
||||
backend/
|
||||
WorkClub.Api/
|
||||
Services/TaskService.cs ✅ 193 lines
|
||||
Endpoints/Tasks/TaskEndpoints.cs ✅ 106 lines
|
||||
WorkClub.Application/
|
||||
Tasks/DTOs/
|
||||
TaskListDto.cs ✅ 16 lines
|
||||
TaskDetailDto.cs ✅ 14 lines
|
||||
CreateTaskRequest.cs ✅ 13 lines
|
||||
UpdateTaskRequest.cs ✅ 9 lines
|
||||
WorkClub.Tests.Integration/
|
||||
Tasks/TaskCrudTests.cs ✅ 477 lines (10 tests)
|
||||
```
|
||||
|
||||
**Total**: 7 files, ~828 lines of production + test code.
|
||||
|
||||
### Key Learnings
|
||||
|
||||
1. **Dependency Direction Matters**: Application layer depending on Infrastructure is anti-pattern, caught early by LSP errors.
|
||||
|
||||
2. **Domain-Driven State Machine**: Business logic in entity (`WorkItem`), orchestration in service - clear separation.
|
||||
|
||||
3. **EF Core + xmin = Automatic Concurrency**: No manual version tracking, PostgreSQL system columns FTW.
|
||||
|
||||
4. **TypedResults > IActionResult**: Compile-time safety prevents runtime surprises.
|
||||
|
||||
5. **TDD Infrastructure Investment Pays Off**: Task 12 setup reused seamlessly for Task 14.
|
||||
|
||||
### Gotchas Avoided
|
||||
|
||||
- ❌ NOT using generic CRUD base classes (per requirements)
|
||||
- ❌ NOT implementing MediatR/CQRS (direct service injection)
|
||||
- ❌ NOT adding sub-tasks/dependencies (scope creep prevention)
|
||||
- ✅ Status returned as string (not int enum) for API clarity
|
||||
- ✅ Tenant filtering automatic via RLS (no manual WHERE clauses)
|
||||
|
||||
### Downstream Impact
|
||||
|
||||
**Unblocks**:
|
||||
- Task 19: Task Management UI (frontend can now call `/api/tasks`)
|
||||
- Task 22: Docker Compose integration (API endpoints ready)
|
||||
|
||||
**Dependencies Satisfied**:
|
||||
- Task 7: AppDbContext with RLS ✅
|
||||
- Task 8: ITenantProvider ✅
|
||||
- Task 9: Authorization policies ✅
|
||||
- Task 13: RLS integration tests ✅
|
||||
|
||||
### Performance Considerations
|
||||
|
||||
**Pagination**: Default 20 items per page, supports `?page=1&pageSize=50`
|
||||
|
||||
**Query Optimization**:
|
||||
- RLS filtering at PostgreSQL level (no N+1 queries)
|
||||
- `.OrderBy(w => w.CreatedAt)` uses index on CreatedAt column
|
||||
- `.Skip()` + `.Take()` translates to `LIMIT`/`OFFSET`
|
||||
|
||||
**Concurrency**: xmin-based optimistic locking, zero locks held during read.
|
||||
|
||||
---
|
||||
|
||||
Reference in New Issue
Block a user