Development Guide
Development Guide
Setup, conventions, daily workflow for engineers on the Legacy Modernization project
Tech: .NET 8 | React 18 | Azure Service Bus | Docker | AI-first
1. Getting Started
1.1 Prerequisites
Required:
□ .NET 8 SDK → dotnet --version (8.0+)
□ Node.js 20 LTS → node --version (20+)
□ Docker Desktop → docker --version
□ Git → git --version
□ Azure CLI → az --version
IDE (choose one):
□ Cursor Pro (recommended) → AI-first, agentic mode
□ VS Code + Extensions → if Cursor not available
□ JetBrains Rider → if preferred
AI Tools:
□ Cursor Pro license → request from Tech Lead
□ Claude Code (CLI) → npm install -g @anthropic-ai/claude-code
□ Ollama (local models) → brew install ollama
□ CodeRabbit → auto-enabled on repo PRs
1.2 Repository Setup
# Clone repo
git clone https://github.com/phoenixdx/product-a-modernization.git
cd product-a-modernization
# Install dependencies
dotnet restore
cd src/frontend && npm ci && cd ../..
# Start local environment
docker-compose up -d
# Verify everything is running
curl http://localhost:5000/health # API Gateway
curl http://localhost:5001/health # Travel Service
curl http://localhost:5002/health # Event Service
curl http://localhost:5003/health # Comms Service
curl http://localhost:5004/health # Workforce Service
curl http://localhost:5005/health # Reporting Service
# Run all tests
dotnet test
cd src/frontend && npm test && cd ../..
1.3 Repository Structure
product-a-modernization/
├── src/
│ ├── gateway/ ← YARP API Gateway
│ │ └── Gateway.API/
│ │
│ ├── services/ ← Microservices
│ │ ├── travel-booking/
│ │ │ ├── TravelBooking.API/
│ │ │ ├── TravelBooking.Application/
│ │ │ ├── TravelBooking.Domain/
│ │ │ ├── TravelBooking.Infrastructure/
│ │ │ └── tests/
│ │ │ ├── UnitTests/
│ │ │ ├── IntegrationTests/
│ │ │ └── ContractTests/
│ │ │
│ │ ├── event-management/ ← Same structure
│ │ ├── workforce/
│ │ ├── communications/
│ │ └── reporting/
│ │
│ ├── shared/ ← Shared NuGet packages
│ │ ├── SharedKernel/ ← Base entities, events, logging
│ │ └── Contracts/ ← Shared event contracts
│ │
│ └── frontend/ ← React 18 SPA
│ ├── src/
│ │ ├── modules/
│ │ │ ├── travel/
│ │ │ ├── events/
│ │ │ ├── reports/
│ │ │ └── shared/ ← Design system components
│ │ ├── App.tsx
│ │ └── main.tsx
│ ├── package.json
│ └── vite.config.ts
│
├── infra/ ← IaC (Bicep)
│ ├── modules/
│ ├── environments/
│ │ ├── dev.bicepparam
│ │ ├── staging.bicepparam
│ │ └── prod.bicepparam
│ └── main.bicep
│
├── docs/ ← Documentation
│ ├── adrs/ ← Architecture Decision Records
│ ├── runbooks/ ← Operational runbooks
│ └── api/ ← OpenAPI specs
│
├── prompts/ ← AI Prompt Library
│ ├── migration/
│ │ ├── analyze-module.md
│ │ ├── scaffold-service.md
│ │ ├── migrate-business-logic.md
│ │ └── generate-tests.md
│ └── review/
│ ├── security-checklist.md
│ └── business-logic-validation.md
│
├── scripts/ ← Utility scripts
│ ├── smoke-test.sh
│ ├── seed-data.sh
│ └── monitor-canary.sh
│
├── .github/
│ ├── workflows/ ← CI/CD pipelines
│ │ ├── service-ci.yml
│ │ ├── frontend-ci.yml
│ │ └── infra-ci.yml
│ └── copilot-instructions.md ← Copilot project context
│
├── .cursorrules ← Cursor AI rules
├── CLAUDE.md ← Claude Code project context
├── docker-compose.yml ← Local development
└── README.md
2. Coding Conventions
2.1 .NET 8 Backend
Naming:
Classes: PascalCase → BookingService
Methods: PascalCase → CreateBookingAsync()
Properties: PascalCase → BookingId
Private fields: _camelCase → _bookingRepository
Local variables: camelCase → bookingCount
Constants: PascalCase → MaxRetryCount
Interfaces: I + PascalCase → IBookingRepository
Files:
1 class per file (exceptions: small related DTOs)
File name = class name
Folder = namespace
Async:
All I/O operations MUST be async
Suffix with Async → GetBookingAsync()
Use CancellationToken in all async methods
Nullable:
Nullable reference types ENABLED
No null without explicit ? annotation
Use required keyword for non-optional properties
Service Template:
// Controllers — thin, delegate to Application layer
[ApiController]
[Route("api/[controller]")]
public class BookingsController(IMediator mediator) : ControllerBase
{
[HttpPost]
public async Task<ActionResult<BookingResponse>> Create(
CreateBookingRequest request,
CancellationToken ct)
{
var result = await mediator.Send(
new CreateBookingCommand(request), ct);
return CreatedAtAction(nameof(GetById),
new { id = result.Id }, result);
}
}
// Application — business logic orchestration
public class CreateBookingCommandHandler(
IBookingRepository repo,
IEventPublisher events)
: IRequestHandler<CreateBookingCommand, BookingResponse>
{
public async Task<BookingResponse> Handle(
CreateBookingCommand command,
CancellationToken ct)
{
var booking = Booking.Create(command.UserId, command.Destination);
await repo.AddAsync(booking, ct);
await events.PublishAsync(new BookingCreated(booking.Id), ct);
return booking.ToResponse();
}
}
// Domain — pure business rules, no dependencies
public class Booking
{
public required Guid Id { get; init; }
public required string UserId { get; init; }
public required string Destination { get; init; }
public BookingStatus Status { get; private set; }
public static Booking Create(string userId, string destination)
{
// Business rules here
return new Booking
{
Id = Guid.NewGuid(),
UserId = userId,
Destination = destination,
Status = BookingStatus.Pending
};
}
}
2.2 React 18 Frontend
Naming:
Components: PascalCase → BookingCard.tsx
Hooks: useCamelCase → useBookings.ts
Utilities: camelCase → formatDate.ts
Types: PascalCase → BookingResponse.ts
Constants: SCREAMING_SNAKE → API_BASE_URL
Structure per module:
modules/travel/
├── components/ ← UI components
├── hooks/ ← Custom hooks (data fetching, state)
├── types/ ← TypeScript types
├── services/ ← API calls
└── index.ts ← Module exports
Rules:
• Functional components only (no class components)
• TypeScript strict mode ON
• No any — type everything
• React Query (TanStack Query) for server state
• Zustand for client state (if needed)
• Tailwind CSS + shared design system components
2.3 Event Contracts
// src/shared/Contracts/Events/BookingCreated.cs
namespace Contracts.Events;
public record BookingCreated
{
public required string EventId { get; init; } = Guid.NewGuid().ToString();
public required string EventType { get; init; } = "travel.booking.created";
public required string Source { get; init; } = "travel-booking-service";
public required DateTimeOffset Timestamp { get; init; } = DateTimeOffset.UtcNow;
public required string CorrelationId { get; init; }
public required string Version { get; init; } = "1.0";
// Domain data
public required Guid BookingId { get; init; }
public required string UserId { get; init; }
public required string Destination { get; init; }
public required decimal TotalAmount { get; init; }
public required string Currency { get; init; }
}
3. Daily Development Workflow
3.1 Feature Development Flow
1. Pick task from board (Shaped → Building)
└── Read spec / acceptance criteria
2. Create branch
└── git checkout -b feature/travel-booking-search
3. Understand context (AI-first)
└── Cursor: @codebase "how does booking search work in legacy?"
└── If migrating: read legacy module analysis doc
4. Write tests first (TDD with AI)
└── Cursor: "Generate contract tests for GET /api/travel/search"
└── Cursor: "Generate unit tests for BookingSearchService"
└── Review AI-generated tests — validate business rules
5. Implement (AI-assisted)
└── Cursor Agent mode: implement across files
└── Human: review logic, adjust business rules
└── Run tests: dotnet test
6. Local verification
└── docker-compose up -d
└── Manual test in browser / Postman
└── Check logs: http://localhost:5341 (Seq)
7. Push & PR
└── git push origin feature/travel-booking-search
└── Create PR → CodeRabbit auto-reviews
└── Fix CodeRabbit issues
└── Request human review
8. After merge
└── Auto-deploys to Dev
└── Verify in Dev environment
└── Move task to Done
3.2 Migration-Specific Flow
1. AI Analyze Legacy Module
└── Gemini 2.5 Pro (full codebase context) or Cursor @codebase
└── Use prompt: prompts/migration/analyze-module.md
└── Output: dependency map, business rules, DB tables
2. AI Scaffold New Service
└── Cursor Agent: "Scaffold .NET 8 service for [Module]"
└── Use prompt: prompts/migration/scaffold-service.md
└── Output: project structure, DI, EF Core, Dockerfile
3. AI Migrate Business Logic
└── Claude Sonnet 4: translate .NET Framework → .NET 8
└── Use prompt: prompts/migration/migrate-business-logic.md
└── ⚠️ MANDATORY: human review every business rule
4. AI Generate Tests
└── Claude Sonnet 4: generate contract + unit tests
└── Use prompt: prompts/migration/generate-tests.md
└── Human: validate test cases cover real scenarios
5. Data Migration Setup
└── Claude Code: generate CDC configuration
└── Run CDC in Dev → verify data integrity
6. PR + Review + Deploy
└── Same as regular flow but deeper review on business logic
4. Testing Guide
4.1 Running Tests
# All tests
dotnet test
# Specific service
dotnet test src/services/travel-booking/tests/UnitTests
dotnet test src/services/travel-booking/tests/ContractTests
dotnet test src/services/travel-booking/tests/IntegrationTests
# With coverage report
dotnet test --collect:"XPlat Code Coverage"
reportgenerator -reports:**/coverage.cobertura.xml \
-targetdir:coverage-report -reporttypes:Html
# Frontend tests
cd src/frontend
npm test # unit tests (Vitest)
npm run test:e2e # E2E tests (Playwright)
# Contract test verification (Pact)
dotnet test --filter "Category=Pact"
4.2 Writing Tests
Unit Test (business logic):
public class BookingTests
{
[Fact]
public void Create_WithValidData_ReturnsBooking()
{
var booking = Booking.Create("user-1", "Tokyo");
Assert.NotEqual(Guid.Empty, booking.Id);
Assert.Equal(BookingStatus.Pending, booking.Status);
}
[Fact]
public void Create_WithEmptyDestination_ThrowsDomainException()
{
Assert.Throws<DomainException>(
() => Booking.Create("user-1", ""));
}
}
Contract Test (Pact):
public class TravelApiConsumerTests
{
private readonly IPactBuilderV4 _pact;
[Fact]
public async Task GetBooking_WhenExists_ReturnsBooking()
{
_pact
.UponReceiving("a request for a booking")
.WithRequest(HttpMethod.Get, "/api/travel/bookings/123")
.WillRespond()
.WithStatus(200)
.WithJsonBody(new
{
id = "123",
destination = Match.Type("Tokyo"),
status = Match.Regex("Pending|Confirmed|Cancelled",
"Pending")
});
await _pact.VerifyAsync(async ctx =>
{
var client = new TravelApiClient(ctx.MockServerUri);
var booking = await client.GetBookingAsync("123");
Assert.Equal("123", booking.Id);
});
}
}
5. AI Development Workflow
5.1 Cursor Setup
.cursorrules (in repo root):
You are working on a .NET 8 microservices project for legacy modernization.
Project context:
- Migrating legacy .NET monolith to .NET 8 microservices
- 6 bounded contexts: Travel, Event, Workforce, Comms, Reporting, Payment (legacy)
- Payment stays in legacy monolith Phase 1, accessed via ACL
- Event-driven architecture using Azure Service Bus
- Per-service databases, Clean Architecture per service
Coding rules:
- All async methods use CancellationToken
- All I/O is async/await
- Nullable reference types enabled
- Use records for DTOs and events
- Use MediatR for CQRS command/query handling
- Domain layer has zero external dependencies
- Infrastructure concerns stay in Infrastructure layer
- Use Serilog structured logging with correlation IDs
- Every public API endpoint needs contract test (Pact)
When migrating legacy code:
- Preserve ALL existing behavior (migration, not refactoring)
- Flag ambiguous logic with // TODO: REVIEW
- Update to .NET 8 patterns (async, nullable, records)
- Generate contract tests to verify backward compatibility
5.2 Claude Code Setup
CLAUDE.md (in repo root):
# Project: Product A Modernization
## Build & Run
- Build: `dotnet build`
- Test: `dotnet test`
- Run locally: `docker-compose up -d`
- Frontend: `cd src/frontend && npm run dev`
## Architecture
- .NET 8 microservices (Clean Architecture)
- React 18 frontend
- Azure Service Bus for events
- YARP API Gateway
- Per-service SQL databases
## Conventions
- Async everywhere with CancellationToken
- MediatR for CQRS
- Pact for contract testing
- Serilog structured logging
- Domain events via Azure Service Bus
## Migration Rules
- Strangler Fig pattern
- Payment frozen Phase 1 (ACL only)
- Every migration has contract tests
- Business logic migration requires human review
5.3 When to Use Which AI Tool
┌──────────────────────────────────────────────────────────┐
│ Task │ Tool │
│ ─────────────────────────── │ ────────────────────────── │
│ Inline code completion │ Cursor (Tab) │
│ Multi-file feature │ Cursor Agent mode │
│ Batch migration (10+ files) │ Claude Code (CLI) │
│ Legacy code analysis │ Cursor @codebase │
│ Architecture brainstorm │ Claude.ai (browser) │
│ React from mockup │ GPT-4o (multimodal) │
│ PR auto-review │ CodeRabbit (auto) │
│ Payment code review │ Ollama + Llama 3.3 (local) │
│ Generate tests │ Cursor Agent mode │
│ Debugging │ Cursor inline chat │
└──────────────────────────────────────────────────────────┘
6. Common Tasks (How-To)
6.1 Add a New Service
# 1. Scaffold (AI-assisted)
# In Cursor: "Scaffold new .NET 8 service called WorkforceService
# following the Travel Booking service pattern"
# 2. Or manually:
mkdir -p src/services/workforce/{Workforce.API,Workforce.Application,\
Workforce.Domain,Workforce.Infrastructure,tests/UnitTests,\
tests/ContractTests,tests/IntegrationTests}
# 3. Add to docker-compose.yml
# 4. Add to API Gateway routes (YARP config)
# 5. Create database (EF Core migration)
# 6. Add CI/CD workflow (.github/workflows/workforce-ci.yml)
# 7. Add to IaC (infra/modules/)
6.2 Add a New API Endpoint
# 1. Define contract (OpenAPI or request/response types)
# 2. Write contract test FIRST (Pact)
# 3. Add controller action
# 4. Add MediatR command/query handler
# 5. Add domain logic (if new business rule)
# 6. Add repository method (if DB access needed)
# 7. Run tests → PR → review
6.3 Publish a Domain Event
// 1. Define event in Contracts
public record BookingCancelled
{
public required Guid BookingId { get; init; }
public required string Reason { get; init; }
// ... standard event fields
}
// 2. Publish from domain handler
await eventPublisher.PublishAsync(
new BookingCancelled
{
BookingId = booking.Id,
Reason = command.Reason,
CorrelationId = correlationId
}, ct);
// 3. Subscribe in another service
public class BookingCancelledHandler
: IMessageHandler<BookingCancelled>
{
public async Task HandleAsync(
BookingCancelled @event, CancellationToken ct)
{
// React to event (e.g., send cancellation email)
await notificationService.SendCancellationNotice(
@event.BookingId, ct);
}
}
6.4 Call Legacy Payment (via ACL)
// In Travel Booking Service — call payment through ACL
public class PaymentAclClient(
HttpClient httpClient,
ILogger<PaymentAclClient> logger) : IPaymentGateway
{
public async Task<PaymentResult> ProcessAsync(
PaymentRequest request, CancellationToken ct)
{
// Translate new contract → legacy format
var legacyRequest = new
{
OrderId = request.BookingId.ToString(),
Amount = request.Amount,
CurrCode = request.Currency,
RefNo = request.IdempotencyKey
};
var response = await httpClient.PostAsJsonAsync(
"/api/legacy/payment/process", legacyRequest, ct);
var legacyResult = await response.Content
.ReadFromJsonAsync<LegacyPaymentResult>(ct);
// Translate legacy response → new contract
return new PaymentResult
{
Success = legacyResult?.Status == "OK",
TransactionId = legacyResult?.TxnId ?? "",
Status = MapStatus(legacyResult?.Status)
};
}
}
7. Troubleshooting
| Problem | Solution |
|---|---|
| Docker compose won't start | docker-compose down -v && docker-compose up -d (reset volumes) |
| SQL Server connection refused | Wait 30s after docker-compose up (SQL Server startup). Check docker logs sqlserver |
| Service Bus connection error | Check emulator is running: docker ps | grep servicebus |
| Tests fail locally but pass in CI | Check Docker Compose running. Run docker-compose ps |
| Cursor @codebase not finding code | Re-index: Cmd+Shift+P → "Cursor: Reindex" |
| Claude Code not understanding project | Ensure CLAUDE.md exists in repo root with correct context |
| Port conflict | Check lsof -i :5000 — kill conflicting process or change ports |
| EF Core migration fails | dotnet ef database update --project Infrastructure --startup-project API |
| Contract test fails after API change | Update consumer expectations first, then provider. Pact is consumer-driven |
| CodeRabbit too noisy | Adjust .coderabbit.yaml rules in repo root |