# NFR Design Patterns — Unit 1: master-backend ## Pattern 1 — Resilience: Polly Pipeline via `AddResilienceHandler` **NFR**: Exponential backoff (3 attempts), per-attempt timeout (`HttpTimeoutSeconds`) **Pattern**: Single shared resilience pipeline registered on the `IHttpClientBuilder` for `SlaveApiClient`. All HTTP calls from `SlaveApiClient` pass through the pipeline automatically — no per-method boilerplate. **Pipeline composition** (outer → inner execution order): 1. **Retry** — `AddRetry` with exponential backoff; max 2 retries (3 total attempts); base delay 1s → 2s with jitter; retries on `HttpRequestException` and non-2xx responses 2. **Timeout** — `AddTimeout` with `TimeSpan.FromSeconds(MasterModuleOptions.HttpTimeoutSeconds)`; applied per attempt (not total) **Registration** (in `MasterModule.RegisterServices`): ```csharp services.AddHttpClient() .AddResilienceHandler("slave-resilience", (builder, ctx) => { var opts = ctx.ServiceProvider .GetRequiredService>().Value; builder.AddRetry(new HttpRetryStrategyOptions { MaxRetryAttempts = 2, Delay = TimeSpan.FromSeconds(1), BackoffType = DelayBackoffType.Exponential, UseJitter = true, ShouldHandle = args => ValueTask.FromResult( args.Outcome.Exception is HttpRequestException || (args.Outcome.Result?.IsSuccessStatusCode == false)) }); builder.AddTimeout(TimeSpan.FromSeconds(opts.HttpTimeoutSeconds)); }); ``` **Retry flow**: ```mermaid graph TD Call["SlaveApiClient HTTP call"] Attempt["Execute HTTP request\n(with per-attempt timeout)"] Success{"Response\nsuccessful?"} ReturnOk["Return result"] MaxReached{"Max attempts\n(3) reached?"} Backoff["Wait exponential delay\n1s or 2s plus jitter"] ReturnFail["Return false\nor throw on final attempt"] Call --> Attempt --> Success Success -->|"yes"| ReturnOk Success -->|"no"| MaxReached MaxReached -->|"yes"| ReturnFail MaxReached -->|"no"| Backoff --> Attempt classDef decision fill:#FFC107,stroke:#F57F17,stroke-width:2px,color:#000 classDef action fill:#63b3ed,stroke:#2b6cb0,stroke-width:1px,color:#000 classDef terminal fill:#9ae6b4,stroke:#2f855a,stroke-width:2px,color:#000 classDef fail fill:#FC8181,stroke:#C53030,stroke-width:2px,color:#000 class Success,MaxReached decision class Call,Attempt,Backoff action class ReturnOk terminal class ReturnFail fail ``` Text alternative: HTTP call enters pipeline; per-attempt timeout applies; on failure checks if max attempts reached; if not waits exponential delay and retries; after 3 total failures returns false. --- ## Pattern 2 — Security: `IApiKeyProtector` Wrapper **NFR**: ApiKey encrypted at rest; decrypted only for HTTP calls; never exposed in responses **Pattern**: Thin wrapper interface over ASP.NET Core Data Protection. Keeps `CmsInstanceService` independent of Data Protection internals and makes unit tests trivially simple (mock returns plain strings). **Interface**: ```csharp public interface IApiKeyProtector { string Protect(string plainApiKey); string Unprotect(string encryptedApiKey); } ``` **Implementation**: ```csharp public class ApiKeyProtector : IApiKeyProtector { private readonly IDataProtector _protector; public ApiKeyProtector(IDataProtectionProvider provider) { _protector = provider.CreateProtector("SlpModularCms.Master.ApiKey"); } public string Protect(string plainApiKey) => _protector.Protect(plainApiKey); public string Unprotect(string encrypted) => _protector.Unprotect(encrypted); } ``` **Registration** (in `MasterModule.RegisterServices`): ```csharp services.AddDataProtection(); services.AddSingleton(); ``` **Usage in tests**: ```csharp var protector = new Mock(); protector.Setup(p => p.Protect(It.IsAny())).Returns((string s) => $"enc:{s}"); protector.Setup(p => p.Unprotect(It.IsAny())).Returns((string s) => s.Replace("enc:", "")); ``` --- ## Pattern 3 — Constructor Aggregation: `MasterServiceDependencies` **Rationale**: `CmsInstanceService` requires 6 dependencies. Wrapping them in a record removes constructor noise and groups related parameters semantically. **Record definition**: ```csharp public record MasterServiceDependencies( ICmsInstanceRepository Repository, ISlaveApiClient SlaveClient, IApiKeyProtector ApiKeyProtector, IOptions Options, IHttpContextAccessor HttpContextAccessor, ILogger Logger ); ``` **Registration** (framework resolves all fields automatically): ```csharp services.AddScoped(); services.AddScoped(); ``` **`CmsInstanceService` constructor**: ```csharp public CmsInstanceService(MasterServiceDependencies deps) { _deps = deps; } ``` **Test construction** (explicit, no DI container needed): ```csharp var deps = new MasterServiceDependencies( mockRepo.Object, mockSlaveClient.Object, mockProtector.Object, Options.Create(new MasterModuleOptions()), mockHttpContextAccessor.Object, NullLogger.Instance ); var svc = new CmsInstanceService(deps); ``` --- ## Pattern 4 — Background Service Scope Isolation **NFR**: `ICmsInstanceService` is Scoped; `IntegrityCheckBackgroundService` is Singleton **Pattern**: Create and dispose a dedicated `IServiceScope` per tick. No singleton scope leakage. ```csharp protected override async Task ExecuteAsync(CancellationToken stoppingToken) { using var timer = new PeriodicTimer( TimeSpan.FromMinutes(_options.Value.IntegrityCheckIntervalMinutes)); while (await timer.WaitForNextTickAsync(stoppingToken)) { await using var scope = _scopeFactory.CreateAsyncScope(); try { var svc = scope.ServiceProvider .GetRequiredService(); await svc.VerifyIntegrityAsync(); } catch (Exception ex) { _logger.LogError(ex, "Unexpected error during integrity check tick"); } } } ``` --- ## Pattern 5 — Structured Logging **Pattern**: Log with structured fields; never log the raw `ApiKey` value. | Scenario | Level | Fields | |----------|-------|--------| | Status push failed | `Error` | `{InstanceId}`, `{SlaveUrl}`, `{Exception}` | | Integrity check: slave unreachable | `Warning` | `{InstanceId}`, `{SlaveUrl}` | | Integrity check: URL mismatch | `Warning` | `{InstanceId}`, `{SlaveUrl}`, `{ExpectedUrl}`, `{RegisteredUrl}` | | Integrity check: re-registration ok | `Information` | `{InstanceId}`, `{SlaveUrl}` | | Integrity check: re-registration failed | `Warning` | `{InstanceId}`, `{SlaveUrl}` | | ApiKey decryption failure | `Error` | `{InstanceId}` — do NOT log key material | | Tick started | `Debug` | `{ActiveInstanceCount}` | **Example**: ```csharp _logger.LogWarning( "Slave {InstanceId} at {SlaveUrl} is unreachable during integrity check", instance.Id, instance.Url); ```