using Microsoft.AspNetCore.DataProtection; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using ZB.MOM.WW.ScadaBridge.Commons.Interfaces; using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories; using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Services; using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Transport; using ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Maintenance; using ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Repositories; using ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Services; namespace ZB.MOM.WW.ScadaBridge.ConfigurationDatabase; public static class ServiceCollectionExtensions { /// /// Registers the ScadaBridgeDbContext with the provided SQL Server connection string. /// /// The service collection to register into. /// SQL Server connection string for the central configuration database. /// The same collection, for chaining. public static IServiceCollection AddConfigurationDatabase(this IServiceCollection services, string connectionString) { // The DbContext is constructed via the (options, IDataProtectionProvider) overload so // secret-bearing configuration columns are encrypted at rest. AddDataProtection below // registers IDataProtectionProvider as a singleton; resolving it here does not recurse // because key-ring loading is lazy (first Protect/Unprotect), not triggered by // CreateProtector during model building. services.AddDbContext((serviceProvider, options) => { options.UseSqlServer(connectionString) .ConfigureWarnings(w => w.Ignore( Microsoft.EntityFrameworkCore.Diagnostics.RelationalEventId.PendingModelChangesWarning)); }); // AddDbContext registers ScadaBridgeDbContext via EF's activator, which only injects // DbContextOptions. Override that registration (last registration wins for resolution) // with a factory that also supplies the IDataProtectionProvider, so the encrypting // value converter for secret columns is always wired up at runtime. services.AddScoped(serviceProvider => { var options = serviceProvider.GetRequiredService>(); var protectionProvider = serviceProvider.GetRequiredService(); return new ScadaBridgeDbContext(options, protectionProvider); }); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); // Auth re-arch (C5): inbound API keys are no longer persisted in SQL Server — // the repository now exposes only API-method access, so a plain scoped // registration suffices (no peppered-hasher accessor to wire). services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); // #23 M6 Bundle D: IPartitionMaintenance drives the daily roll-forward // of pf_AuditLog_Month from the central AuditLogPartitionMaintenanceService // hosted service. Scoped because the implementation reuses the per-scope // ScadaBridgeDbContext for raw-SQL execution; the hosted service opens a // fresh scope on each tick (mirrors AuditLogPurgeActor / AuditLogIngestActor). services.AddScoped(); services.AddDataProtection() .PersistKeysToDbContext(); return services; } /// /// Obsolete parameterless overload. This previously registered nothing, which meant a /// central node wired up with it failed late and opaquely — the first repository /// resolution threw a DI exception far from the actual misconfiguration. Use /// and pass the /// configured connection string. /// /// The service collection (unused; this overload always throws). /// /// Always thrown. The connection string is required; there is no valid no-op registration. /// /// Never returns; always throws . [Obsolete( "AddConfigurationDatabase() with no connection string registers nothing and is not a " + "valid configuration. Call AddConfigurationDatabase(connectionString) instead.", error: true)] public static IServiceCollection AddConfigurationDatabase(this IServiceCollection services) { // Defence-in-depth: even if a caller suppresses the compile-time obsolete error, // fail fast at wire-up time rather than silently registering nothing and surfacing // an opaque DI resolution failure much later. throw new InvalidOperationException( "AddConfigurationDatabase() requires a connection string. Call " + "AddConfigurationDatabase(connectionString) with the configured " + "'ScadaBridge:Database:ConfigurationDb' value."); } }