using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Npgsql; using StellaOps.Infrastructure.Postgres.Migrations; using StellaOps.JobEngine.Core.Backfill; using StellaOps.JobEngine.Core.DeadLetter; using StellaOps.JobEngine.Core.Observability; using StellaOps.JobEngine.Core.Repositories; using StellaOps.JobEngine.Core.Services; using StellaOps.JobEngine.Infrastructure.Caching; using StellaOps.JobEngine.Infrastructure.Ledger; using StellaOps.JobEngine.Infrastructure.Observability; using StellaOps.JobEngine.Infrastructure.Options; using StellaOps.JobEngine.Infrastructure.Postgres; using StellaOps.JobEngine.Infrastructure.Repositories; using StellaOps.JobEngine.Infrastructure.Services; using System; using System.Linq; namespace StellaOps.JobEngine.Infrastructure; /// /// Extension methods for registering JobEngine infrastructure services. /// public static class ServiceCollectionExtensions { /// /// Adds JobEngine infrastructure services to the service collection. /// /// The service collection. /// The configuration. /// The service collection for chaining. public static IServiceCollection AddJobEngineInfrastructure( this IServiceCollection services, IConfiguration configuration) { // Register configuration options services.AddOptions() .Bind(configuration.GetSection(JobEngineServiceOptions.SectionName)) .PostConfigure(options => { var explicitConnection = configuration[$"{JobEngineServiceOptions.SectionName}:Database:ConnectionString"]; if (!string.IsNullOrWhiteSpace(explicitConnection)) { options.Database.ConnectionString = explicitConnection; return; } var orchestratorConnection = configuration["Orchestrator:Database:ConnectionString"]; if (ShouldReplaceConnectionString(options.Database.ConnectionString) && !string.IsNullOrWhiteSpace(orchestratorConnection)) { options.Database.ConnectionString = orchestratorConnection; } var fallbackConnection = configuration.GetConnectionString("Default") ?? configuration["ConnectionStrings:Default"]; if (string.IsNullOrWhiteSpace(fallbackConnection)) { return; } if (ShouldReplaceConnectionString(options.Database.ConnectionString)) { options.Database.ConnectionString = fallbackConnection; } }); services.AddStartupMigrations( schemaName: "orchestrator", moduleName: "JobEngine", migrationsAssembly: typeof(JobEngineDataSource).Assembly, connectionStringSelector: options => options.Database.ConnectionString); // Register data source services.AddSingleton(); // Register repositories services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); // Register audit and ledger repositories services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); // Register ledger exporter service services.AddScoped(); // Register duplicate suppression factory services.AddSingleton(); // Register golden signals metrics (per ORCH-OBS-51-001) services.AddSingleton(); // Register incident mode hooks (per ORCH-OBS-55-001) var incidentModeOptions = configuration .GetSection(IncidentModeHooksOptions.SectionName) .Get() ?? new IncidentModeHooksOptions(); services.AddSingleton(incidentModeOptions); services.AddSingleton(); // First signal (TTFS) services services.Configure(configuration.GetSection(FirstSignalOptions.SectionName)); services.AddHttpClient(); services.AddSingleton(); services.AddScoped(); // Circuit breaker and quota governance services (per SPRINT_20260208_042) services.AddScoped(); services.AddScoped(); return services; } private static bool ShouldReplaceConnectionString(string? configuredConnectionString) { if (string.IsNullOrWhiteSpace(configuredConnectionString)) { return true; } try { var builder = new NpgsqlConnectionStringBuilder(configuredConnectionString); var host = builder.Host?.Trim(); if (string.IsNullOrWhiteSpace(host)) { return true; } return host.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) .All(IsLoopbackHost); } catch { return false; } } private static bool IsLoopbackHost(string host) { return host.Equals("localhost", StringComparison.OrdinalIgnoreCase) || host.Equals("127.0.0.1", StringComparison.OrdinalIgnoreCase) || host.Equals("::1", StringComparison.OrdinalIgnoreCase); } }