Files
slp-modular-cms/aidlc-docs/features/master-cms-module/construction/slave-availability-extension/nfr-design/nfr-design-patterns.md
T

7.4 KiB

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:

public interface IMasterApiKeyProtector
{
    string Protect(string plainApiKey);
    string? Unprotect(string encryptedApiKey);  // null on CryptographicException
}

Implementation:

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):

services.AddDataProtection();
services.AddSingleton<IMasterApiKeyProtector, MasterApiKeyProtector>();

Usage in tests:

var protector = Substitute.For<IMasterApiKeyProtector>();
protector.Protect(Arg.Any<string>()).Returns(s => $"enc:{s.ArgAt<string>(0)}");
protector.Unprotect(Arg.Any<string>()).Returns(s => s.ArgAt<string>(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:

private static volatile bool _masterIsAvailable = true;
private static volatile string? _masterDisableMessage = null;

Return type:

public record MasterGateStatus(bool IsAvailable, string? DisableMessage);

Interface method (sync — no Task):

public interface IMasterAvailabilityService
{
    Task RegisterAsync(string masterUrl, string apiKey);
    Task PushStatusAsync(string apiKey, bool isAvailable, string? disableMessage);
    Task<string?> GetRegisteredUrlAsync(string apiKey);
    MasterGateStatus GetMasterStatus();    // sync; reads volatile fields
}

Implementation:

public MasterGateStatus GetMasterStatus()
    => new(_masterIsAvailable, _masterDisableMessage);

Write point (only in PushStatusAsync):

_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:

[ExcludeFromCodeCoverage]
public record MasterAvailabilityServiceDependencies(
    IMasterRegistrationRepository Repository,
    IMasterApiKeyProtector KeyProtector,
    ILogger<MasterAvailabilityService> Logger
);

Registration:

services.AddScoped<MasterAvailabilityServiceDependencies>();
services.AddScoped<IMasterAvailabilityService, MasterAvailabilityService>();

MasterAvailabilityService constructor:

public MasterAvailabilityService(MasterAvailabilityServiceDependencies deps)
{
    _deps = deps;
}

Test construction (no DI container):

var deps = new MasterAvailabilityServiceDependencies(
    Substitute.For<IMasterRegistrationRepository>(),
    Substitute.For<IMasterApiKeyProtector>(),
    NullLogger<MasterAvailabilityService>.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:

public async Task InvokeAsync(
    HttpContext context,
    IAvailabilityService localSvc,
    IMasterAvailabilityService masterSvc)

Evaluation order:

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):

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):

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):

var masterSvc = Substitute.For<IMasterAvailabilityService>();
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:

_logger.LogWarning(
    "Master API key mismatch on {Endpoint} — returning 401",
    "POST /api/v1/master/status");