| name | backend-domain |
| description | TeensyROM .NET backend domain knowledge โ architecture, MediatR CQRS, serial protocol, storage systems, RadEndpoints, and testing patterns. Use when working on backend code, adding endpoints, modifying serial commands, updating storage/caching logic, writing backend tests, or understanding backend architecture. Covers .NET 9 API, pipeline behaviors, state machines, and multi-device orchestration. |
Backend Domain Skill
Architecture guardrails and task routing for the TeensyROM .NET 9 backend. Enables any agent to understand layer boundaries, enforce patterns, and navigate to the right documentation.
When to Use
- Implementing or modifying backend endpoints, commands, or services
- Planning backend features or architectural changes
- Working with serial communication or device management
- Modifying storage, caching, or indexing logic
- Writing backend unit or integration tests
- Debugging serial protocol or state machine issues
Architecture Overview
Layered .NET 9 Web API bridging the Angular frontend to physical TeensyROM devices via serial ports.
Layer dependency direction (core โ outer):
TeensyRom.Core (entities, abstractions)
โ TeensyRom.Core.Serial (MediatR commands, state machine)
โ TeensyRom.Core.Storage (indexing, caching, search)
โ TeensyRom.Core.Device (multi-device orchestration)
โ TeensyRom.Api (RadEndpoints, SignalR hubs)
All serial operations flow through MediatR with pipeline behaviors:
Endpoint โ MediatR โ LoggingBehavior โ ExceptionBehavior โ SerialBehavior โ Handler โ Serial I/O
Critical Rules
- Never access
SerialPort directly โ Always use ISerialStateContext to respect the serial state machine. Direct port access causes race conditions.
- All serial commands use MediatR โ Implement
ITeensyCommand<T>. Pipeline behaviors (logging, locking, exception handling) are applied automatically.
- Endpoints are thin adapters โ Extract request, resolve device/service, delegate to MediatR/service, map to DTO, send response. No business logic in endpoints.
- Multi-device: always bind DeviceId โ Commands must set
DeviceId so SerialBehavior binds the correct ISerialStateContext.
- Thread-safe singletons โ
DeviceConnectionManager is a singleton orchestrating concurrent devices. All state mutation must be thread-safe.
- Cache invalidation must cascade โ Storage cache updates/deletes must cascade to children and siblings (e.g., favorites affect original + copy).
- Let
ExceptionBehavior handle errors โ Don't swallow exceptions in handlers. The pipeline converts them to error responses and publishes alerts.
- Metadata enrichment at index time โ HVSC (music) and OneLoad64 (games) enrichment runs during indexing, not on-demand reads.
Implementation Patterns
Adding an Endpoint
Each endpoint lives in Endpoints/[Domain]/[Action]/ with [Action]Endpoint.cs + [Action]Models.cs. Extends RadEndpoint<TRequest, TResponse> with Configure() for routing and Handle() for logic. See RadEndpoints docs.
Adding a Serial Command
- Create command in
TeensyRom.Core.Serial/Commands/[Name]/ implementing ITeensyCommand<TResult>
- Create handler implementing
IRequestHandler<TCommand, TResult>
- Pipeline behaviors apply automatically โ no registration needed
- Protocol: send token bytes โ wait ACK/NAK โ send parameters โ parse result
Serial State Machine
Five states: Start โ Connectable โ Connected โ Busy, with ConnectionLost for recovery. Health check polls every 2s. Reconnection logic handles COM port reassignment after device reset.
Storage Caching
Three strategies: lazy (cache miss โ fetch โ cache), full indexing (recursive walk, 5-10 min for large SD), incremental (single path merge). Cache persists to disk as *.cache.json.
SignalR Hubs
/logHub for real-time device logs, /deviceEventHub for device state changes. Backend pushes via IAlertService and DeviceEventStream.
Testing
- Unit tests: Mock interfaces (
ISerialStateContext, IStorageService, etc.) with NSubstitute
- Integration tests:
WebApplicationFactory with real DI container
- Handler tests: Mock serial port operations, verify command/response flow
- Frameworks: xUnit, FluentAssertions, NSubstitute
Task Routing
| Task | Read First |
|---|
| Full architecture, diagrams, sequences | docs/BACKEND_ARCHITECTURE.md |
| Frontend โ backend integration | docs/OVERVIEW_CONTEXT.md |
| API client generation workflow | .github/skills/api-client-generation/SKILL.md |
| RadEndpoints patterns | RadEndpoints README |
| Endpoint examples | apps/api/src/TeensyRom.Api/Endpoints/ |
| Serial commands & behaviors | apps/api/src/TeensyRom.Core.Serial/Commands/ |
| Storage service & cache | apps/api/src/TeensyRom.Core.Storage/ |
| Device management | apps/api/src/TeensyRom.Core.Device/ |
| Domain entities & abstractions | apps/api/src/TeensyRom.Core/ |
Anti-Patterns
- Direct
SerialPort access โ Bypasses state machine, causes race conditions
- Singleton mutation without locks โ
DeviceConnectionManager must be thread-safe
- Blocking serial reads โ Use timeouts and polling loops, never infinite waits
- Swallowing exceptions โ Let
ExceptionBehavior handle; don't catch/ignore in handlers
- Forgetting
DeviceId โ Multi-device commands silently bind wrong serial context
- Cache invalidation gaps โ Updates must cascade to children/siblings
- Business logic in endpoints โ Endpoints delegate to services/MediatR only