using Microsoft.AspNetCore.DataProtection; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using ScadaLink.Commons.Interfaces; using ScadaLink.Commons.Interfaces.Repositories; using ScadaLink.Commons.Interfaces.Services; using ScadaLink.Commons.Interfaces.Transport; using ScadaLink.ConfigurationDatabase.Maintenance; using ScadaLink.ConfigurationDatabase.Repositories; using ScadaLink.ConfigurationDatabase.Services; namespace ScadaLink.ConfigurationDatabase; public static class ServiceCollectionExtensions { /// /// Registers the ScadaLinkDbContext with the provided SQL Server connection string. /// /// The service collection to register into. /// SQL Server connection string for the central configuration database. 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 ScadaLinkDbContext 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 ScadaLinkDbContext(options, protectionProvider); }); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); // CD-016: factory registration wires a lazy accessor for IApiKeyHasher so // the production peppered hasher is used (via DI) when GetApiKeyByValueAsync // is actually called, but composition roots that never call it (and may // not register IApiKeyHasher at all) still bring up the repository. services.AddScoped(sp => new InboundApiRepository( sp.GetRequiredService(), hasherAccessor: () => sp.GetService() ?? Commons.Types.InboundApi.ApiKeyHasher.Default, logger: sp.GetService>())); 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 // ScadaLinkDbContext 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. /// [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 " + "'ScadaLink:Database:ConfigurationDb' value."); } }