| name | development-workflow |
| description | Debugging workflow, Docker dual-runtime requirements, dev tunnels, local CI. Use when debugging issues or working with deployment configuration. |
| user-invocable | false |
Development Workflow
Code Quality Standards
Testing Requirements
- ALWAYS run tests before committing changes
- All tests must pass
- Write tests for new domain objects following existing patterns
- Use integration tests for API endpoints
Code Duplication Guidelines
- Duplication over wrong abstraction — bad abstractions are harder to fix
- Consider domain boundaries — similar code in different contexts should stay separate
- Evaluate coupling costs — sometimes shared code creates expensive coupling
- Optimize for readability — sometimes duplication is clearer than complex abstraction
Debugging Workflow
When encountering bugs or errors, ALWAYS follow this structured approach:
Step 1: Reproduce the Bug
- Write a failing test first (TDD approach)
- The test should fail for the right reason (exposing the bug)
- Run existing tests to see if they already catch the problem
- Document the exact steps to reproduce
Step 2: Gather Complete Error Information
- Full exception stack trace (not just the first line)
- Exception type (e.g.,
DbUpdateException, ArgumentException)
- Actual error message and any inner exceptions
- Application logs for additional context
Step 3: Analyze Root Cause
- Read the error message carefully — it often tells you exactly what's wrong
- Check database constraints (column types, precision, null constraints)
- Verify domain validation rules match database schema
- Look for data type mismatches
Common issues:
DECIMAL(5,4) max value is 9.9999 — can't store 10.0
- Percentage rates: use decimal format (
0.10 for 10%), not whole numbers
- String length violations: check
NVARCHAR(N) limits
- Null constraint violations: ensure required fields are set
Step 4: Fix with Validation
- Add domain-level validation to catch errors early
- Provide clear, helpful error messages explaining valid range/format and how to fix
if (gstRate > 1.0m)
throw new ArgumentOutOfRangeException(nameof(gstRate), gstRate,
"GST rate must be a decimal value between 0 and 1 (e.g., 0.10 for 10%). " +
"Database constraint: DECIMAL(5,4) with max value 9.9999.");
Step 5: Add Tests
- Write tests that verify the validation works
- Test boundary conditions (edge cases)
- Test both valid and invalid inputs
Step 6: Verify the Fix
- Run all tests:
dotnet test
- Test the original reproduction case manually
- Verify error messages are clear and helpful
Anti-Patterns to Avoid
- Guessing at fixes without understanding root cause
- Partial error messages — always get full stack trace
- Skipping tests — untested fixes often break later
- Silent failures — add validation that gives clear feedback
- Fixing symptoms instead of root cause
Docker & Deployment Notes
Aspire-First Runtime Requirement
Aspire is the supported local orchestration path. Docker remains required for Aspire-managed dependencies, Testcontainers, the Functions runtime container, and deployable image validation. When making infrastructure changes, verify Aspire plus direct image builds:
- Connection strings: Standardized on
database key — Aspire injects this for local orchestration; real container deployments must provide it explicitly
- Health checks:
app.MapHealthChecks("/health") must be mapped unconditionally — container platforms use readiness/liveness probes outside Development
- Service registration: All DI registrations must work without Aspire service discovery present — use conditional checks or fallback defaults where Aspire provides configuration
- Image validation: CI builds API, DbMigrator, and Functions images with direct
docker build -f ... . commands
Smoke Testing
After deploying or modifying infrastructure, run the smoke test script against the live environment:
./scripts/smoke-test.sh https://localhost:7286
./scripts/smoke-test.sh https://staging.example.com
The script tests all CRUD endpoints, validators (email, currency, OrderId), conflict responses (409), and not-found responses (404). It uses unique test data per run and exits non-zero on failure — suitable for CI post-deploy gates.
.NET 10 Dockerfiles in this repo
This repo's three Dockerfiles (src/StarterApp.Api, src/StarterApp.DbMigrator, src/StarterApp.Functions) pull the .NET SDK/runtime from digest-pinned mcr.microsoft.com images (dotnet/sdk:10.0@sha256:… for build, dotnet/aspnet:10.0@sha256:… or azure-functions/dotnet-isolated:…@sha256:… for the final stage) — there is NO Microsoft apt repo, GPG-key import (gpg --dearmor/signed-by=), or prod.list to maintain. The only apt step in the final stage installs curl for the healthcheck via plain apt-get install -y --no-install-recommends curl (then clears /var/lib/apt/lists/*). Do not introduce a Microsoft package-repo block — the runtime comes from the base image. Resolve a new digest with docker buildx imagetools inspect <image> (mutable tags would defeat the digest pin and the Dependabot docker updater).
Azure Service Bus Emulator in Aspire
The Service Bus emulator is the most fragile part of the Aspire stack. These learnings were hard-won — do NOT repeat these mistakes.
NEVER use WithConfigurationFile() for emulator topology
WithConfigurationFile() can cause container mount and networking issues — the emulator container may fail to join the Aspire network, leaving it unable to reach its backing database container. Always use the fluent API instead:
var serviceBus = builder.AddAzureServiceBus("servicebus");
var topic = serviceBus.AddServiceBusTopic("domain-events");
topic.AddServiceBusSubscription("email-notifications")
.WithProperties(sub =>
{
sub.Rules.Add(new AzureServiceBusRule("MyFilter")
{
FilterType = AzureServiceBusFilterType.CorrelationFilter,
CorrelationFilter = new AzureServiceBusCorrelationFilter
{
Properties = { ["EventType"] = "order.created.v1" }
}
});
});
serviceBus.RunAsEmulator(emulator => emulator
.WithLifetime(ContainerLifetime.Persistent));
Do not add a static Service Bus emulator JSON file as a second topology source.
NEVER use WithConfiguration() with raw JSON for correlation filters
Aspire's WithConfiguration(doc => { ... }) serializer maps CorrelationFilter using its own schema, which produces empty Properties: {} in the emulator JSON — even if you set ApplicationProperties correctly in the JsonObject. The emulator then rejects the filter with: "At least one system or user property must be set for a correlation filter." Use the fluent API above instead.
Memory pressure with the emulator
The Service Bus emulator runs its own backing database container (servicebus-mssql). On machines with limited Docker memory (e.g. 8 GB default WSL2 on a 16 GB machine), the emulator stack can OOM (exit code 139). Symptoms:
- Emulator starts, creates topics/subscriptions, then dies with
Out of memory
- Container shows
Exited (139) — SIGKILL from OOM killer
If this happens, increase Docker/WSL2 memory via %USERPROFILE%\.wslconfig:
[wsl2]
memory=12GB
Then wsl --shutdown and restart Docker Desktop. On ARM/Apple Silicon the problem is worse due to Rosetta/QEMU overhead (the emulator image is linux/amd64 only).
Debugging emulator startup failures
- Check
docker ps -a — look for Created (never started) or Exited (139) (OOM)
- Check
docker inspect <container> --format '{{json .NetworkSettings.Networks}}' — empty {} means network wasn't attached
- Check
docker logs <container> — look for "SQL DB Unhealthy" (network issue) or "At least one system or user property" (filter serialization issue)
- If containers are in bad state, remove them and the Aspire network:
docker rm -f <containers>; docker network rm <aspire-network>
Dev Tunnels
Expose your local API to the internet for webhook testing, mobile app development, or sharing with teammates. Uses Aspire.Hosting.DevTunnels.
Prerequisites: Install Dev Tunnel CLI (https://aka.ms/devtunnels/docs):
brew install --cask devtunnel
winget install Microsoft.devtunnel
curl -sL https://aka.ms/DevTunnelCliInstall | bash
devtunnel user login
Usage (opt-in, requires an explicit security acknowledgment):
DEV_TUNNEL_ACK_UNSIGNED_API=true dotnet run --project src/StarterApp.AppHost -- --devtunnel
Local CI with nektos/act
Run GitHub Actions workflows locally using Docker before pushing.
Prerequisites (https://nektosact.com/installation/):
brew install act
winget install nektos.act
curl -s https://raw.githubusercontent.com/nektos/act/master/install.sh | sudo bash
Usage (.actrc pre-configures architecture and image):
act
act -j build
act -v
act --list
Config files: .actrc sets --container-architecture linux/amd64, the runner image (-P ubuntu-latest=catthehacker/ubuntu:act-latest), and --env-file .act.env. .act.env sets TESTCONTAINERS_RYUK_DISABLED=true so Testcontainers-backed tests run under act's nested Docker (the Ryuk reaper can't be reached across the nested-Docker boundary). It deliberately does NOT override PATH — the catthehacker runner image already puts the correct bundled Node on PATH, and pinning a patch version here breaks on every image bump.
Post-Session Retrospective
After completing a significant batch of work (bug fixes, feature additions, refactoring), retrospect and update rules:
- Re-read all
.claude/skills/*/SKILL.md files
- Identify stale guidance that contradicts the changes just made (e.g., old patterns that were replaced)
- Update convention class lists and pattern descriptions to match current reality (do NOT hardcode test counts — they go stale immediately)
- Add new patterns or anti-patterns discovered during the session
- Update
CLAUDE.md if architectural decisions or project structure changed
- Mirror equivalent guidance changes into
.agents/skills/** and AGENTS.md; keep only intentional agent-specific path/name drift (AgentDocsConventionTests fails the build on any other drift)
- Update
docs/ARCHITECTURE_REVIEW.md to mark resolved issues
This prevents rules from drifting out of sync with the codebase — stale rules actively mislead future work.