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