feat: Add functional tests for OrderService and update InventoryService command and idempotency logic.
This commit is contained in:
528
.agent/skills/development-lifecycle/SKILL.md
Normal file
528
.agent/skills/development-lifecycle/SKILL.md
Normal file
@@ -0,0 +1,528 @@
|
||||
---
|
||||
name: development-lifecycle
|
||||
description: Development lifecycle workflow - Code → Build → Test → Fix → Deploy. Use for understanding complete development process, debugging build errors, fixing test failures, and continuous improvement cycles in GoodGo microservices.
|
||||
compatibility: ".NET 10+, Docker 24+, docker-compose 2.x"
|
||||
metadata:
|
||||
author: Velik Ho
|
||||
version: "1.0"
|
||||
---
|
||||
|
||||
# Development Lifecycle / Quy Trình Phát Triển
|
||||
|
||||
Complete iterative development workflow from code to deployment with focus on troubleshooting and debugging.
|
||||
|
||||
## When to Use This Skill / Khi Nào Sử Dụng
|
||||
|
||||
Use this skill when:
|
||||
- Starting development on a new feature / Bắt đầu phát triển feature mới
|
||||
- Debugging build or test failures / Debug lỗi build hoặc test
|
||||
- Understanding the complete dev cycle / Hiểu quy trình phát triển hoàn chỉnh
|
||||
- Setting up local development environment / Setup môi trường local
|
||||
- Troubleshooting deployment issues / Khắc phục sự cố deployment
|
||||
|
||||
## Core Concepts / Khái Niệm Cốt Lõi
|
||||
|
||||
### Development Cycle / Chu Kỳ Phát Triển
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
A[① CODE<br/>Write Features] --> B[② BUILD<br/>Compile & Package]
|
||||
B --> C[③ TEST<br/>Run Tests]
|
||||
C --> D{Pass?}
|
||||
D -->|No| E[④ FIX<br/>Debug & Resolve]
|
||||
E --> B
|
||||
D -->|Yes| F[⑤ DEPLOY<br/>Local/Staging/Prod]
|
||||
F --> G{Issues?}
|
||||
G -->|Yes| E
|
||||
G -->|No| H[✓ Done]
|
||||
```
|
||||
|
||||
### Philosophy / Triết Lý
|
||||
|
||||
1. **Fail Fast** - Phát hiện lỗi sớm nhất có thể
|
||||
2. **Iterative** - Cải tiến liên tục qua từng cycle
|
||||
3. **Automated** - Tự động hóa các bước lặp lại
|
||||
4. **Observable** - Luôn có visibility vào system state
|
||||
|
||||
## Phase 1: CODE ✍️
|
||||
|
||||
**Goal**: Viết code sạch, maintainable theo architecture chuẩn
|
||||
|
||||
### Workflow
|
||||
|
||||
**Use existing skills** cho coding phase:
|
||||
- [dotnet-microservice-workflow](../dotnet-microservice-workflow/SKILL.md) - 4-layer architecture
|
||||
- [api-design](../api-design/SKILL.md) - RESTful endpoints
|
||||
- [cqrs-mediatr](../cqrs-mediatr/SKILL.md) - Commands/Queries
|
||||
- [domain-driven-design](../domain-driven-design/SKILL.md) - Domain modeling
|
||||
|
||||
### Best Practices
|
||||
|
||||
```csharp
|
||||
// ✅ GOOD: Clear intention, follows patterns
|
||||
public class CreateOrderCommandHandler : IRequestHandler<CreateOrderCommand, OrderResult>
|
||||
{
|
||||
private readonly IOrderRepository _repository;
|
||||
|
||||
public async Task<OrderResult> Handle(CreateOrderCommand cmd, CancellationToken ct)
|
||||
{
|
||||
var order = new Order(cmd.UserId, cmd.ShippingAddress);
|
||||
foreach (var item in cmd.Items)
|
||||
order.AddItem(item.ProductId, item.Quantity, item.Price);
|
||||
|
||||
await _repository.AddAsync(order, ct);
|
||||
await _repository.UnitOfWork.SaveChangesAsync(ct);
|
||||
|
||||
return new OrderResult(order.Id);
|
||||
}
|
||||
}
|
||||
|
||||
// ❌ BAD: Mixed concerns, no separation
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> CreateOrder(CreateOrderRequest request)
|
||||
{
|
||||
var order = new Order { UserId = request.UserId };
|
||||
_context.Orders.Add(order);
|
||||
await _context.SaveChangesAsync();
|
||||
return Ok(order.Id);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: BUILD 🔨
|
||||
|
||||
**Goal**: Compile code và tạo artifacts (DLLs, Docker images)
|
||||
|
||||
### Local .NET Build
|
||||
|
||||
```bash
|
||||
# Restore NuGet packages
|
||||
dotnet restore
|
||||
|
||||
# Build solution
|
||||
dotnet build
|
||||
|
||||
# Build specific project
|
||||
dotnet build src/MyService.API/
|
||||
|
||||
# Release build
|
||||
dotnet build -c Release
|
||||
|
||||
# Verbose output (for debugging)
|
||||
dotnet build -v detailed
|
||||
```
|
||||
|
||||
### Docker Build
|
||||
|
||||
```bash
|
||||
# Build service image
|
||||
docker build -t my-service:latest -f services/my-service-net/Dockerfile .
|
||||
|
||||
# Build via docker-compose
|
||||
docker-compose -f deployments/local/docker-compose.yml build my-service-net
|
||||
|
||||
# No cache (clean build)
|
||||
docker-compose -f deployments/local/docker-compose.yml build --no-cache my-service-net
|
||||
|
||||
# Parallel build multiple services
|
||||
docker-compose -f deployments/local/docker-compose.yml build --parallel
|
||||
```
|
||||
|
||||
### Common Build Errors
|
||||
|
||||
| Error Code | Cause | Solution |
|
||||
|------------|-------|----------|
|
||||
| **CS0246** | Missing type/namespace | Run `dotnet restore`, add package reference |
|
||||
| **CS0103** | Name does not exist | Add `using` statement |
|
||||
| **CS1061** | Missing member | Check property/method name, NuGet version |
|
||||
| **NU1101** | Unable to find package | Check package name, NuGet source |
|
||||
| **Docker context** | Wrong build context | Verify `-f` and context path |
|
||||
|
||||
**See detailed solutions**: [references/build-errors.md](references/build-errors.md)
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: TEST 🧪
|
||||
|
||||
**Goal**: Validate correctness through automated tests
|
||||
|
||||
### Test Pyramid
|
||||
|
||||
```
|
||||
┌──────────────┐
|
||||
│ E2E Tests │ ← ~5% - Slow, brittle
|
||||
│ (UI/API) │
|
||||
├──────────────┤
|
||||
│ Integration │ ← ~15% - Medium speed
|
||||
│ Tests │ Database, HTTP
|
||||
├──────────────┤
|
||||
│ Unit Tests │ ← ~80% - Fast, isolated
|
||||
│ (Handlers, │ Pure logic
|
||||
│ Domain) │
|
||||
└──────────────┘
|
||||
```
|
||||
|
||||
### Test Commands
|
||||
|
||||
```bash
|
||||
# Run all tests
|
||||
dotnet test
|
||||
|
||||
# Run specific test project
|
||||
dotnet test tests/MyService.UnitTests/
|
||||
|
||||
# Run with filter
|
||||
dotnet test --filter Category=Unit
|
||||
dotnet test --filter FullyQualifiedName~CreateOrderHandler
|
||||
|
||||
# Watch mode (TDD)
|
||||
dotnet watch test --project tests/MyService.UnitTests/
|
||||
|
||||
# Coverage report
|
||||
dotnet test /p:CollectCoverage=true /p:CoverageReportFormat=opencover
|
||||
|
||||
# Detailed output
|
||||
dotnet test --logger "console;verbosity=detailed"
|
||||
```
|
||||
|
||||
### Test Structure
|
||||
|
||||
**Use testing skill**: [dotnet-senior-tester](../dotnet-senior-tester/SKILL.md)
|
||||
|
||||
```csharp
|
||||
// ✅ GOOD: Arrange-Act-Assert, clear mocking
|
||||
public class CreateOrderCommandHandlerTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task Handle_ValidCommand_CreatesOrder()
|
||||
{
|
||||
// Arrange
|
||||
var repository = Substitute.For<IOrderRepository>();
|
||||
var unitOfWork = Substitute.For<IUnitOfWork>();
|
||||
repository.UnitOfWork.Returns(unitOfWork);
|
||||
|
||||
var handler = new CreateOrderCommandHandler(repository);
|
||||
var command = new CreateOrderCommand(UserId: Guid.NewGuid(), /* ... */);
|
||||
|
||||
// Act
|
||||
var result = await handler.Handle(command, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
await repository.Received(1).AddAsync(Arg.Any<Order>(), Arg.Any<CancellationToken>());
|
||||
await unitOfWork.Received(1).SaveChangesAsync(Arg.Any<CancellationToken>());
|
||||
Assert.NotEqual(Guid.Empty, result.OrderId);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**See test failure patterns**: [references/test-failures.md](references/test-failures.md)
|
||||
|
||||
---
|
||||
|
||||
## Phase 4: FIX 🔧
|
||||
|
||||
**Goal**: Debug và resolve issues nhanh chóng
|
||||
|
||||
### Debugging Workflow
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[Error Detected] --> B{Error Type?}
|
||||
B -->|Build| C[Check Build Logs]
|
||||
B -->|Test| D[Check Test Output]
|
||||
B -->|Runtime| E[Check App Logs]
|
||||
|
||||
C --> F[Identify Root Cause]
|
||||
D --> F
|
||||
E --> F
|
||||
|
||||
F --> G[Apply Fix]
|
||||
G --> H[Verify Fix]
|
||||
H --> I{Fixed?}
|
||||
I -->|No| F
|
||||
I -->|Yes| J[Document Solution]
|
||||
```
|
||||
|
||||
### Debugging Tools
|
||||
|
||||
**Build Errors:**
|
||||
```bash
|
||||
# Verbose build output
|
||||
dotnet build -v detailed
|
||||
|
||||
# Clean and rebuild
|
||||
dotnet clean && dotnet build
|
||||
|
||||
# Check references
|
||||
dotnet list package
|
||||
```
|
||||
|
||||
**Test Failures:**
|
||||
```bash
|
||||
# Run single test with details
|
||||
dotnet test --filter TestMethodName --logger "console;verbosity=detailed"
|
||||
|
||||
# Debug test in VS Code
|
||||
# Set breakpoint, F5 to debug
|
||||
```
|
||||
|
||||
**Runtime Issues:**
|
||||
```bash
|
||||
# Container logs
|
||||
docker logs -f my-service-net
|
||||
docker logs --tail 100 my-service-net
|
||||
|
||||
# Follow logs
|
||||
docker-compose -f deployments/local/docker-compose.yml logs -f my-service-net
|
||||
|
||||
# Exec into container
|
||||
docker exec -it my-service-net sh
|
||||
|
||||
# Check health endpoint
|
||||
curl http://localhost/api/v1/my-service/health
|
||||
```
|
||||
|
||||
### Common Fix Patterns
|
||||
|
||||
| Issue | Pattern | Example |
|
||||
|-------|---------|---------|
|
||||
| **NullReferenceException** | Check mock setup | `repository.Setup(x => x.Get()).Returns(entity)` |
|
||||
| **Async deadlock** | Use `ConfigureAwait(false)` | `await task.ConfigureAwait(false);` |
|
||||
| **Database timeout** | Check connection string, indexes | Add index on frequently queried columns |
|
||||
| **Container won't start** | Check environment vars, ports | Verify `docker-compose.yml` config |
|
||||
|
||||
**See detailed guide**: [references/debugging-guide.md](references/debugging-guide.md)
|
||||
|
||||
---
|
||||
|
||||
## Phase 5: DEPLOY 🚀
|
||||
|
||||
**Goal**: Deploy và verify application in target environment
|
||||
|
||||
### Local Deployment
|
||||
|
||||
```bash
|
||||
# Start service with docker-compose
|
||||
docker-compose -f deployments/local/docker-compose.yml up -d my-service-net
|
||||
|
||||
# Check status
|
||||
docker-compose -f deployments/local/docker-compose.yml ps
|
||||
|
||||
# Health check
|
||||
curl http://localhost/api/v1/my-service/health
|
||||
|
||||
# Swagger UI
|
||||
open http://localhost/api/v1/my-service/swagger
|
||||
```
|
||||
|
||||
### Health Checks
|
||||
|
||||
Every service MUST have `/health` endpoint:
|
||||
|
||||
```csharp
|
||||
// Program.cs
|
||||
builder.Services.AddHealthChecks()
|
||||
.AddNpgSql(connectionString, name: "database")
|
||||
.AddRedis(redisConnection, name: "redis");
|
||||
|
||||
app.MapHealthChecks("/health");
|
||||
```
|
||||
|
||||
### Verification Checklist
|
||||
|
||||
- [ ] Service starts without errors
|
||||
- [ ] `/health` returns 200 OK
|
||||
- [ ] Database migrations applied
|
||||
- [ ] Swagger UI accessible
|
||||
- [ ] Sample API call works
|
||||
- [ ] Logs show no errors
|
||||
|
||||
### Staging/Production
|
||||
|
||||
**Use deployment skill**: [deployment-kubernetes](../deployment-kubernetes/SKILL.md)
|
||||
|
||||
```bash
|
||||
# kubectl commands for K8s
|
||||
kubectl apply -f deployments/kubernetes/my-service.yaml
|
||||
kubectl rollout status deployment/my-service
|
||||
kubectl get pods -l app=my-service
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Development Patterns / Các Mẫu Phát Triển
|
||||
|
||||
### Pattern 1: Test-Driven Development (TDD)
|
||||
|
||||
```
|
||||
1. ❌ RED - Write failing test
|
||||
2. ✅ GREEN - Write minimal code to pass
|
||||
3. 🔧 REFACTOR - Improve code quality
|
||||
4. Repeat
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- Better design (testable code)
|
||||
- High confidence in changes
|
||||
- Living documentation
|
||||
|
||||
### Pattern 2: Incremental Development
|
||||
|
||||
```bash
|
||||
# Small commits with working state
|
||||
git commit -m "feat: add CreateOrder command"
|
||||
git commit -m "test: add CreateOrderHandler tests"
|
||||
git commit -m "fix: validate order items quantity"
|
||||
```
|
||||
|
||||
### Pattern 3: Hot Reload Development
|
||||
|
||||
```bash
|
||||
# .NET hot reload (faster iteration)
|
||||
dotnet watch run --project src/MyService.API
|
||||
|
||||
# Docker with volume mount (for config changes)
|
||||
docker-compose up --build
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Helper Scripts / Scripts Hỗ Trợ
|
||||
|
||||
### Check Environment
|
||||
|
||||
```bash
|
||||
# Verify development environment
|
||||
./scripts/check-env.sh
|
||||
```
|
||||
|
||||
### Quick Build
|
||||
|
||||
```bash
|
||||
# Build single service quickly
|
||||
./scripts/quick-build.sh my-service-net
|
||||
```
|
||||
|
||||
### Debug Build
|
||||
|
||||
```bash
|
||||
# Debug build errors with verbose output
|
||||
./scripts/debug-build.sh my-service-net
|
||||
```
|
||||
|
||||
### Health Check
|
||||
|
||||
```bash
|
||||
# Check all services health
|
||||
./scripts/health-check.sh
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Common Mistakes / Lỗi Thường Gặp
|
||||
|
||||
### 1. Skip Tests
|
||||
|
||||
**Problem**: Bugs go to production
|
||||
**Solution**: Write tests first (TDD) or immediately after feature
|
||||
|
||||
```bash
|
||||
# ❌ BAD: Push without testing
|
||||
git push origin main
|
||||
|
||||
# ✅ GOOD: Always test first
|
||||
dotnet test && git push origin main
|
||||
```
|
||||
|
||||
### 2. Ignore Build Warnings
|
||||
|
||||
**Problem**: Tech debt accumulates, future breaks
|
||||
**Solution**: Treat warnings as errors
|
||||
|
||||
```xml
|
||||
<!-- MyService.API.csproj -->
|
||||
<PropertyGroup>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
```
|
||||
|
||||
### 3. No Logging
|
||||
|
||||
**Problem**: Hard to debug production issues
|
||||
**Solution**: Add structured logging
|
||||
|
||||
```csharp
|
||||
// ✅ GOOD: Structured logging
|
||||
_logger.LogInformation("Creating order for user {UserId} with {ItemCount} items",
|
||||
command.UserId, command.Items.Count);
|
||||
```
|
||||
|
||||
### 4. Skip Health Checks
|
||||
|
||||
**Problem**: Can't verify deployment success
|
||||
**Solution**: Always add `/health` endpoint
|
||||
|
||||
```csharp
|
||||
app.MapHealthChecks("/health", new HealthCheckOptions
|
||||
{
|
||||
ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Quick Reference / Tham Chiếu Nhanh
|
||||
|
||||
### Development Commands
|
||||
|
||||
| Task | Command |
|
||||
|------|---------|
|
||||
| **Restore packages** | `dotnet restore` |
|
||||
| **Build** | `dotnet build` |
|
||||
| **Run tests** | `dotnet test` |
|
||||
| **Run locally** | `dotnet run --project src/MyService.API` |
|
||||
| **Watch mode** | `dotnet watch run` |
|
||||
| **Hot reload tests** | `dotnet watch test` |
|
||||
|
||||
### Docker Commands
|
||||
|
||||
| Task | Command |
|
||||
|------|---------|
|
||||
| **Build image** | `docker-compose build my-service-net` |
|
||||
| **Start service** | `docker-compose up -d my-service-net` |
|
||||
| **View logs** | `docker-compose logs -f my-service-net` |
|
||||
| **Restart** | `docker-compose restart my-service-net` |
|
||||
| **Stop** | `docker-compose down` |
|
||||
| **Rebuild & start** | `docker-compose up -d --build my-service-net` |
|
||||
|
||||
### Debugging Commands
|
||||
|
||||
| Task | Command |
|
||||
|------|---------|
|
||||
| **Verbose build** | `dotnet build -v detailed` |
|
||||
| **Clean build** | `dotnet clean && dotnet build` |
|
||||
| **Test with logs** | `dotnet test --logger console` |
|
||||
| **Container logs** | `docker logs -f my-service-net` |
|
||||
| **Exec into container** | `docker exec -it my-service-net sh` |
|
||||
|
||||
---
|
||||
|
||||
## Resources / Tài Nguyên
|
||||
|
||||
### Related Skills
|
||||
- [dotnet-microservice-workflow](../dotnet-microservice-workflow/SKILL.md) - 4-phase architecture workflow
|
||||
- [dotnet-senior-tester](../dotnet-senior-tester/SKILL.md) - Comprehensive testing
|
||||
- [docker-traefik](../docker-traefik/SKILL.md) - Docker patterns
|
||||
- [error-handling-patterns](../error-handling-patterns/SKILL.md) - Exception handling
|
||||
- [project-rules](../project-rules/SKILL.md) - Coding standards
|
||||
|
||||
### Helper Resources
|
||||
- [Build Errors Catalog](references/build-errors.md) - Common build errors and solutions
|
||||
- [Test Failures Guide](references/test-failures.md) - Test debugging patterns
|
||||
- [Debugging Guide](references/debugging-guide.md) - Advanced debugging techniques
|
||||
|
||||
### External Resources
|
||||
- [.NET CLI Reference](https://learn.microsoft.com/en-us/dotnet/core/tools/)
|
||||
- [Docker Compose Reference](https://docs.docker.com/compose/compose-file/)
|
||||
- [xUnit Documentation](https://xunit.net/)
|
||||
298
.agent/skills/development-lifecycle/references/build-errors.md
Normal file
298
.agent/skills/development-lifecycle/references/build-errors.md
Normal file
@@ -0,0 +1,298 @@
|
||||
# Common Build Errors Catalog
|
||||
|
||||
Quick reference for troubleshooting .NET and Docker build errors in GoodGo microservices.
|
||||
|
||||
## .NET Build Errors
|
||||
|
||||
### CS0246: Type or namespace not found
|
||||
|
||||
**Error:**
|
||||
```
|
||||
error CS0246: The type or namespace name 'X' could not be found
|
||||
```
|
||||
|
||||
**Common Causes:**
|
||||
1. Missing package reference
|
||||
2. Incorrect namespace
|
||||
3. Package not restored
|
||||
|
||||
**Solutions:**
|
||||
```bash
|
||||
# 1. Restore packages
|
||||
dotnet restore
|
||||
|
||||
# 2. Add missing package
|
||||
dotnet add package <PackageName>
|
||||
|
||||
# 3. Check using statements
|
||||
# Add: using YourNamespace;
|
||||
|
||||
# 4. Clean and rebuild
|
||||
dotnet clean && dotnet build
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### CS0103: Name does not exist in current context
|
||||
|
||||
**Error:**
|
||||
```
|
||||
error CS0103: The name 'X' does not exist in the current context
|
||||
```
|
||||
|
||||
**Common Causes:**
|
||||
1. Missing `using` directive
|
||||
2. Typo in variable/method name
|
||||
3. Wrong scope
|
||||
|
||||
**Solutions:**
|
||||
```csharp
|
||||
// Add using directive
|
||||
using System.Linq;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
// Check variable is in scope
|
||||
public void Method()
|
||||
{
|
||||
var item = new Item();
|
||||
// item is only available within Method()
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### CS1061: Does not contain a definition
|
||||
|
||||
**Error:**
|
||||
```
|
||||
error CS1061: 'Type' does not contain a definition for 'Member'
|
||||
```
|
||||
|
||||
**Common Causes:**
|
||||
1. Typo in property/method name
|
||||
2. Wrong NuGet package version
|
||||
3. Extension method not imported
|
||||
|
||||
**Solutions:**
|
||||
```csharp
|
||||
// 1. Check spelling
|
||||
order.Items // not order.Item
|
||||
|
||||
// 2. Import extension methods
|
||||
using System.Linq; // for .Where(), .Select(), etc.
|
||||
|
||||
// 3. Update package version
|
||||
dotnet list package --outdated
|
||||
dotnet add package Microsoft.EntityFrameworkCore --version 10.0.0
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### NU1101: Unable to find package
|
||||
|
||||
**Error:**
|
||||
```
|
||||
error NU1101: Unable to find package 'X'. No packages exist with this id
|
||||
```
|
||||
|
||||
**Solutions:**
|
||||
```bash
|
||||
# 1. Check package name (spelling)
|
||||
dotnet add package Swashbuckle.AspNetCore # not Swashbuckle.AspNet
|
||||
|
||||
# 2. Check NuGet sources
|
||||
dotnet nuget list source
|
||||
|
||||
# 3. Clear NuGet cache
|
||||
dotnet nuget locals all --clear
|
||||
|
||||
# 4. Restore with verbose
|
||||
dotnet restore -v detailed
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### MSB3277: Version conflicts
|
||||
|
||||
**Error:**
|
||||
```
|
||||
warning MSB3277: Found conflicts between different versions of "X"
|
||||
```
|
||||
|
||||
**Solutions:**
|
||||
```xml
|
||||
<!-- Directory.Build.props or .csproj -->
|
||||
<ItemGroup>
|
||||
<!-- Force specific version across all projects -->
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.2" />
|
||||
</ItemGroup>
|
||||
```
|
||||
|
||||
```bash
|
||||
# Or update all references
|
||||
dotnet add package Microsoft.EntityFrameworkCore --version 10.0.2
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Docker Build Errors
|
||||
|
||||
### Cannot connect to Docker daemon
|
||||
|
||||
**Error:**
|
||||
```
|
||||
Cannot connect to the Docker daemon at unix:///var/run/docker.sock
|
||||
```
|
||||
|
||||
**Solutions:**
|
||||
```bash
|
||||
# 1. Start Docker Desktop (macOS/Windows)
|
||||
open -a Docker
|
||||
|
||||
# 2. Check Docker status
|
||||
docker info
|
||||
|
||||
# 3. Restart Docker daemon (Linux)
|
||||
sudo systemctl restart docker
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Context error: Dockerfile not found
|
||||
|
||||
**Error:**
|
||||
```
|
||||
ERROR: failed to solve: failed to read dockerfile: failed to resolve path
|
||||
```
|
||||
|
||||
**Common Causes:**
|
||||
- Wrong build context
|
||||
- Dockerfile in wrong location
|
||||
|
||||
**Solutions:**
|
||||
```bash
|
||||
# Correct: Build from repo root with context
|
||||
docker build -t my-service -f services/my-service-net/Dockerfile .
|
||||
|
||||
# ❌ Wrong: Building from service directory
|
||||
cd services/my-service-net
|
||||
docker build -t my-service .
|
||||
|
||||
# ✅ Correct: Specify context
|
||||
docker build -t my-service -f Dockerfile ../..
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### COPY failed: file not found
|
||||
|
||||
**Error:**
|
||||
```
|
||||
ERROR: failed to copy files: file not found
|
||||
```
|
||||
|
||||
**Common Cause:** Wrong COPY path in Dockerfile
|
||||
|
||||
**Solutions:**
|
||||
```dockerfile
|
||||
# ❌ Wrong: Path relative to Dockerfile location
|
||||
COPY src/ /app/
|
||||
|
||||
# ✅ Correct: Path relative to build context (repo root)
|
||||
COPY services/my-service-net/src/ /app/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Docker build cache issues
|
||||
|
||||
**Problem:** Old files being used despite changes
|
||||
|
||||
**Solutions:**
|
||||
```bash
|
||||
# Build without cache
|
||||
docker build --no-cache -t my-service .
|
||||
|
||||
# Or via docker-compose
|
||||
docker-compose build --no-cache my-service-net
|
||||
|
||||
# Clear all build cache
|
||||
docker builder prune -a
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Entity Framework Errors
|
||||
|
||||
### No migrations found
|
||||
|
||||
**Error:**
|
||||
```
|
||||
No migrations were found
|
||||
```
|
||||
|
||||
**Solutions:**
|
||||
```bash
|
||||
# Create initial migration
|
||||
dotnet ef migrations add InitialCreate --project src/MyService.Infrastructure
|
||||
|
||||
# Verify migrations folder exists
|
||||
ls src/MyService.Infrastructure/Migrations/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Connection string error
|
||||
|
||||
**Error:**
|
||||
```
|
||||
A connection string could not be constructed
|
||||
```
|
||||
|
||||
**Solutions:**
|
||||
```bash
|
||||
# 1. Add connection string to appsettings.json
|
||||
{
|
||||
"ConnectionStrings": {
|
||||
"DefaultConnection": "Host=localhost;Database=mydb;..."
|
||||
}
|
||||
}
|
||||
|
||||
# 2. Set environment variable
|
||||
export ConnectionStrings__DefaultConnection="Host=..."
|
||||
|
||||
# 3. Verify in Program.cs
|
||||
builder.Services.AddDbContext<MyContext>(options =>
|
||||
options.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnection")));
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Quick Troubleshooting Checklist
|
||||
|
||||
When build fails:
|
||||
|
||||
- [ ] Run `dotnet restore`
|
||||
- [ ] Check for typos
|
||||
- [ ] Verify `using` statements
|
||||
- [ ] Check package versions (`dotnet list package`)
|
||||
- [ ] Clean build: `dotnet clean && dotnet build`
|
||||
- [ ] Check build logs: `dotnet build -v detailed`
|
||||
- [ ] Clear NuGet cache: `dotnet nuget locals all --clear`
|
||||
|
||||
When Docker build fails:
|
||||
|
||||
- [ ] Check Docker daemon is running
|
||||
- [ ] Verify build context path
|
||||
- [ ] Check Dockerfile COPY paths
|
||||
- [ ] Try `--no-cache` build
|
||||
- [ ] Check `.dockerignore` file
|
||||
- [ ] Verify base image exists
|
||||
|
||||
---
|
||||
|
||||
## Related Resources
|
||||
|
||||
- [Test Failures Guide](test-failures.md)
|
||||
- [Debugging Guide](debugging-guide.md)
|
||||
- Main Skill: [development-lifecycle](../SKILL.md)
|
||||
@@ -0,0 +1,462 @@
|
||||
# Debugging Guide
|
||||
|
||||
Advanced debugging techniques for .NET microservices development.
|
||||
|
||||
## Visual Studio Code Debugging
|
||||
|
||||
### Setup launch.json
|
||||
|
||||
```json
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": ".NET Debug",
|
||||
"type": "coreclr",
|
||||
"request": "launch",
|
||||
"preLaunchTask": "build",
|
||||
"program": "${workspaceFolder}/src/MyService.API/bin/Debug/net10.0/MyService.API.dll",
|
||||
"args": [],
|
||||
"cwd": "${workspaceFolder}/src/MyService.API",
|
||||
"console": "internalConsole",
|
||||
"stopAtEntry": false,
|
||||
"env": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": ".NET Attach",
|
||||
"type": "coreclr",
|
||||
"request": "attach"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Debug Tests
|
||||
|
||||
```json
|
||||
{
|
||||
"name": ".NET Test Debug",
|
||||
"type": "coreclr",
|
||||
"request": "launch",
|
||||
"preLaunchTask": "build",
|
||||
"program": "dotnet",
|
||||
"args": [
|
||||
"test",
|
||||
"--filter",
|
||||
"FullyQualifiedName~MyTestMethod"
|
||||
],
|
||||
"cwd": "${workspaceFolder}",
|
||||
"console": "internalConsole"
|
||||
}
|
||||
```
|
||||
|
||||
**Usage:**
|
||||
1. Set breakpoint in test or code
|
||||
2. F5 to start debugging
|
||||
3. Step through with F10 (over) / F11 (into)
|
||||
|
||||
---
|
||||
|
||||
## Remote Debugging Docker Containers
|
||||
|
||||
### Enable remote debugging in Dockerfile
|
||||
|
||||
```dockerfile
|
||||
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS debug
|
||||
WORKDIR /src
|
||||
COPY . .
|
||||
RUN dotnet restore
|
||||
RUN dotnet build --no-restore
|
||||
|
||||
# Install debugger
|
||||
RUN apt-get update && apt-get install -y procps
|
||||
|
||||
EXPOSE 8080
|
||||
EXPOSE 4024
|
||||
|
||||
ENTRYPOINT ["dotnet", "run", "--no-build", "--project", "src/MyService.API"]
|
||||
```
|
||||
|
||||
### Docker Compose debug config
|
||||
|
||||
```yaml
|
||||
services:
|
||||
my-service-debug:
|
||||
build:
|
||||
context: ../..
|
||||
dockerfile: services/my-service-net/Dockerfile
|
||||
target: debug
|
||||
environment:
|
||||
- ASPNETCORE_ENVIRONMENT=Development
|
||||
- DOTNET_USE_POLLING_FILE_WATCHER=true
|
||||
ports:
|
||||
- "8080:8080"
|
||||
- "4024:4024" # Debugger port
|
||||
volumes:
|
||||
- ./src:/src:ro
|
||||
```
|
||||
|
||||
### Attach to running container
|
||||
|
||||
```bash
|
||||
# 1. Start container with debug enabled
|
||||
docker-compose up my-service-debug
|
||||
|
||||
# 2. Find process ID
|
||||
docker exec my-service-debug ps aux | grep dotnet
|
||||
|
||||
# 3. In VS Code, use "Attach to Process" configuration
|
||||
# Select the dotnet process from Docker container
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Logging Strategies
|
||||
|
||||
### Structured Logging with Serilog
|
||||
|
||||
```csharp
|
||||
// Program.cs
|
||||
builder.Host.UseSerilog((context, config) =>
|
||||
{
|
||||
config
|
||||
.ReadFrom.Configuration(context.Configuration)
|
||||
.Enrich.FromLogContext()
|
||||
.Enrich.WithProperty("Service", "MyService")
|
||||
.WriteTo.Console(new JsonFormatter())
|
||||
.WriteTo.File("logs/log-.txt", rollingInterval: RollingInterval.Day);
|
||||
});
|
||||
|
||||
// Usage in code
|
||||
_logger.LogInformation(
|
||||
"Creating order for {UserId} with {ItemCount} items and total {Total:C}",
|
||||
command.UserId,
|
||||
command.Items.Count,
|
||||
order.Total
|
||||
);
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- Searchable structured data
|
||||
- Context preservation
|
||||
- Easy filtering
|
||||
|
||||
---
|
||||
|
||||
### Log Scopes
|
||||
|
||||
```csharp
|
||||
using (_logger.BeginScope("OrderId: {OrderId}", orderId))
|
||||
{
|
||||
_logger.LogInformation("Processing order");
|
||||
_logger.LogInformation("Validating items");
|
||||
_logger.LogInformation("Calculating total");
|
||||
}
|
||||
|
||||
// All logs include OrderId automatically
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Debugging Tools
|
||||
|
||||
### dotnet-dump (Memory dumps)
|
||||
|
||||
```bash
|
||||
# Install tool
|
||||
dotnet tool install -g dotnet-dump
|
||||
|
||||
# Create dump of running process
|
||||
dotnet-dump collect -p <pid>
|
||||
|
||||
# Analyze dump
|
||||
dotnet-dump analyze <dump-file>
|
||||
|
||||
# Inside analyzer
|
||||
> clrstack # View call stack
|
||||
> dumpheap -stat # Heap statistics
|
||||
> eeheap -gc # GC heap info
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### dotnet-trace (Performance tracing)
|
||||
|
||||
```bash
|
||||
# Install tool
|
||||
dotnet tool install -g dotnet-trace
|
||||
|
||||
# Collect trace
|
||||
dotnet-trace collect -p <pid> --duration 00:00:30
|
||||
|
||||
# Convert to speedscope format
|
||||
dotnet-trace convert trace.nettrace --format speedscope
|
||||
|
||||
# View in browser
|
||||
open https://www.speedscope.app/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### dotnet-counters (Live metrics)
|
||||
|
||||
```bash
|
||||
# Install tool
|
||||
dotnet tool install -g dotnet-counters
|
||||
|
||||
# Monitor live
|
||||
dotnet-counters monitor -p <pid>
|
||||
|
||||
# Custom metrics
|
||||
dotnet-counters monitor \
|
||||
-p <pid> \
|
||||
--counters System.Runtime,Microsoft.AspNetCore.Hosting
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Database Debugging
|
||||
|
||||
### EF Core Query Logging
|
||||
|
||||
```csharp
|
||||
// Enable sensitive data logging (Development only!)
|
||||
builder.Services.AddDbContext<MyContext>(options =>
|
||||
{
|
||||
options.UseNpgsql(connectionString);
|
||||
|
||||
if (builder.Environment.IsDevelopment())
|
||||
{
|
||||
options
|
||||
.EnableSensitiveDataLogging()
|
||||
.EnableDetailedErrors();
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
**Output:**
|
||||
```
|
||||
Executed DbCommand (23ms) [Parameters=[@p0='123'], CommandType='Text']
|
||||
SELECT * FROM orders WHERE id = @p0
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### SQL Profiling
|
||||
|
||||
```bash
|
||||
# PostgreSQL: Enable query logging
|
||||
# In postgresql.conf or docker-compose
|
||||
POSTGRES_EXTRA_FLAGS="-c log_statement=all"
|
||||
|
||||
# View logs
|
||||
docker-compose logs -f postgres | grep "statement:"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## HTTP Debugging
|
||||
|
||||
### Fiddler / Postman for API testing
|
||||
|
||||
**Setup authentication:**
|
||||
```bash
|
||||
# Get JWT token
|
||||
curl -X POST http://localhost/api/v1/auth/login \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"username":"test","password":"test123"}'
|
||||
|
||||
# Use token in requests
|
||||
curl http://localhost/api/v1/orders \
|
||||
-H "Authorization: Bearer <token>"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Request/Response logging middleware
|
||||
|
||||
```csharp
|
||||
// Custom middleware
|
||||
public class RequestResponseLoggingMiddleware
|
||||
{
|
||||
private readonly RequestDelegate _next;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
public async Task InvokeAsync(HttpContext context)
|
||||
{
|
||||
// Log request
|
||||
context.Request.EnableBuffering();
|
||||
var requestBody = await ReadBodyAsync(context.Request.Body);
|
||||
|
||||
_logger.LogInformation(
|
||||
"HTTP {Method} {Path} Body: {Body}",
|
||||
context.Request.Method,
|
||||
context.Request.Path,
|
||||
requestBody
|
||||
);
|
||||
|
||||
// Capture response
|
||||
var originalBody = context.Response.Body;
|
||||
using var responseBody = new MemoryStream();
|
||||
context.Response.Body = responseBody;
|
||||
|
||||
await _next(context);
|
||||
|
||||
// Log response
|
||||
responseBody.Seek(0, SeekOrigin.Begin);
|
||||
var response = await new StreamReader(responseBody).ReadToEndAsync();
|
||||
|
||||
_logger.LogInformation(
|
||||
"HTTP {StatusCode} Response: {Response}",
|
||||
context.Response.StatusCode,
|
||||
response
|
||||
);
|
||||
|
||||
// Copy to original stream
|
||||
responseBody.Seek(0, SeekOrigin.Begin);
|
||||
await responseBody.CopyToAsync(originalBody);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Common Debugging Scenarios
|
||||
|
||||
### Find source of exception
|
||||
|
||||
```bash
|
||||
# Enable first-chance exceptions in VS Code
|
||||
# .vscode/launch.json
|
||||
"exceptionOptions": {
|
||||
"breakpoints": {
|
||||
"raised": true,
|
||||
"userUnhandled": false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Memory leak investigation
|
||||
|
||||
```bash
|
||||
# 1. Take baseline dump
|
||||
dotnet-dump collect -p <pid> -o baseline.dump
|
||||
|
||||
# 2. Exercise application
|
||||
# ... use the app normally ...
|
||||
|
||||
# 3. Take second dump
|
||||
dotnet-dump collect -p <pid> -o leaked.dump
|
||||
|
||||
# 4. Compare
|
||||
dotnet-dump analyze leaked.dump
|
||||
> dumpheap -stat
|
||||
# Look for growing object counts
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Slow request diagnosis
|
||||
|
||||
**Add request timing:**
|
||||
```csharp
|
||||
app.Use(async (context, next) =>
|
||||
{
|
||||
var sw = Stopwatch.StartNew();
|
||||
await next();
|
||||
sw.Stop();
|
||||
|
||||
if (sw.ElapsedMilliseconds > 1000)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"Slow request: {Method} {Path} took {Elapsed}ms",
|
||||
context.Request.Method,
|
||||
context.Request.Path,
|
||||
sw.ElapsedMilliseconds
|
||||
);
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Performance Profiling
|
||||
|
||||
### BenchmarkDotNet
|
||||
|
||||
```csharp
|
||||
using BenchmarkDotNet.Attributes;
|
||||
using BenchmarkDotNet.Running;
|
||||
|
||||
[MemoryDiagnoser]
|
||||
public class OrderBenchmarks
|
||||
{
|
||||
private Order _order;
|
||||
|
||||
[GlobalSetup]
|
||||
public void Setup()
|
||||
{
|
||||
_order = new Order(/* ... */);
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
public void AddItem()
|
||||
{
|
||||
_order.AddItem(Guid.NewGuid(), 1, 10.0m);
|
||||
}
|
||||
}
|
||||
|
||||
// Run benchmarks
|
||||
BenchmarkRunner.Run<OrderBenchmarks>();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Quick Reference
|
||||
|
||||
### VS Code Shortcuts
|
||||
|
||||
| Action | Shortcut |
|
||||
|--------|----------|
|
||||
| Start debugging | F5 |
|
||||
| Step over | F10 |
|
||||
| Step into | F11 |
|
||||
| Step out | Shift+F11 |
|
||||
| Continue | F5 |
|
||||
| Toggle breakpoint | F9 |
|
||||
| Add watch | Right-click → Add to Watch |
|
||||
|
||||
### Useful Commands
|
||||
|
||||
```bash
|
||||
# View running processes
|
||||
ps aux | grep dotnet
|
||||
|
||||
# Find process by port
|
||||
lsof -i :8080
|
||||
|
||||
# Kill process
|
||||
kill -9 <pid>
|
||||
|
||||
# View environment variables
|
||||
docker exec my-service env
|
||||
|
||||
# Execute command in container
|
||||
docker exec -it my-service sh
|
||||
|
||||
# Follow logs with filter
|
||||
docker logs -f my-service | grep ERROR
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Related Resources
|
||||
|
||||
- [Build Errors Catalog](build-errors.md)
|
||||
- [Test Failures Guide](test-failures.md)
|
||||
- Main Skill: [development-lifecycle](../SKILL.md)
|
||||
- [.NET Debugging Documentation](https://learn.microsoft.com/en-us/dotnet/core/diagnostics/)
|
||||
354
.agent/skills/development-lifecycle/references/test-failures.md
Normal file
354
.agent/skills/development-lifecycle/references/test-failures.md
Normal file
@@ -0,0 +1,354 @@
|
||||
# Test Failures Guide
|
||||
|
||||
Common test failure patterns and how to fix them in .NET microservices.
|
||||
|
||||
## Mock Setup Issues
|
||||
|
||||
### NullReferenceException in tests
|
||||
|
||||
**Symptom:**
|
||||
```
|
||||
System.NullReferenceException: Object reference not set to an instance of an object
|
||||
```
|
||||
|
||||
**Common Cause:** Mock not properly configured
|
||||
|
||||
**Solution:**
|
||||
```csharp
|
||||
// ❌ BAD: Mock returns null by default
|
||||
var repository = Substitute.For<IOrderRepository>();
|
||||
var order = await repository.GetByIdAsync(orderId); // returns null!
|
||||
order.Status; // NullReferenceException
|
||||
|
||||
// ✅ GOOD: Configure mock to return data
|
||||
var repository = Substitute.For<IOrderRepository>();
|
||||
var expectedOrder = new Order(/* ... */);
|
||||
repository.GetByIdAsync(orderId).Returns(expectedOrder);
|
||||
|
||||
var order = await repository.GetByIdAsync(orderId);
|
||||
Assert.NotNull(order);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### UnitOfWork mock setup
|
||||
|
||||
**Problem:** `SaveChangesAsync` not properly mocked
|
||||
|
||||
**Solution:**
|
||||
```csharp
|
||||
// ✅ Complete mock setup
|
||||
var repository = Substitute.For<IOrderRepository>();
|
||||
var unitOfWork = Substitute.For<IUnitOfWork>();
|
||||
|
||||
// Important: Link repository to unitOfWork
|
||||
repository.UnitOfWork.Returns(unitOfWork);
|
||||
|
||||
// Configure SaveChangesAsync behavior
|
||||
unitOfWork.SaveChangesAsync(Arg.Any<CancellationToken>())
|
||||
.Returns(Task.FromResult(1));
|
||||
|
||||
// Now handler can use it
|
||||
var handler = new CreateOrderHandler(repository);
|
||||
await handler.Handle(command, CancellationToken.None);
|
||||
|
||||
// Verify SaveChangesAsync was called
|
||||
await unitOfWork.Received(1).SaveChangesAsync(Arg.Any<CancellationToken>());
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Async/Await Issues
|
||||
|
||||
### Test hangs or times out
|
||||
|
||||
**Common Causes:**
|
||||
1. Missing `await`
|
||||
2. Deadlock from `.Result` or `.Wait()`
|
||||
3. Infinite loop
|
||||
|
||||
**Solutions:**
|
||||
```csharp
|
||||
// ❌ BAD: Forgetting await
|
||||
[Fact]
|
||||
public async Task TestMethod()
|
||||
{
|
||||
var result = handler.Handle(command, ct); // Missing await!
|
||||
Assert.NotNull(result); // Wrong - this is Task, not result
|
||||
}
|
||||
|
||||
// ✅ GOOD: Proper await
|
||||
[Fact]
|
||||
public async Task TestMethod()
|
||||
{
|
||||
var result = await handler.Handle(command, ct);
|
||||
Assert.NotNull(result);
|
||||
}
|
||||
|
||||
// ❌ BAD: Blocking async code
|
||||
var result = repository.GetByIdAsync(id).Result; // Deadlock risk!
|
||||
|
||||
// ✅ GOOD: Await properly
|
||||
var result = await repository.GetByIdAsync(id);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Assertion Failures
|
||||
|
||||
### Expected vs Actual mismatch
|
||||
|
||||
**Symptom:**
|
||||
```
|
||||
Assert.Equal() Failure
|
||||
Expected: 5
|
||||
Actual: 0
|
||||
```
|
||||
|
||||
**Debugging:**
|
||||
```csharp
|
||||
// ❌ Unclear what went wrong
|
||||
Assert.Equal(5, order.Items.Count);
|
||||
|
||||
// ✅ Better: Add message
|
||||
Assert.Equal(5, order.Items.Count,
|
||||
$"Expected 5 items but got {order.Items.Count}");
|
||||
|
||||
// ✅ Best: Use specific assertions
|
||||
Assert.NotEmpty(order.Items);
|
||||
Assert.Equal(5, order.Items.Count);
|
||||
Assert.All(order.Items, item => Assert.NotNull(item.ProductId));
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Collection comparison failures
|
||||
|
||||
**Problem:** Comparing collections incorrectly
|
||||
|
||||
**Solutions:**
|
||||
```csharp
|
||||
// ❌ BAD: Reference comparison
|
||||
var expected = new List<int> { 1, 2, 3 };
|
||||
var actual = service.GetNumbers();
|
||||
Assert.Equal(expected, actual); // May fail even if content same
|
||||
|
||||
// ✅ GOOD: Value comparison
|
||||
Assert.Equal(expected.Count, actual.Count);
|
||||
Assert.All(expected, item => Assert.Contains(item, actual));
|
||||
|
||||
// ✅ Or use FluentAssertions
|
||||
actual.Should().BeEquivalentTo(expected);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Database Test Issues
|
||||
|
||||
### Test fails due to database state
|
||||
|
||||
**Problem:** Tests interfere with each other
|
||||
|
||||
**Solutions:**
|
||||
```csharp
|
||||
// ✅ Use in-memory database per test
|
||||
public class OrderRepositoryTests : IDisposable
|
||||
{
|
||||
private readonly DbContextOptions<OrderContext> _options;
|
||||
private readonly OrderContext _context;
|
||||
|
||||
public OrderRepositoryTests()
|
||||
{
|
||||
_options = new DbContextOptionsBuilder<OrderContext>()
|
||||
.UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString())
|
||||
.Options;
|
||||
|
||||
_context = new OrderContext(_options);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_context.Database.EnsureDeleted();
|
||||
_context.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ Or use Testcontainers for real database
|
||||
public class OrderRepositoryIntegrationTests : IAsyncLifetime
|
||||
{
|
||||
private PostgreSqlContainer _container;
|
||||
|
||||
public async Task InitializeAsync()
|
||||
{
|
||||
_container = new PostgreSqlBuilder().Build();
|
||||
await _container.StartAsync();
|
||||
}
|
||||
|
||||
public async Task DisposeAsync()
|
||||
{
|
||||
await _container.DisposeAsync();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Integration Test Issues
|
||||
|
||||
### TestServer authentication failures
|
||||
|
||||
**Problem:** Requests return 401 Unauthorized
|
||||
|
||||
**Solutions:**
|
||||
```csharp
|
||||
// ✅ Setup test authentication
|
||||
public class CustomWebApplicationFactory : WebApplicationFactory<Program>
|
||||
{
|
||||
protected override void ConfigureWebHost(IWebHostBuilder builder)
|
||||
{
|
||||
builder.ConfigureServices(services =>
|
||||
{
|
||||
// Remove real auth
|
||||
services.RemoveAll<IAuthenticationService>();
|
||||
|
||||
// Add test auth
|
||||
services.AddAuthentication("Test")
|
||||
.AddScheme<AuthenticationSchemeOptions, TestAuthHandler>(
|
||||
"Test", options => { });
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Usage in test
|
||||
var client = _factory.CreateClient();
|
||||
client.DefaultRequestHeaders.Add("X-Test-User-Id", userId.ToString());
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Port already in use
|
||||
|
||||
**Error:**
|
||||
```
|
||||
Address already in use: bind
|
||||
```
|
||||
|
||||
**Solutions:**
|
||||
```csharp
|
||||
// ✅ Let TestServer choose random port
|
||||
var factory = new WebApplicationFactory<Program>()
|
||||
.WithWebHostBuilder(builder =>
|
||||
{
|
||||
builder.UseUrls(); // Empty = random port
|
||||
});
|
||||
|
||||
// ✅ Or use different port per test class
|
||||
builder.UseUrls("http://localhost:0"); // 0 = random port
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Flaky Tests
|
||||
|
||||
### Tests pass/fail intermittently
|
||||
|
||||
**Common Causes:**
|
||||
1. Race conditions
|
||||
2. Time-dependent logic
|
||||
3. Shared state
|
||||
4. External dependencies
|
||||
|
||||
**Solutions:**
|
||||
```csharp
|
||||
// ❌ BAD: Time-dependent test
|
||||
[Fact]
|
||||
public void TestCreatedDate()
|
||||
{
|
||||
var order = new Order();
|
||||
Assert.Equal(DateTime.UtcNow, order.CreatedAt); // Flaky!
|
||||
}
|
||||
|
||||
// ✅ GOOD: Test with tolerance
|
||||
[Fact]
|
||||
public void TestCreatedDate()
|
||||
{
|
||||
var before = DateTime.UtcNow;
|
||||
var order = new Order();
|
||||
var after = DateTime.UtcNow;
|
||||
|
||||
Assert.InRange(order.CreatedAt, before, after);
|
||||
}
|
||||
|
||||
// ✅ Or inject time provider
|
||||
public class Order
|
||||
{
|
||||
public DateTime CreatedAt { get; }
|
||||
|
||||
public Order(ITimeProvider timeProvider)
|
||||
{
|
||||
CreatedAt = timeProvider.UtcNow;
|
||||
}
|
||||
}
|
||||
|
||||
// In test: mock time
|
||||
var timeProvider = Substitute.For<ITimeProvider>();
|
||||
timeProvider.UtcNow.Returns(new DateTime(2024, 1, 1));
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Test Coverage Issues
|
||||
|
||||
### Low coverage on critical code
|
||||
|
||||
**Problem:** Important logic not tested
|
||||
|
||||
**Solutions:**
|
||||
```bash
|
||||
# Generate coverage report
|
||||
dotnet test /p:CollectCoverage=true /p:CoverageReportFormat=cobertura
|
||||
|
||||
# View HTML report
|
||||
reportgenerator -reports:coverage.cobertura.xml -targetdir:coveragereport
|
||||
open coveragereport/index.html
|
||||
|
||||
# Or use coverlet
|
||||
dotnet add package coverlet.collector
|
||||
dotnet test --collect:"XPlat Code Coverage"
|
||||
```
|
||||
|
||||
**Focus on:**
|
||||
- Domain logic (business rules)
|
||||
- Command/Query handlers
|
||||
- Critical paths (checkout, payment, etc.)
|
||||
|
||||
---
|
||||
|
||||
## Quick Troubleshooting Checklist
|
||||
|
||||
When tests fail:
|
||||
|
||||
- [ ] Check mock setup (returns correct values?)
|
||||
- [ ] Verify async/await (no missing `await`?)
|
||||
- [ ] Check assertions (expected vs actual clear?)
|
||||
- [ ] Isolate test (run alone, not in suite)
|
||||
- [ ] Check test output logs
|
||||
- [ ] Add debug logging
|
||||
- [ ] Use debugger with breakpoints
|
||||
|
||||
For integration tests:
|
||||
|
||||
- [ ] Check TestServer configuration
|
||||
- [ ] Verify authentication setup
|
||||
- [ ] Check database state (clean between tests?)
|
||||
- [ ] Review test ordering (independent?)
|
||||
- [ ] Check external dependencies (mocked?)
|
||||
|
||||
---
|
||||
|
||||
## Related Resources
|
||||
|
||||
- [Build Errors Catalog](build-errors.md)
|
||||
- [Debugging Guide](debugging-guide.md)
|
||||
- [dotnet-senior-tester skill](../../dotnet-senior-tester/SKILL.md)
|
||||
- Main Skill: [development-lifecycle](../SKILL.md)
|
||||
120
.agent/skills/development-lifecycle/scripts/check-env.sh
Executable file
120
.agent/skills/development-lifecycle/scripts/check-env.sh
Executable file
@@ -0,0 +1,120 @@
|
||||
#!/bin/bash
|
||||
# EN: Check development environment for GoodGo microservices
|
||||
# VI: Kiểm tra môi trường phát triển cho GoodGo microservices
|
||||
|
||||
set -e
|
||||
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo " Development Environment Check"
|
||||
echo " Kiểm tra Môi Trường Phát Triển"
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo ""
|
||||
|
||||
# Colors
|
||||
GREEN='\033[0;32m'
|
||||
RED='\033[0;31m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
check_command() {
|
||||
local cmd=$1
|
||||
local required_version=$2
|
||||
|
||||
if command -v $cmd &> /dev/null; then
|
||||
local version=$($cmd --version 2>&1 | head -n 1)
|
||||
echo -e "${GREEN}✓${NC} $cmd: $version"
|
||||
return 0
|
||||
else
|
||||
echo -e "${RED}✗${NC} $cmd: Not installed"
|
||||
if [ ! -z "$required_version" ]; then
|
||||
echo -e " ${YELLOW}Required: $required_version${NC}"
|
||||
fi
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
check_dotnet() {
|
||||
if command -v dotnet &> /dev/null; then
|
||||
local version=$(dotnet --version)
|
||||
echo -e "${GREEN}✓${NC} .NET SDK: $version"
|
||||
|
||||
# Check if version >= 8.0
|
||||
local major=$(echo $version | cut -d. -f1)
|
||||
if [ "$major" -lt 8 ]; then
|
||||
echo -e " ${YELLOW}Warning: .NET 8.0+ recommended${NC}"
|
||||
fi
|
||||
return 0
|
||||
else
|
||||
echo -e "${RED}✗${NC} .NET SDK: Not installed"
|
||||
echo -e " ${YELLOW}Required: .NET 8.0+${NC}"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
check_docker() {
|
||||
if command -v docker &> /dev/null; then
|
||||
local version=$(docker --version | cut -d' ' -f3 | tr -d ',')
|
||||
echo -e "${GREEN}✓${NC} Docker: $version"
|
||||
|
||||
# Check if Docker daemon is running
|
||||
if docker ps &> /dev/null; then
|
||||
echo -e " ${GREEN}Docker daemon is running${NC}"
|
||||
else
|
||||
echo -e " ${RED}Docker daemon is not running${NC}"
|
||||
return 1
|
||||
fi
|
||||
return 0
|
||||
else
|
||||
echo -e "${RED}✗${NC} Docker: Not installed"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
check_docker_compose() {
|
||||
if command -v docker-compose &> /dev/null; then
|
||||
local version=$(docker-compose --version | cut -d' ' -f4 | tr -d ',')
|
||||
echo -e "${GREEN}✓${NC} docker-compose: $version"
|
||||
return 0
|
||||
elif docker compose version &> /dev/null; then
|
||||
local version=$(docker compose version | cut -d' ' -f4 | tr -d 'v')
|
||||
echo -e "${GREEN}✓${NC} docker compose: $version"
|
||||
return 0
|
||||
else
|
||||
echo -e "${RED}✗${NC} docker-compose: Not installed"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Main checks
|
||||
echo "Core Tools:"
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
ERRORS=0
|
||||
|
||||
check_dotnet || ((ERRORS++))
|
||||
check_docker || ((ERRORS++))
|
||||
check_docker_compose || ((ERRORS++))
|
||||
|
||||
echo ""
|
||||
echo "Optional Tools:"
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
|
||||
check_command "git" || true
|
||||
check_command "curl" || true
|
||||
check_command "jq" || true
|
||||
|
||||
echo ""
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
|
||||
if [ $ERRORS -eq 0 ]; then
|
||||
echo -e "${GREEN}✓ All core tools are installed!${NC}"
|
||||
echo ""
|
||||
exit 0
|
||||
else
|
||||
echo -e "${RED}✗ $ERRORS core tool(s) missing or not working${NC}"
|
||||
echo ""
|
||||
echo "Installation guides:"
|
||||
echo " .NET SDK: https://dotnet.microsoft.com/download"
|
||||
echo " Docker: https://docs.docker.com/get-docker/"
|
||||
echo ""
|
||||
exit 1
|
||||
fi
|
||||
85
.agent/skills/development-lifecycle/scripts/debug-build.sh
Executable file
85
.agent/skills/development-lifecycle/scripts/debug-build.sh
Executable file
@@ -0,0 +1,85 @@
|
||||
#!/bin/bash
|
||||
# EN: Debug build errors with verbose output
|
||||
# VI: Debug lỗi build với output chi tiết
|
||||
|
||||
set -e
|
||||
|
||||
# Colors
|
||||
GREEN='\033[0;32m'
|
||||
RED='\033[0;31m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
if [ $# -eq 0 ]; then
|
||||
echo -e "${RED}Error: Service name required${NC}"
|
||||
echo "Usage: $0 <service-name>"
|
||||
echo "Example: $0 my-service-net"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
SERVICE_NAME=$1
|
||||
SERVICE_DIR="services/${SERVICE_NAME}"
|
||||
LOG_FILE="build-debug-$(date +%Y%m%d-%H%M%S).log"
|
||||
|
||||
echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
||||
echo -e "${BLUE} Debug Build: ${SERVICE_NAME}${NC}"
|
||||
echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
||||
echo ""
|
||||
|
||||
# Check if service exists
|
||||
if [ ! -d "$SERVICE_DIR" ]; then
|
||||
echo -e "${RED}✗ Service not found: $SERVICE_DIR${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cd "$SERVICE_DIR"
|
||||
|
||||
# Find solution file
|
||||
SOLUTION_FILE=$(find . -maxdepth 1 -name "*.slnx" -o -name "*.sln" | head -n 1)
|
||||
|
||||
if [ -z "$SOLUTION_FILE" ]; then
|
||||
echo -e "${RED}✗ No solution file found${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo -e "${YELLOW}Step 1: Cleaning previous build artifacts...${NC}"
|
||||
dotnet clean "$SOLUTION_FILE"
|
||||
|
||||
echo ""
|
||||
echo -e "${YELLOW}Step 2: Restoring packages with detailed output...${NC}"
|
||||
dotnet restore "$SOLUTION_FILE" -v detailed 2>&1 | tee "$LOG_FILE"
|
||||
|
||||
echo ""
|
||||
echo -e "${YELLOW}Step 3: Building with detailed verbosity...${NC}"
|
||||
echo -e "${YELLOW}(This will take longer but show all information)${NC}"
|
||||
echo ""
|
||||
|
||||
# Build with detailed output
|
||||
dotnet build "$SOLUTION_FILE" --no-restore -v detailed 2>&1 | tee -a "$LOG_FILE"
|
||||
|
||||
BUILD_STATUS=$?
|
||||
|
||||
echo ""
|
||||
echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
||||
|
||||
if [ $BUILD_STATUS -eq 0 ]; then
|
||||
echo -e "${GREEN}✓ Build completed successfully!${NC}"
|
||||
echo ""
|
||||
echo "Log saved to: $LOG_FILE"
|
||||
else
|
||||
echo -e "${RED}✗ Build failed${NC}"
|
||||
echo ""
|
||||
echo "Full debug log saved to: $LOG_FILE"
|
||||
echo ""
|
||||
echo -e "${YELLOW}Common issues to check:${NC}"
|
||||
echo " 1. Missing package references (dotnet add package)"
|
||||
echo " 2. Using statements (add 'using' directive)"
|
||||
echo " 3. NuGet package version conflicts"
|
||||
echo " 4. Target framework mismatch"
|
||||
echo ""
|
||||
echo "Grep for errors:"
|
||||
echo " cat $LOG_FILE | grep -i error"
|
||||
echo " cat $LOG_FILE | grep -i CS[0-9]"
|
||||
exit 1
|
||||
fi
|
||||
90
.agent/skills/development-lifecycle/scripts/health-check.sh
Executable file
90
.agent/skills/development-lifecycle/scripts/health-check.sh
Executable file
@@ -0,0 +1,90 @@
|
||||
#!/bin/bash
|
||||
# EN: Check health of all running services
|
||||
# VI: Kiểm tra health của tất cả services đang chạy
|
||||
|
||||
set -e
|
||||
|
||||
# Colors
|
||||
GREEN='\033[0;32m'
|
||||
RED='\033[0;31m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
||||
echo -e "${BLUE} Service Health Check${NC}"
|
||||
echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
||||
echo ""
|
||||
|
||||
# Default base URL
|
||||
BASE_URL="${BASE_URL:-http://localhost}"
|
||||
TIMEOUT=5
|
||||
|
||||
check_health() {
|
||||
local service=$1
|
||||
local path=$2
|
||||
local url="${BASE_URL}${path}"
|
||||
|
||||
echo -n "Checking $service... "
|
||||
|
||||
if response=$(curl -s -o /dev/null -w "%{http_code}" --max-time $TIMEOUT "$url" 2>/dev/null); then
|
||||
if [ "$response" = "200" ]; then
|
||||
echo -e "${GREEN}✓ Healthy (200)${NC}"
|
||||
return 0
|
||||
else
|
||||
echo -e "${YELLOW}⚠ Response: $response${NC}"
|
||||
return 1
|
||||
fi
|
||||
else
|
||||
echo -e "${RED}✗ Not reachable${NC}"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Track results
|
||||
TOTAL=0
|
||||
HEALTHY=0
|
||||
UNHEALTHY=0
|
||||
|
||||
# Service list (customize based on your services)
|
||||
SERVICES=(
|
||||
"IAM Service:/api/v1/iam/health"
|
||||
"Storage Service:/api/v1/storage/health"
|
||||
"Catalog Service:/api/v1/catalog/health"
|
||||
"Order Service:/api/v1/order/health"
|
||||
"Inventory Service:/api/v1/inventory/health"
|
||||
"F&B Engine:/api/v1/fnb-engine/health"
|
||||
"Booking Service:/api/v1/booking/health"
|
||||
)
|
||||
|
||||
for SERVICE_INFO in "${SERVICES[@]}"; do
|
||||
IFS=':' read -r NAME PATH <<< "$SERVICE_INFO"
|
||||
((TOTAL++))
|
||||
|
||||
if check_health "$NAME" "$PATH"; then
|
||||
((HEALTHY++))
|
||||
else
|
||||
((UNHEALTHY++))
|
||||
fi
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
||||
echo "Summary:"
|
||||
echo " Total services: $TOTAL"
|
||||
echo -e " Healthy: ${GREEN}$HEALTHY${NC}"
|
||||
echo -e " Unhealthy: ${RED}$UNHEALTHY${NC}"
|
||||
echo ""
|
||||
|
||||
if [ $UNHEALTHY -eq 0 ]; then
|
||||
echo -e "${GREEN}✓ All services are healthy!${NC}"
|
||||
exit 0
|
||||
else
|
||||
echo -e "${YELLOW}⚠ Some services are not healthy${NC}"
|
||||
echo ""
|
||||
echo "Troubleshooting:"
|
||||
echo " • Check if services are running: docker-compose ps"
|
||||
echo " • View logs: docker-compose logs -f <service-name>"
|
||||
echo " • Restart service: docker-compose restart <service-name>"
|
||||
exit 1
|
||||
fi
|
||||
58
.agent/skills/development-lifecycle/scripts/quick-build.sh
Executable file
58
.agent/skills/development-lifecycle/scripts/quick-build.sh
Executable file
@@ -0,0 +1,58 @@
|
||||
#!/bin/bash
|
||||
# EN: Quick build script for a single service
|
||||
# VI: Script build nhanh cho một service
|
||||
|
||||
set -e
|
||||
|
||||
# Colors
|
||||
GREEN='\033[0;32m'
|
||||
RED='\033[0;31m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
if [ $# -eq 0 ]; then
|
||||
echo -e "${RED}Error: Service name required${NC}"
|
||||
echo "Usage: $0 <service-name>"
|
||||
echo "Example: $0 my-service-net"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
SERVICE_NAME=$1
|
||||
SERVICE_DIR="services/${SERVICE_NAME}"
|
||||
|
||||
echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
||||
echo -e "${BLUE} Quick Build: ${SERVICE_NAME}${NC}"
|
||||
echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
||||
echo ""
|
||||
|
||||
# Check if service exists
|
||||
if [ ! -d "$SERVICE_DIR" ]; then
|
||||
echo -e "${RED}✗ Service not found: $SERVICE_DIR${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cd "$SERVICE_DIR"
|
||||
|
||||
# Find solution file
|
||||
SOLUTION_FILE=$(find . -maxdepth 1 -name "*.slnx" -o -name "*.sln" | head -n 1)
|
||||
|
||||
if [ -z "$SOLUTION_FILE" ]; then
|
||||
echo -e "${RED}✗ No solution file found${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo -e "${YELLOW}📦 Restoring packages...${NC}"
|
||||
dotnet restore "$SOLUTION_FILE"
|
||||
|
||||
echo ""
|
||||
echo -e "${YELLOW}🔨 Building solution...${NC}"
|
||||
dotnet build "$SOLUTION_FILE" --no-restore
|
||||
|
||||
echo ""
|
||||
echo -e "${GREEN}✓ Build completed successfully!${NC}"
|
||||
echo ""
|
||||
echo "Next steps:"
|
||||
echo " • Run tests: dotnet test"
|
||||
echo " • Run locally: dotnet run --project src/*/API/"
|
||||
echo " • Build Docker: docker-compose -f deployments/local/docker-compose.yml build ${SERVICE_NAME}"
|
||||
Reference in New Issue
Block a user