9.2 KiB
Logical Components — Unit 2: slave-availability-extension
Component Overview
graph TD
subgraph Controllers
MC["MasterController\n(new)"]
end
subgraph Services
IMAS["IMasterAvailabilityService"]
MAS["MasterAvailabilityService\n(volatile static cache)"]
IMAKP["IMasterApiKeyProtector"]
MAKP["MasterApiKeyProtector"]
MASD["MasterAvailabilityServiceDependencies\n(record)"]
end
subgraph Repositories
IMRR["IMasterRegistrationRepository"]
MRR["MasterRegistrationRepository"]
end
subgraph Data
AVDBCTX["AvailabilityDbContext\n(new)"]
MR["MasterRegistration\n(entity)"]
end
subgraph Middleware
AVMW["AvailabilityMiddleware\n(extended)"]
end
subgraph External
DP["IDataProtectionProvider\n(ASP.NET Core)"]
end
MC -->|delegates| IMAS
MAS -.->|implements| IMAS
MAS -->|uses| MASD
MASD -->|contains| IMRR
MASD -->|contains| IMAKP
MRR -.->|implements| IMRR
MAKP -.->|implements| IMAKP
MRR -->|reads/writes| AVDBCTX
AVDBCTX -->|owns| MR
MAKP -->|wraps| DP
AVMW -->|InvokeAsync param| IMAS
classDef interface fill:#fff,stroke:#63b3ed,stroke-width:2px,color:#2b6cb0
classDef impl fill:#63b3ed,stroke:#2b6cb0,color:#fff
classDef entity fill:#FFC107,stroke:#F57F17,color:#000
classDef infra fill:#9ae6b4,stroke:#2f855a,color:#000
classDef middleware fill:#CE93D8,stroke:#6A1B9A,color:#000
classDef external fill:#eee,stroke:#999,color:#333
class IMAS,IMRR,IMAKP interface
class MAS,MRR,MAKP,MASD impl
class MR,AVDBCTX entity
class MC infra
class AVMW middleware
class DP external
Text alternative: MasterController delegates to IMasterAvailabilityService. MasterAvailabilityService uses MasterAvailabilityServiceDependencies which holds IMasterRegistrationRepository and IMasterApiKeyProtector. Repository uses AvailabilityDbContext. MasterApiKeyProtector wraps IDataProtectionProvider. AvailabilityMiddleware receives IMasterAvailabilityService as third InvokeAsync parameter.
Component Specifications
1. MasterRegistration (Entity)
Namespace: SlpModularCms.Modules.Availability.Data.Entities
| Property | Type | Notes |
|---|---|---|
Id |
Guid |
PK; always new Guid("00000000-0000-0000-0000-000000000001") |
MasterUrl |
string |
Required; max 500 |
ApiKey |
string |
Required; max 2000 (encrypted via Data Protection) |
RegisteredAt |
DateTimeOffset |
Set once on creation |
LastContactedAt |
DateTimeOffset? |
Updated on every valid master call |
2. AvailabilityDbContext
Namespace: SlpModularCms.Modules.Availability.Data
public class AvailabilityDbContext : DbContext
{
public DbSet<MasterRegistration> MasterRegistrations => Set<MasterRegistration>();
}
| Aspect | Decision |
|---|---|
| Migration assembly | SlpModularCms.Modules.Availability |
| Table | AvailabilityMasterRegistrations |
| Applied at | AvailabilityModule.UseModule → MigrateAsync() |
| Connection string | ConnectionStrings:DefaultConnection (same as other DbContexts) |
3. IMasterRegistrationRepository / MasterRegistrationRepository
Namespace: SlpModularCms.Modules.Availability.Repositories
public interface IMasterRegistrationRepository
{
Task<MasterRegistration?> GetAsync();
Task UpsertAsync(MasterRegistration registration);
Task SaveChangesAsync();
}
| Method | Notes |
|---|---|
GetAsync() |
Loads singleton by fixed Id; returns null if row does not exist |
UpsertAsync(registration) |
Add if not tracked; Update if tracked or found by Id |
SaveChangesAsync() |
Explicit save; service controls transaction boundary |
Registration: services.AddScoped<IMasterRegistrationRepository, MasterRegistrationRepository>()
4. IMasterApiKeyProtector / MasterApiKeyProtector
Namespace: SlpModularCms.Modules.Availability.Services
public interface IMasterApiKeyProtector
{
string Protect(string plainApiKey);
string? Unprotect(string encryptedApiKey); // null on CryptographicException
}
Registration: services.AddSingleton<IMasterApiKeyProtector, MasterApiKeyProtector>()
5. MasterAvailabilityServiceDependencies (Record)
Namespace: SlpModularCms.Modules.Availability.Services
[ExcludeFromCodeCoverage]
public record MasterAvailabilityServiceDependencies(
IMasterRegistrationRepository Repository,
IMasterApiKeyProtector KeyProtector,
ILogger<MasterAvailabilityService> Logger
);
Registration: services.AddScoped<MasterAvailabilityServiceDependencies>()
6. IMasterAvailabilityService / MasterAvailabilityService
Namespace: SlpModularCms.Modules.Availability.Services
public interface IMasterAvailabilityService
{
Task RegisterAsync(string masterUrl, string apiKey);
Task PushStatusAsync(string apiKey, bool isAvailable, string? disableMessage);
Task<string?> GetRegisteredUrlAsync(string apiKey);
MasterGateStatus GetMasterStatus();
}
Static fields (in implementation):
private static volatile bool _masterIsAvailable = true;
private static volatile string? _masterDisableMessage = null;
Key validation helper (private, reused across all 3 write-path methods):
private async Task<MasterRegistration?> ValidateApiKeyAsync(string apiKey)
{
var registration = await _deps.Repository.GetAsync();
if (registration is null) return null;
var stored = _deps.KeyProtector.Unprotect(registration.ApiKey);
return stored == apiKey ? registration : null;
}
Registration: services.AddScoped<IMasterAvailabilityService, MasterAvailabilityService>()
7. MasterGateStatus (Record)
Namespace: SlpModularCms.Modules.Availability.Services
[ExcludeFromCodeCoverage]
public record MasterGateStatus(bool IsAvailable, string? DisableMessage);
8. MasterController
Namespace: SlpModularCms.Modules.Availability.Controllers
[ApiController]
[Route("[controller]")] // → /api/v1/master via ApiPrefixConvention
public class MasterController : ControllerBase
{
[HttpPost("register")] // POST /api/v1/master/register
[HttpPost("status")] // POST /api/v1/master/status
[HttpGet("registered-url")] // GET /api/v1/master/registered-url
}
Constructor: MasterController(IMasterAvailabilityService svc) — single dependency, no record wrapper needed.
Auth: No [Authorize] attribute — API key validated in MasterAvailabilityService.
Response on 401: Unauthorized() — no body to avoid leaking registration state.
9. AvailabilityMiddleware (Extended)
Extended fields (added to existing class):
// Updated bypass prefix list
private static readonly string[] _bypassPrefixes =
[
"/api/v1/Availability/status",
"/api/v1/Auth/",
"/api/v1/Setup/status",
"/api/v1/master/" // NEW
];
Updated InvokeAsync signature:
public async Task InvokeAsync(
HttpContext context,
IAvailabilityService localSvc,
IMasterAvailabilityService masterSvc)
Dependency Registration Summary
All new registrations added to AvailabilityModule.RegisterServices:
// Data
services.AddDbContext<AvailabilityDbContext>((sp, options) =>
options.UseSqlServer(sp.GetRequiredService<IConfiguration>()
.GetConnectionString("DefaultConnection")));
// Security
services.AddDataProtection();
services.AddSingleton<IMasterApiKeyProtector, MasterApiKeyProtector>();
// Repositories
services.AddScoped<IMasterRegistrationRepository, MasterRegistrationRepository>();
// Services
services.AddScoped<MasterAvailabilityServiceDependencies>();
services.AddScoped<IMasterAvailabilityService, MasterAvailabilityService>();
And in AvailabilityModule.UseModule:
using var scope = app.ApplicationServices.CreateScope();
await scope.ServiceProvider
.GetRequiredService<AvailabilityDbContext>()
.Database.MigrateAsync();
New Files Summary
| File | Project | Type |
|---|---|---|
Data/Entities/MasterRegistration.cs |
Availability | Entity |
Data/AvailabilityDbContext.cs |
Availability | DbContext |
Repositories/IMasterRegistrationRepository.cs |
Availability | Interface |
Repositories/MasterRegistrationRepository.cs |
Availability | Implementation |
Services/IMasterApiKeyProtector.cs |
Availability | Interface |
Services/MasterApiKeyProtector.cs |
Availability | Implementation |
Services/MasterGateStatus.cs |
Availability | Record |
Services/MasterAvailabilityServiceDependencies.cs |
Availability | Record |
Services/IMasterAvailabilityService.cs |
Availability | Interface |
Services/MasterAvailabilityService.cs |
Availability | Implementation |
Controllers/MasterController.cs |
Availability | Controller |
Middleware/AvailabilityMiddleware.cs |
Availability | Modified (extended) |
AvailabilityModule.cs |
Availability | Modified (registration + migration) |
Data/Migrations/* |
Availability | EF Core auto-generated |