Add integration tests for migration categories and execution
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Concelier Attestation Tests / attestation-tests (push) Has been cancelled
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Concelier Attestation Tests / attestation-tests (push) Has been cancelled
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
- Implemented MigrationCategoryTests to validate migration categorization for startup, release, seed, and data migrations. - Added tests for edge cases, including null, empty, and whitespace migration names. - Created StartupMigrationHostTests to verify the behavior of the migration host with real PostgreSQL instances using Testcontainers. - Included tests for migration execution, schema creation, and handling of pending release migrations. - Added SQL migration files for testing: creating a test table, adding a column, a release migration, and seeding data.
This commit is contained in:
755
docs/router/28-Step.md
Normal file
755
docs/router/28-Step.md
Normal file
@@ -0,0 +1,755 @@
|
||||
# Step 28: Agent Process Guidelines
|
||||
|
||||
## Overview
|
||||
|
||||
This document provides comprehensive guidelines for AI agents (Claude, Copilot, etc.) implementing the Stella Router. It establishes conventions, patterns, and decision frameworks to ensure consistent, high-quality implementations across all phases.
|
||||
|
||||
## Goals
|
||||
|
||||
1. Define clear coding standards and patterns for Router implementation
|
||||
2. Establish decision frameworks for common scenarios
|
||||
3. Provide checklists for implementation quality
|
||||
4. Document testing requirements and coverage expectations
|
||||
5. Define commit and PR conventions
|
||||
|
||||
## Implementation Standards
|
||||
|
||||
### Code Organization
|
||||
|
||||
```
|
||||
src/Router/
|
||||
├── StellaOps.Router.Core/ # Core abstractions and contracts
|
||||
│ ├── Abstractions/ # Interfaces
|
||||
│ ├── Configuration/ # Config models
|
||||
│ ├── Extensions/ # Extension methods
|
||||
│ └── Primitives/ # Value types
|
||||
│
|
||||
├── StellaOps.Router.Gateway/ # Gateway implementation
|
||||
│ ├── Routing/ # Route matching
|
||||
│ ├── Handlers/ # Route handlers
|
||||
│ ├── Pipeline/ # Request pipeline
|
||||
│ └── Middleware/ # Gateway middleware
|
||||
│
|
||||
├── StellaOps.Router.Transport/ # Transport implementations
|
||||
│ ├── InMemory/ # In-process transport
|
||||
│ ├── Tcp/ # TCP transport
|
||||
│ └── Tls/ # TLS transport
|
||||
│
|
||||
├── StellaOps.Router.Microservice/ # Microservice SDK
|
||||
│ ├── Hosting/ # Host builder
|
||||
│ ├── Endpoints/ # Endpoint handling
|
||||
│ └── Context/ # Request context
|
||||
│
|
||||
├── StellaOps.Router.Security/ # Security components
|
||||
│ ├── Jwt/ # JWT validation
|
||||
│ ├── Claims/ # Claim hydration
|
||||
│ └── RateLimiting/ # Rate limiting
|
||||
│
|
||||
└── StellaOps.Router.Observability/ # Observability
|
||||
├── Logging/ # Structured logging
|
||||
├── Metrics/ # Prometheus metrics
|
||||
└── Tracing/ # OpenTelemetry tracing
|
||||
```
|
||||
|
||||
### Naming Conventions
|
||||
|
||||
| Element | Convention | Example |
|
||||
|---------|------------|---------|
|
||||
| Interfaces | `I` prefix, noun/adjective | `IRouteHandler`, `IConnectable` |
|
||||
| Classes | PascalCase, noun | `JwtValidator`, `RouteTable` |
|
||||
| Async methods | `Async` suffix | `ValidateTokenAsync`, `SendAsync` |
|
||||
| Config classes | `Options` or `Configuration` suffix | `JwtValidationOptions` |
|
||||
| Event handlers | `On` prefix | `OnConnectionEstablished` |
|
||||
| Factory methods | `Create` prefix | `CreateHandler`, `CreateConnection` |
|
||||
| Boolean properties | `Is`/`Has`/`Can` prefix | `IsValid`, `HasExpired`, `CanRetry` |
|
||||
|
||||
### File Structure
|
||||
|
||||
```csharp
|
||||
// File: StellaOps.Router.Core/Abstractions/IRouteHandler.cs
|
||||
|
||||
// 1. License header (if required)
|
||||
// 2. Using statements (sorted: System, Microsoft, Third-party, Internal)
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Router.Core.Configuration;
|
||||
|
||||
// 3. Namespace (one per file, matches folder structure)
|
||||
namespace StellaOps.Router.Core.Abstractions;
|
||||
|
||||
// 4. XML documentation
|
||||
/// <summary>
|
||||
/// Handles requests for a specific route type.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Implementations must be thread-safe and support concurrent request handling.
|
||||
/// </remarks>
|
||||
public interface IRouteHandler
|
||||
{
|
||||
// 5. Interface members (properties, then methods)
|
||||
|
||||
/// <summary>
|
||||
/// Gets the handler type identifier.
|
||||
/// </summary>
|
||||
string HandlerType { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Determines if this handler can process the given route.
|
||||
/// </summary>
|
||||
bool CanHandle(RouteConfiguration route);
|
||||
|
||||
/// <summary>
|
||||
/// Processes an incoming request.
|
||||
/// </summary>
|
||||
Task<ResponsePayload> HandleAsync(
|
||||
RequestPayload request,
|
||||
RouteConfiguration route,
|
||||
CancellationToken cancellationToken = default);
|
||||
}
|
||||
```
|
||||
|
||||
### Error Handling Patterns
|
||||
|
||||
```csharp
|
||||
// Pattern 1: Result types for expected failures
|
||||
public readonly struct Result<T>
|
||||
{
|
||||
public T? Value { get; }
|
||||
public Error? Error { get; }
|
||||
public bool IsSuccess => Error == null;
|
||||
|
||||
private Result(T? value, Error? error)
|
||||
{
|
||||
Value = value;
|
||||
Error = error;
|
||||
}
|
||||
|
||||
public static Result<T> Success(T value) => new(value, null);
|
||||
public static Result<T> Failure(Error error) => new(default, error);
|
||||
|
||||
public Result<TNext> Map<TNext>(Func<T, TNext> map) =>
|
||||
IsSuccess ? Result<TNext>.Success(map(Value!)) : Result<TNext>.Failure(Error!);
|
||||
|
||||
public async Task<Result<TNext>> MapAsync<TNext>(Func<T, Task<TNext>> map) =>
|
||||
IsSuccess ? Result<TNext>.Success(await map(Value!)) : Result<TNext>.Failure(Error!);
|
||||
}
|
||||
|
||||
public record Error(string Code, string Message, Exception? Inner = null);
|
||||
|
||||
// Usage
|
||||
public async Task<Result<JwtClaims>> ValidateTokenAsync(string token)
|
||||
{
|
||||
try
|
||||
{
|
||||
var claims = await _validator.ValidateAsync(token);
|
||||
return Result<JwtClaims>.Success(claims);
|
||||
}
|
||||
catch (SecurityTokenExpiredException ex)
|
||||
{
|
||||
return Result<JwtClaims>.Failure(new Error("TOKEN_EXPIRED", "JWT has expired", ex));
|
||||
}
|
||||
catch (SecurityTokenInvalidSignatureException ex)
|
||||
{
|
||||
return Result<JwtClaims>.Failure(new Error("INVALID_SIGNATURE", "JWT signature invalid", ex));
|
||||
}
|
||||
}
|
||||
|
||||
// Pattern 2: Exceptions for unexpected failures
|
||||
public class RouterException : Exception
|
||||
{
|
||||
public string ErrorCode { get; }
|
||||
public int StatusCode { get; }
|
||||
|
||||
public RouterException(string errorCode, string message, int statusCode = 500)
|
||||
: base(message)
|
||||
{
|
||||
ErrorCode = errorCode;
|
||||
StatusCode = statusCode;
|
||||
}
|
||||
}
|
||||
|
||||
public class ConfigurationException : RouterException
|
||||
{
|
||||
public ConfigurationException(string message)
|
||||
: base("CONFIG_ERROR", message, 500) { }
|
||||
}
|
||||
|
||||
public class TransportException : RouterException
|
||||
{
|
||||
public TransportException(string message, Exception? inner = null)
|
||||
: base("TRANSPORT_ERROR", message, 503) { }
|
||||
}
|
||||
```
|
||||
|
||||
### Async Patterns
|
||||
|
||||
```csharp
|
||||
// Pattern 1: CancellationToken propagation
|
||||
public async Task<ResponsePayload> HandleAsync(
|
||||
RequestPayload request,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
// Always check at start of long operations
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
// Propagate to all async calls
|
||||
var validated = await _validator.ValidateAsync(request, cancellationToken);
|
||||
var enriched = await _enricher.EnrichAsync(validated, cancellationToken);
|
||||
var response = await _handler.ProcessAsync(enriched, cancellationToken);
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
// Pattern 2: Timeout handling
|
||||
public async Task<T> WithTimeoutAsync<T>(
|
||||
Func<CancellationToken, Task<T>> operation,
|
||||
TimeSpan timeout,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
||||
cts.CancelAfter(timeout);
|
||||
|
||||
try
|
||||
{
|
||||
return await operation(cts.Token);
|
||||
}
|
||||
catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
throw new TimeoutException($"Operation timed out after {timeout}");
|
||||
}
|
||||
}
|
||||
|
||||
// Pattern 3: Fire-and-forget with logging
|
||||
public void FireAndForget(Func<Task> operation, ILogger logger, string operationName)
|
||||
{
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
await operation();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Fire-and-forget operation {Operation} failed", operationName);
|
||||
}
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### Dependency Injection Patterns
|
||||
|
||||
```csharp
|
||||
// Pattern 1: Constructor injection with validation
|
||||
public class JwtValidator : IJwtValidator
|
||||
{
|
||||
private readonly JwtValidationOptions _options;
|
||||
private readonly IKeyProvider _keyProvider;
|
||||
private readonly ILogger<JwtValidator> _logger;
|
||||
|
||||
public JwtValidator(
|
||||
IOptions<JwtValidationOptions> options,
|
||||
IKeyProvider keyProvider,
|
||||
ILogger<JwtValidator> logger)
|
||||
{
|
||||
_options = options?.Value ?? throw new ArgumentNullException(nameof(options));
|
||||
_keyProvider = keyProvider ?? throw new ArgumentNullException(nameof(keyProvider));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
|
||||
ValidateOptions(_options);
|
||||
}
|
||||
|
||||
private static void ValidateOptions(JwtValidationOptions options)
|
||||
{
|
||||
if (string.IsNullOrEmpty(options.Issuer))
|
||||
throw new ConfigurationException("JWT issuer is required");
|
||||
if (options.ClockSkew < TimeSpan.Zero)
|
||||
throw new ConfigurationException("Clock skew cannot be negative");
|
||||
}
|
||||
}
|
||||
|
||||
// Pattern 2: Factory registration for complex objects
|
||||
public static class ServiceCollectionExtensions
|
||||
{
|
||||
public static IServiceCollection AddStellaRouter(
|
||||
this IServiceCollection services,
|
||||
Action<RouterOptions> configure)
|
||||
{
|
||||
services.Configure(configure);
|
||||
|
||||
// Core services
|
||||
services.AddSingleton<IRouteTable, RouteTable>();
|
||||
services.AddSingleton<IRequestPipeline, RequestPipeline>();
|
||||
|
||||
// Keyed services for handlers
|
||||
services.AddKeyedSingleton<IRouteHandler, MicroserviceHandler>("microservice");
|
||||
services.AddKeyedSingleton<IRouteHandler, GraphQLHandler>("graphql");
|
||||
services.AddKeyedSingleton<IRouteHandler, ReverseProxyHandler>("proxy");
|
||||
|
||||
// Factory for route handler resolution
|
||||
services.AddSingleton<IRouteHandlerFactory>(sp => new RouteHandlerFactory(
|
||||
sp.GetServices<IRouteHandler>().ToDictionary(h => h.HandlerType)));
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
|
||||
// Pattern 3: Scoped services for request context
|
||||
public static class RequestScopeExtensions
|
||||
{
|
||||
public static IServiceCollection AddRequestScope(this IServiceCollection services)
|
||||
{
|
||||
services.AddScoped<IRequestContext, RequestContext>();
|
||||
services.AddScoped(sp => sp.GetRequiredService<IRequestContext>().User);
|
||||
services.AddScoped(sp => sp.GetRequiredService<IRequestContext>().CorrelationId);
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Decision Framework
|
||||
|
||||
### When to Create New Types vs. Reuse
|
||||
|
||||
| Scenario | Decision | Rationale |
|
||||
|----------|----------|-----------|
|
||||
| Similar data, different context | Create new type | Type safety, clear intent |
|
||||
| Same data, same context | Reuse type | DRY, reduce cognitive load |
|
||||
| Third-party type | Create wrapper | Abstraction, testability |
|
||||
| Config vs. runtime | Separate types | Immutability guarantees |
|
||||
|
||||
```csharp
|
||||
// Example: Separate types for config vs runtime
|
||||
public record RouteConfiguration(
|
||||
string Path,
|
||||
string Method,
|
||||
string HandlerType,
|
||||
Dictionary<string, string> Metadata);
|
||||
|
||||
public class CompiledRoute
|
||||
{
|
||||
public RouteConfiguration Config { get; }
|
||||
public Regex PathPattern { get; }
|
||||
public IRouteHandler Handler { get; }
|
||||
// Runtime-computed fields
|
||||
}
|
||||
```
|
||||
|
||||
### When to Use Interfaces vs. Abstract Classes
|
||||
|
||||
| Use Interface | Use Abstract Class |
|
||||
|---------------|-------------------|
|
||||
| Multiple inheritance needed | Shared implementation |
|
||||
| Contract-only definition | Template method pattern |
|
||||
| Third-party implementation | Internal hierarchy only |
|
||||
| Mocking/testing priority | Code reuse priority |
|
||||
|
||||
### Logging Level Guidelines
|
||||
|
||||
| Level | When to Use | Example |
|
||||
|-------|-------------|---------|
|
||||
| `Trace` | Internal flow details | `"Route matching attempt for {Path}"` |
|
||||
| `Debug` | Diagnostic information | `"Cache hit for key {Key}"` |
|
||||
| `Information` | Significant events | `"Request completed: {Method} {Path} → {Status}"` |
|
||||
| `Warning` | Recoverable issues | `"Rate limit approaching: {Current}/{Max}"` |
|
||||
| `Error` | Failures requiring attention | `"Failed to connect to Authority: {Error}"` |
|
||||
| `Critical` | System-wide failures | `"Configuration invalid, router cannot start"` |
|
||||
|
||||
```csharp
|
||||
// Structured logging patterns
|
||||
_logger.LogInformation(
|
||||
"Request processed: {Method} {Path} → {StatusCode} in {ElapsedMs}ms",
|
||||
request.Method,
|
||||
request.Path,
|
||||
response.StatusCode,
|
||||
stopwatch.ElapsedMilliseconds);
|
||||
|
||||
// Use LoggerMessage for high-performance paths
|
||||
private static readonly Action<ILogger, string, string, int, long, Exception?> LogRequestComplete =
|
||||
LoggerMessage.Define<string, string, int, long>(
|
||||
LogLevel.Information,
|
||||
new EventId(1001, "RequestComplete"),
|
||||
"Request processed: {Method} {Path} → {StatusCode} in {ElapsedMs}ms");
|
||||
|
||||
// Usage
|
||||
LogRequestComplete(_logger, method, path, statusCode, elapsed, null);
|
||||
```
|
||||
|
||||
## Implementation Checklists
|
||||
|
||||
### Before Starting a Component
|
||||
|
||||
- [ ] Read the step documentation thoroughly
|
||||
- [ ] Understand dependencies on previous steps
|
||||
- [ ] Review related existing code patterns
|
||||
- [ ] Identify configuration requirements
|
||||
- [ ] Plan test coverage strategy
|
||||
|
||||
### During Implementation
|
||||
|
||||
- [ ] Follow naming conventions
|
||||
- [ ] Add XML documentation to public APIs
|
||||
- [ ] Implement `IDisposable`/`IAsyncDisposable` where needed
|
||||
- [ ] Add structured logging at appropriate levels
|
||||
- [ ] Handle cancellation tokens throughout
|
||||
- [ ] Use result types for expected failures
|
||||
- [ ] Validate all configuration at startup
|
||||
|
||||
### Before Marking Complete
|
||||
|
||||
- [ ] All public types have XML documentation
|
||||
- [ ] Unit tests achieve >80% coverage
|
||||
- [ ] Integration tests cover happy path + error cases
|
||||
- [ ] No compiler warnings
|
||||
- [ ] Code passes all linting rules
|
||||
- [ ] Configuration is validated
|
||||
- [ ] README/documentation updated if needed
|
||||
|
||||
### Pull Request Checklist
|
||||
|
||||
- [ ] PR title follows convention: `feat(router): description`
|
||||
- [ ] Description explains what and why
|
||||
- [ ] All tests pass
|
||||
- [ ] No unrelated changes
|
||||
- [ ] Breaking changes documented
|
||||
- [ ] Reviewable size (<500 lines preferred)
|
||||
|
||||
## Testing Requirements
|
||||
|
||||
### Unit Test Coverage Targets
|
||||
|
||||
| Component Type | Target Coverage |
|
||||
|---------------|-----------------|
|
||||
| Core logic | 90% |
|
||||
| Handlers | 85% |
|
||||
| Middleware | 80% |
|
||||
| Configuration | 75% |
|
||||
| Extensions | 70% |
|
||||
|
||||
### Test Structure
|
||||
|
||||
```csharp
|
||||
// Test file naming: {ClassName}Tests.cs
|
||||
// Test method naming: {Method}_{Scenario}_{ExpectedResult}
|
||||
|
||||
public class JwtValidatorTests
|
||||
{
|
||||
private readonly JwtValidator _sut; // System Under Test
|
||||
private readonly Mock<IKeyProvider> _keyProviderMock;
|
||||
private readonly Mock<ILogger<JwtValidator>> _loggerMock;
|
||||
|
||||
public JwtValidatorTests()
|
||||
{
|
||||
_keyProviderMock = new Mock<IKeyProvider>();
|
||||
_loggerMock = new Mock<ILogger<JwtValidator>>();
|
||||
|
||||
var options = Options.Create(new JwtValidationOptions
|
||||
{
|
||||
Issuer = "https://auth.example.com",
|
||||
Audience = "stella-router"
|
||||
});
|
||||
|
||||
_sut = new JwtValidator(options, _keyProviderMock.Object, _loggerMock.Object);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ValidateAsync_ValidToken_ReturnsSuccessWithClaims()
|
||||
{
|
||||
// Arrange
|
||||
var token = GenerateValidToken();
|
||||
_keyProviderMock
|
||||
.Setup(x => x.GetSigningKeyAsync(It.IsAny<string>()))
|
||||
.ReturnsAsync(TestKeys.ValidKey);
|
||||
|
||||
// Act
|
||||
var result = await _sut.ValidateAsync(token);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.IsSuccess);
|
||||
Assert.NotNull(result.Value);
|
||||
Assert.Equal("test-user", result.Value.Subject);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ValidateAsync_ExpiredToken_ReturnsFailure()
|
||||
{
|
||||
// Arrange
|
||||
var token = GenerateExpiredToken();
|
||||
|
||||
// Act
|
||||
var result = await _sut.ValidateAsync(token);
|
||||
|
||||
// Assert
|
||||
Assert.False(result.IsSuccess);
|
||||
Assert.Equal("TOKEN_EXPIRED", result.Error!.Code);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(null)]
|
||||
[InlineData("")]
|
||||
[InlineData(" ")]
|
||||
public async Task ValidateAsync_NullOrEmptyToken_ReturnsFailure(string? token)
|
||||
{
|
||||
// Act
|
||||
var result = await _sut.ValidateAsync(token!);
|
||||
|
||||
// Assert
|
||||
Assert.False(result.IsSuccess);
|
||||
Assert.Equal("INVALID_TOKEN", result.Error!.Code);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Integration Test Patterns
|
||||
|
||||
```csharp
|
||||
public class RouterIntegrationTests : IClassFixture<RouterTestFixture>
|
||||
{
|
||||
private readonly RouterTestFixture _fixture;
|
||||
|
||||
public RouterIntegrationTests(RouterTestFixture fixture)
|
||||
{
|
||||
_fixture = fixture;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EndToEnd_AuthenticatedRequest_ReturnsSuccess()
|
||||
{
|
||||
// Arrange
|
||||
var client = _fixture.CreateAuthenticatedClient(claims: new()
|
||||
{
|
||||
["sub"] = "test-user",
|
||||
["role"] = "admin"
|
||||
});
|
||||
|
||||
// Act
|
||||
var response = await client.GetAsync("/api/users/123");
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
|
||||
var user = await response.Content.ReadFromJsonAsync<UserDto>();
|
||||
Assert.NotNull(user);
|
||||
Assert.Equal("123", user.Id);
|
||||
}
|
||||
}
|
||||
|
||||
// Test fixture
|
||||
public class RouterTestFixture : IAsyncLifetime
|
||||
{
|
||||
private IHost? _gatewayHost;
|
||||
private IHost? _microserviceHost;
|
||||
|
||||
public async Task InitializeAsync()
|
||||
{
|
||||
// Start microservice
|
||||
_microserviceHost = await CreateMicroserviceHost();
|
||||
await _microserviceHost.StartAsync();
|
||||
|
||||
// Start gateway
|
||||
_gatewayHost = await CreateGatewayHost();
|
||||
await _gatewayHost.StartAsync();
|
||||
}
|
||||
|
||||
public async Task DisposeAsync()
|
||||
{
|
||||
if (_gatewayHost != null)
|
||||
await _gatewayHost.StopAsync();
|
||||
if (_microserviceHost != null)
|
||||
await _microserviceHost.StopAsync();
|
||||
|
||||
_gatewayHost?.Dispose();
|
||||
_microserviceHost?.Dispose();
|
||||
}
|
||||
|
||||
public HttpClient CreateAuthenticatedClient(Dictionary<string, object> claims)
|
||||
{
|
||||
var token = GenerateTestToken(claims);
|
||||
var client = new HttpClient
|
||||
{
|
||||
BaseAddress = new Uri("http://localhost:5000")
|
||||
};
|
||||
client.DefaultRequestHeaders.Authorization =
|
||||
new AuthenticationHeaderValue("Bearer", token);
|
||||
return client;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Git and PR Conventions
|
||||
|
||||
### Branch Naming
|
||||
|
||||
```
|
||||
feat/router-<step>-<description>
|
||||
fix/router-<issue-number>
|
||||
refactor/router-<description>
|
||||
test/router-<description>
|
||||
docs/router-<description>
|
||||
```
|
||||
|
||||
### Commit Messages
|
||||
|
||||
```
|
||||
<type>(<scope>): <description>
|
||||
|
||||
[optional body]
|
||||
|
||||
[optional footer]
|
||||
```
|
||||
|
||||
Types: `feat`, `fix`, `refactor`, `test`, `docs`, `chore`
|
||||
|
||||
Examples:
|
||||
```
|
||||
feat(router): implement JWT validation with per-endpoint keys
|
||||
|
||||
- Add JwtValidator with configurable key sources
|
||||
- Support RS256 and ES256 algorithms
|
||||
- Add JWKS endpoint caching with TTL
|
||||
|
||||
Closes #123
|
||||
```
|
||||
|
||||
### PR Template
|
||||
|
||||
```markdown
|
||||
## Summary
|
||||
Brief description of what this PR does.
|
||||
|
||||
## Changes
|
||||
- Change 1
|
||||
- Change 2
|
||||
- Change 3
|
||||
|
||||
## Testing
|
||||
- [ ] Unit tests added/updated
|
||||
- [ ] Integration tests added/updated
|
||||
- [ ] Manual testing performed
|
||||
|
||||
## Checklist
|
||||
- [ ] Code follows project conventions
|
||||
- [ ] Documentation updated
|
||||
- [ ] No breaking changes (or documented if any)
|
||||
- [ ] All tests pass
|
||||
```
|
||||
|
||||
## Common Pitfalls to Avoid
|
||||
|
||||
### Performance
|
||||
|
||||
```csharp
|
||||
// ❌ BAD: Allocating in hot path
|
||||
public bool MatchRoute(string path)
|
||||
{
|
||||
var parts = path.Split('/'); // Allocation
|
||||
// ...
|
||||
}
|
||||
|
||||
// ✅ GOOD: Use Span for parsing
|
||||
public bool MatchRoute(ReadOnlySpan<char> path)
|
||||
{
|
||||
// Zero-allocation parsing
|
||||
foreach (var segment in path.Split('/'))
|
||||
{
|
||||
// ...
|
||||
}
|
||||
}
|
||||
|
||||
// ❌ BAD: Synchronous I/O blocking async context
|
||||
public async Task ProcessAsync()
|
||||
{
|
||||
var config = File.ReadAllText("config.json"); // Blocking!
|
||||
}
|
||||
|
||||
// ✅ GOOD: Async all the way
|
||||
public async Task ProcessAsync()
|
||||
{
|
||||
var config = await File.ReadAllTextAsync("config.json");
|
||||
}
|
||||
```
|
||||
|
||||
### Thread Safety
|
||||
|
||||
```csharp
|
||||
// ❌ BAD: Non-thread-safe collection
|
||||
private readonly Dictionary<string, Route> _routes = new();
|
||||
|
||||
public void AddRoute(string key, Route route)
|
||||
{
|
||||
_routes[key] = route; // Not thread-safe!
|
||||
}
|
||||
|
||||
// ✅ GOOD: Thread-safe collection
|
||||
private readonly ConcurrentDictionary<string, Route> _routes = new();
|
||||
|
||||
public void AddRoute(string key, Route route)
|
||||
{
|
||||
_routes[key] = route; // Thread-safe
|
||||
}
|
||||
|
||||
// ✅ GOOD: Immutable update
|
||||
private ImmutableDictionary<string, Route> _routes =
|
||||
ImmutableDictionary<string, Route>.Empty;
|
||||
|
||||
public void AddRoute(string key, Route route)
|
||||
{
|
||||
ImmutableInterlocked.AddOrUpdate(ref _routes, key, route, (_, _) => route);
|
||||
}
|
||||
```
|
||||
|
||||
### Resource Management
|
||||
|
||||
```csharp
|
||||
// ❌ BAD: Not disposing resources
|
||||
public async Task SendAsync(byte[] data)
|
||||
{
|
||||
var client = new TcpClient();
|
||||
await client.ConnectAsync("host", 9100);
|
||||
await client.GetStream().WriteAsync(data);
|
||||
// client never disposed!
|
||||
}
|
||||
|
||||
// ✅ GOOD: Proper disposal
|
||||
public async Task SendAsync(byte[] data)
|
||||
{
|
||||
using var client = new TcpClient();
|
||||
await client.ConnectAsync("host", 9100);
|
||||
await using var stream = client.GetStream();
|
||||
await stream.WriteAsync(data);
|
||||
}
|
||||
|
||||
// ✅ GOOD: Connection pooling
|
||||
public class ConnectionPool : IDisposable
|
||||
{
|
||||
private readonly Channel<TcpClient> _pool;
|
||||
|
||||
public async Task<TcpClient> RentAsync()
|
||||
{
|
||||
if (_pool.Reader.TryRead(out var client))
|
||||
return client;
|
||||
return await CreateNewConnectionAsync();
|
||||
}
|
||||
|
||||
public void Return(TcpClient client)
|
||||
{
|
||||
if (!_pool.Writer.TryWrite(client))
|
||||
client.Dispose();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Deliverables
|
||||
|
||||
| Artifact | Purpose |
|
||||
|----------|---------|
|
||||
| This document | Agent implementation guidelines |
|
||||
| Code templates | Consistent starting points |
|
||||
| Checklists | Quality gates |
|
||||
| Test patterns | Consistent testing approach |
|
||||
|
||||
## Next Step
|
||||
|
||||
[Step 29: Integration Testing & CI →](29-Step.md)
|
||||
Reference in New Issue
Block a user