# 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
///
/// Handles requests for a specific route type.
///
///
/// Implementations must be thread-safe and support concurrent request handling.
///
public interface IRouteHandler
{
// 5. Interface members (properties, then methods)
///
/// Gets the handler type identifier.
///
string HandlerType { get; }
///
/// Determines if this handler can process the given route.
///
bool CanHandle(RouteConfiguration route);
///
/// Processes an incoming request.
///
Task HandleAsync(
RequestPayload request,
RouteConfiguration route,
CancellationToken cancellationToken = default);
}
```
### Error Handling Patterns
```csharp
// Pattern 1: Result types for expected failures
public readonly struct Result
{
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 Success(T value) => new(value, null);
public static Result Failure(Error error) => new(default, error);
public Result Map(Func map) =>
IsSuccess ? Result.Success(map(Value!)) : Result.Failure(Error!);
public async Task> MapAsync(Func> map) =>
IsSuccess ? Result.Success(await map(Value!)) : Result.Failure(Error!);
}
public record Error(string Code, string Message, Exception? Inner = null);
// Usage
public async Task> ValidateTokenAsync(string token)
{
try
{
var claims = await _validator.ValidateAsync(token);
return Result.Success(claims);
}
catch (SecurityTokenExpiredException ex)
{
return Result.Failure(new Error("TOKEN_EXPIRED", "JWT has expired", ex));
}
catch (SecurityTokenInvalidSignatureException ex)
{
return Result.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 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 WithTimeoutAsync(
Func> 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 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 _logger;
public JwtValidator(
IOptions options,
IKeyProvider keyProvider,
ILogger 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 configure)
{
services.Configure(configure);
// Core services
services.AddSingleton();
services.AddSingleton();
// Keyed services for handlers
services.AddKeyedSingleton("microservice");
services.AddKeyedSingleton("graphql");
services.AddKeyedSingleton("proxy");
// Factory for route handler resolution
services.AddSingleton(sp => new RouteHandlerFactory(
sp.GetServices().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();
services.AddScoped(sp => sp.GetRequiredService().User);
services.AddScoped(sp => sp.GetRequiredService().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 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 LogRequestComplete =
LoggerMessage.Define(
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 _keyProviderMock;
private readonly Mock> _loggerMock;
public JwtValidatorTests()
{
_keyProviderMock = new Mock();
_loggerMock = new Mock>();
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()))
.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
{
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();
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 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--
fix/router-
refactor/router-
test/router-
docs/router-
```
### Commit Messages
```
():
[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 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 _routes = new();
public void AddRoute(string key, Route route)
{
_routes[key] = route; // Not thread-safe!
}
// ✅ GOOD: Thread-safe collection
private readonly ConcurrentDictionary _routes = new();
public void AddRoute(string key, Route route)
{
_routes[key] = route; // Thread-safe
}
// ✅ GOOD: Immutable update
private ImmutableDictionary _routes =
ImmutableDictionary.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 _pool;
public async Task 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)