# NFR Design Patterns — Unit 2: slave-availability-extension ## Pattern 1 — Security: `IMasterApiKeyProtector` Wrapper **NFR**: ApiKey encrypted at rest (Q1=C NFR Requirements); decrypted only for comparison; never logged **Pattern**: Thin wrapper interface over ASP.NET Core Data Protection. Mirrors Unit 1's `IApiKeyProtector` pattern but with a distinct purpose string to prevent cross-module decryption. **Interface**: ```csharp public interface IMasterApiKeyProtector { string Protect(string plainApiKey); string? Unprotect(string encryptedApiKey); // null on CryptographicException } ``` **Implementation**: ```csharp public class MasterApiKeyProtector : IMasterApiKeyProtector { private readonly IDataProtector _protector; public MasterApiKeyProtector(IDataProtectionProvider provider) { _protector = provider.CreateProtector("SlpModularCms.Availability.MasterApiKey"); } public string Protect(string plainApiKey) => _protector.Protect(plainApiKey); public string? Unprotect(string encryptedApiKey) { try { return _protector.Unprotect(encryptedApiKey); } catch (CryptographicException) { return null; } } } ``` **Registration** (in `AvailabilityModule.RegisterServices`): ```csharp services.AddDataProtection(); services.AddSingleton(); ``` **Usage in tests**: ```csharp var protector = Substitute.For(); protector.Protect(Arg.Any()).Returns(s => $"enc:{s.ArgAt(0)}"); protector.Unprotect(Arg.Any()).Returns(s => s.ArgAt(0).Replace("enc:", "")); ``` --- ## Pattern 2 — Performance: `volatile` Static Cache + Sync `GetMasterStatus()` **NFR**: Master gate check is synchronous; zero DB call per request (PERF-01/02) **Pattern**: Volatile static fields in `MasterAvailabilityService` updated only on status push. Exposed via a synchronous interface method so `AvailabilityMiddleware` can mock it in tests without accessing static state directly. **Static fields**: ```csharp private static volatile bool _masterIsAvailable = true; private static volatile string? _masterDisableMessage = null; ``` **Return type**: ```csharp public record MasterGateStatus(bool IsAvailable, string? DisableMessage); ``` **Interface method** (sync — no `Task`): ```csharp public interface IMasterAvailabilityService { Task RegisterAsync(string masterUrl, string apiKey); Task PushStatusAsync(string apiKey, bool isAvailable, string? disableMessage); Task GetRegisteredUrlAsync(string apiKey); MasterGateStatus GetMasterStatus(); // sync; reads volatile fields } ``` **Implementation**: ```csharp public MasterGateStatus GetMasterStatus() => new(_masterIsAvailable, _masterDisableMessage); ``` **Write point** (only in `PushStatusAsync`): ```csharp _masterIsAvailable = isAvailable; _masterDisableMessage = disableMessage; ``` **Why `volatile`**: `bool` and `string` reference assignments are already atomic in .NET. `volatile` adds the memory barrier to ensure other threads see the updated value without a `lock`. Consistent with `PersistentAvailabilityService`'s `_lastErrorTime` pattern. --- ## Pattern 3 — Constructor Aggregation: `MasterAvailabilityServiceDependencies` **NFR**: Consistent with Unit 1's `MasterServiceDependencies` pattern (Q2=B) **Record definition**: ```csharp [ExcludeFromCodeCoverage] public record MasterAvailabilityServiceDependencies( IMasterRegistrationRepository Repository, IMasterApiKeyProtector KeyProtector, ILogger Logger ); ``` **Registration**: ```csharp services.AddScoped(); services.AddScoped(); ``` **`MasterAvailabilityService` constructor**: ```csharp public MasterAvailabilityService(MasterAvailabilityServiceDependencies deps) { _deps = deps; } ``` **Test construction** (no DI container): ```csharp var deps = new MasterAvailabilityServiceDependencies( Substitute.For(), Substitute.For(), NullLogger.Instance ); var svc = new MasterAvailabilityService(deps); ``` --- ## Pattern 4 — Middleware Extension: `InvokeAsync` Third Parameter (Q1=A) **NFR**: Master gate is outer gate; evaluated before local gate; admin bypass applies to both **Pattern**: `AvailabilityMiddleware.InvokeAsync` receives `IMasterAvailabilityService` as a third DI-resolved parameter. ASP.NET Core middleware supports per-request parameter injection on `InvokeAsync`. **Extended `InvokeAsync` signature**: ```csharp public async Task InvokeAsync( HttpContext context, IAvailabilityService localSvc, IMasterAvailabilityService masterSvc) ``` **Evaluation order**: ```mermaid graph TD A[Request] --> B{Bypass path?} B -->|Yes| Z[Pass through] B -->|No| C{Admin JWT?} C -->|Yes| Z C -->|No| D{masterSvc.GetMasterStatus\n.IsAvailable?} D -->|true| E{localSvc\n.IsAvailableAsync?} D -->|false| F[503 with master\nDisableMessage] E -->|Available| Z E -->|Unavailable| G[503 with local\ndisable message] classDef decision fill:#FFC107,stroke:#F57F17,color:#000 classDef pass fill:#9ae6b4,stroke:#2f855a,color:#000 classDef fail fill:#FC8181,stroke:#C53030,color:#000 class B,C,D,E decision class Z pass class F,G fail ``` Text alternative: Request hits bypass path check, then admin JWT check. If both fail, master gate evaluates (sync). If master blocks: 503 with master message. If master passes: local gate evaluates (async). If local blocks: 503 with local message. Otherwise pass through. **Extended bypass prefix** (added to `_bypassPrefixes` array): ```csharp private static readonly string[] _bypassPrefixes = [ "/api/v1/Availability/status", "/api/v1/Auth/", "/api/v1/Setup/status", "/api/v1/master/" // NEW — master can always reach slave ]; ``` **503 response for master gate** (uses `ProblemDetails`): ```csharp var masterStatus = masterSvc.GetMasterStatus(); if (!masterStatus.IsAvailable) { context.Response.StatusCode = StatusCodes.Status503ServiceUnavailable; await context.Response.WriteAsJsonAsync(new ProblemDetails { Status = 503, Title = "Service Unavailable", Detail = masterStatus.DisableMessage }); return; } ``` **Test pattern** (mock `IMasterAvailabilityService`): ```csharp var masterSvc = Substitute.For(); masterSvc.GetMasterStatus().Returns(new MasterGateStatus(false, "Maintenance")); // invoke middleware — assert 503 ``` --- ## Pattern 5 — Structured Logging **Pattern**: Structured fields per event; never log key material. | Scenario | Level | Structured Fields | |----------|-------|------------------| | First master registration | `Information` | `{MasterUrl}` | | Re-registration (key match) | `Information` | `{MasterUrl}` | | API key mismatch | `Warning` | `{Endpoint}` — do NOT log key | | Missing header | `Warning` | `{Endpoint}` | | Status update received | `Information` | `{IsAvailable}`, `{DisableMessage}` | | Master gate blocked request | `Warning` | `{Path}`, `{DisableMessage}` | | Get-registered-url called | `Debug` | — | | Key decryption failure | `Error` | — (no key material) | **Example**: ```csharp _logger.LogWarning( "Master API key mismatch on {Endpoint} — returning 401", "POST /api/v1/master/status"); ```