using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Options; using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Kpi; using ZB.MOM.WW.ScadaBridge.NotificationOutbox.Delivery; using ZB.MOM.WW.ScadaBridge.NotificationOutbox.Kpi; namespace ZB.MOM.WW.ScadaBridge.NotificationOutbox; /// /// DI registration for the Notification Outbox component: binds /// and registers the channel delivery adapters. /// public static class ServiceCollectionExtensions { /// Configuration section bound to . public const string OptionsSection = "ScadaBridge:NotificationOutbox"; /// Configuration section bound to . public const string SmsOptionsSection = "ScadaBridge:Sms"; /// /// Registers the Notification Outbox services: the /// binding and the channel delivery adapters. /// /// This extension covers only the outbox-specific registrations. The /// reuses the /// SMTP machinery — /// Func<ISmtpClientWrapper>, OAuth2TokenService and /// NotificationOptions — so the caller (the Host on the central node) must also /// call AddNotificationService(). Re-registering those services here would /// duplicate them; relying on AddNotificationService keeps a single source of truth. /// /// is registered scoped because it /// takes a scoped /// directly. The resolves the adapters from a fresh /// scope per dispatch sweep rather than holding them, so no scoped adapter is captured by /// the singleton actor. /// /// The (Twilio-REST) is registered the same /// way alongside the Email adapter, so both resolve from /// GetServices<INotificationDeliveryAdapter>(). It also binds + validates /// (ScadaBridge:Sms) and the named "Twilio" /// ; unlike the Email adapter it needs no NotificationService /// machinery (a single outbound HTTPS POST per recipient). /// /// The DI service collection to register notification outbox services into. /// The same instance for chaining. public static IServiceCollection AddNotificationOutbox(this IServiceCollection services) { ArgumentNullException.ThrowIfNull(services); services.AddOptions() .BindConfiguration(OptionsSection); // Bind + validate SmsOptions (ScadaBridge:Sms): the SMS adapter's body cap and // per-request timeout fallback. ValidateOnStart surfaces a bad option at host // startup rather than at first delivery; the validator is registered via // TryAddEnumerable so a re-call of AddNotificationOutbox does not double-register it. services.AddOptions() .BindConfiguration(SmsOptionsSection) .ValidateOnStart(); services.TryAddEnumerable( ServiceDescriptor.Singleton, SmsOptionsValidator>()); // Named HTTP client backing the Twilio-REST SMS adapter. Pooled handler lifetime // is managed by IHttpClientFactory; the adapter applies the per-request timeout // via a linked CTS (so the client's own Timeout stays at its default). services.AddHttpClient(SmsNotificationDeliveryAdapter.HttpClientName); // Scoped: the adapters hold a scoped INotificationRepository. Each is registered // both under the interface (so the dispatch sweep can enumerate every channel // adapter — both Email and SMS must resolve from GetServices()) // and as the concrete type (so callers and tests can resolve it directly). services.AddScoped(); services.AddScoped( sp => sp.GetRequiredService()); services.AddScoped(); services.AddScoped( sp => sp.GetRequiredService()); // KPI history: the recorder singleton enumerates every IKpiSampleSource each // sampling pass to snapshot the outbox delivery KPIs into the central history store. // TryAddEnumerable is idempotent — no double-registration if AddNotificationOutbox // is ever called more than once, matching the AuditLog idiom. services.TryAddEnumerable(ServiceDescriptor.Scoped()); return services; } }