using Microsoft.AspNetCore.DataProtection; using Microsoft.EntityFrameworkCore; namespace ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Maintenance; /// /// The connection the AuditLog maintenance paths run on — the hash-chain job and both purge /// paths (partition switch-out with its archive, per-channel DELETE) — ScadaBridge#107 item 3. /// /// /// /// With AuditLog:MaintenanceConnectionString unset (the default), this is the scope's /// ordinary : one login for everything, exactly as before. /// With it set, the maintenance paths get their OWN context on that connection string, so a /// DBA can give the runtime (writer) login only scadabridge_audit_writer — whose DENYs /// stop it writing chain state or deleting audit rows — and a separate maintenance login the /// scadabridge_audit_chainer + scadabridge_audit_purger roles. The string is read /// after the pre-host ${secret:} expansion, so it may (and in production should) be a /// secret reference. /// /// Scoped: an owned context is disposed with the scope; a borrowed one is left to DI. /// public sealed class AuditMaintenanceContext : IAsyncDisposable, IDisposable { /// DI key of the maintenance-login IAuditLogRepository and IAuditLogArchive. public const string ServiceKey = "audit-maintenance"; private readonly bool _owned; private AuditMaintenanceContext(ScadaBridgeDbContext context, bool owned) { Context = context; _owned = owned; } /// The context to run maintenance SQL on. public ScadaBridgeDbContext Context { get; } /// True when is on the separate maintenance connection string. public bool UsesSeparateLogin => _owned; /// Wraps the scope's ordinary context (no separate maintenance login configured). /// The scope's context. /// The wrapper. public static AuditMaintenanceContext Shared(ScadaBridgeDbContext context) => new(context ?? throw new ArgumentNullException(nameof(context)), owned: false); /// Opens an owned context on the maintenance connection string. /// The maintenance connection string. /// Data protection for secret-bearing columns (same as the runtime context). /// The wrapper; disposing it disposes the context. public static AuditMaintenanceContext Separate(string connectionString, IDataProtectionProvider? protectionProvider) { ArgumentException.ThrowIfNullOrWhiteSpace(connectionString); var options = new DbContextOptionsBuilder() .UseSqlServer(connectionString, sql => sql.EnableRetryOnFailure( maxRetryCount: 5, maxRetryDelay: TimeSpan.FromSeconds(30), errorNumbersToAdd: null)) .ConfigureWarnings(w => w.Ignore( Microsoft.EntityFrameworkCore.Diagnostics.RelationalEventId.PendingModelChangesWarning)) .Options; var context = protectionProvider is null ? new ScadaBridgeDbContext(options) : new ScadaBridgeDbContext(options, protectionProvider); return new AuditMaintenanceContext(context, owned: true); } /// public ValueTask DisposeAsync() => _owned ? Context.DisposeAsync() : ValueTask.CompletedTask; /// public void Dispose() { if (_owned) { Context.Dispose(); } } } /// The configured maintenance connection string (null = use the runtime connection). /// The connection string, or null. public sealed record AuditMaintenanceConnection(string? ConnectionString);