# Tech Stack Decisions — Unit 1: master-backend ## HTTP Client & Resilience ### Decision: Typed HTTP Client via `IHttpClientFactory` + Polly | Aspect | Decision | Rationale | |--------|----------|-----------| | HTTP client abstraction | `ISlaveApiClient` / `SlaveApiClient` typed client | Testable via mock injection; clean contract boundary | | Client registration | `services.AddHttpClient()` | Framework manages `HttpClient` lifetime and connection pooling | | Retry policy | **Polly** `ResiliencePipelineBuilder` with `AddRetry` | Industry standard .NET resilience library; integrates natively with `IHttpClientFactory` via `AddResilienceHandler` | | Retry configuration | 3 total attempts; delays: 1s → 2s (exponential); jitter optional | Bounded worst-case latency; exponential reduces thundering herd on widespread slave outages | | Per-attempt timeout | `MasterModuleOptions.HttpTimeoutSeconds` (default 10s) | Configurable per-environment; keeps controller responses bounded | **NuGet package required**: `Microsoft.Extensions.Http.Resilience` (includes Polly integration) **Registration pattern**: ```csharp services.AddHttpClient() .AddResilienceHandler("slave-retry", builder => { builder.AddRetry(new HttpRetryStrategyOptions { MaxRetryAttempts = 2, Delay = TimeSpan.FromSeconds(1), BackoffType = DelayBackoffType.Exponential, UseJitter = true, }); builder.AddTimeout(TimeSpan.FromSeconds(options.HttpTimeoutSeconds)); }); ``` --- ## Data Protection ### Decision: Default ASP.NET Core Data Protection (file system) | Aspect | Decision | Rationale | |--------|----------|-----------| | Key storage | Default file system (no explicit `PersistKeysTo*` call) | Zero configuration; acceptable for single-instance; owner manages production key persistence | | Purpose string | `"SlpModularCms.Master.ApiKey"` | Scoped protection; prevents cross-purpose decryption | | Registration | `services.AddDataProtection()` (already called by framework if not explicitly called) | No extra setup needed beyond injecting `IDataProtectionProvider` | **Production note** (to be included in Unit 4 README update): For containerized or multi-instance deployments, configure a persistent key ring (e.g., `PersistKeysToDbContext`, `PersistKeysToAzureBlobStorage`). Without it, restarting the container causes all encrypted `ApiKey` values to become unreadable. --- ## Background Service ### Decision: .NET `BackgroundService` + `PeriodicTimer` | Aspect | Decision | Rationale | |--------|----------|-----------| | Base class | `BackgroundService` | Built-in .NET hosted service; lifecycle managed by `IHostApplicationLifetime` | | Timer mechanism | `PeriodicTimer` | Allocates less than `Timer`; await-friendly; cancellation-aware | | Scope management | `IServiceScopeFactory.CreateScope()` per tick | Required because `ICmsInstanceService` is Scoped; prevents captive dependency | | Exception handling | `try/catch` around entire tick body; log `Error` and continue | Prevents background service crash on unexpected errors | --- ## Logging ### Decision: `ILogger` structured logging | Scenario | Log Level | Structured Fields | |----------|-----------|------------------| | Status push failed (slave unreachable) | `Error` | `instanceId`, `slaveUrl`, `exception` | | Integrity check: slave unreachable | `Warning` | `instanceId`, `slaveUrl` | | Integrity check: URL mismatch detected | `Warning` | `instanceId`, `slaveUrl`, `expectedUrl`, `registeredUrl` | | Integrity check: re-registration succeeded | `Information` | `instanceId`, `slaveUrl` | | Integrity check: re-registration failed | `Warning` | `instanceId`, `slaveUrl` | | Background service tick started | `Debug` | `instanceCount` | | ApiKey decryption failed | `Error` | `instanceId` (do NOT log the key itself) | --- ## EF Core / Database ### Decision: Per-module `MasterDbContext` with own migrations | Aspect | Decision | |--------|----------| | DbContext class | `MasterDbContext : DbContext` in `SlpModularCms.Modules.Master` | | Migration assembly | `SlpModularCms.Modules.Master` (same project) | | Migration application | `app.ApplicationServices.CreateScope()` → `MasterDbContext.Database.MigrateAsync()` in `MasterModule.UseModule(IApplicationBuilder)` | | Tables owned | `CmsInstances`, `DataProtectionKeys` (if needed in future) | | Connection string | Reuses the same connection string as `ApplicationDbContext` (from `ConnectionStrings:DefaultConnection`) | --- ## Test Framework ### Decision: xUnit + Moq (matching existing test projects) | Aspect | Decision | Rationale | |--------|----------|-----------| | Test framework | xUnit | Matches existing `Availability.Tests` project | | Mocking | Moq | Matches existing test projects | | Coverage tool | coverlet (via `.runsettings` or `dotnet test --collect`) | Already in use in existing test projects | | `[ExcludeFromCodeCoverage]` targets | `MasterModule`, EF Core migration files, DTO records | Q4 decision | | HTTP testing | Mock `ISlaveApiClient` via Moq | Typed client interface enables clean mocking without `HttpMessageHandler` fakes | --- ## Summary of New Dependencies | Package | Version | Purpose | |---------|---------|---------| | `Microsoft.Extensions.Http.Resilience` | Latest stable | Polly integration for `IHttpClientFactory` retry policies | All other dependencies (EF Core, ASP.NET Core, xUnit, Moq) are already present in the solution.