using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using ZB.MOM.WW.ScadaBridge.Communication.Grpc; namespace ZB.MOM.WW.ScadaBridge.Communication; public static class ServiceCollectionExtensions { /// Registers communication services including options, , gRPC client factory, and debug stream. /// The DI service collection to register services into. /// The same to allow chaining. public static IServiceCollection AddCommunication(this IServiceCollection services) { // Canonical section is ScadaBridge:Communication — the section every real // appsettings actually writes (arch-review 08 round 2 NF8). The Host used to // supply the real values via a duplicate Configure<> binding; that duplicate is // gone, so AddCommunication must bind the canonical section itself. services.AddOptions() .BindConfiguration("ScadaBridge:Communication") .ValidateOnStart(); services.TryAddEnumerable( ServiceDescriptor.Singleton, CommunicationOptionsValidator>()); services.AddSingleton(); // Explicit factory rather than AddSingleton(): the ISitePskProvider dependency is // optional (central registers one, a site node does not), and constructor selection // over a nullable interface parameter is exactly the case the container cannot decide // for itself — GetService returns null cleanly where constructor injection would throw. services.AddSingleton(sp => new SiteStreamGrpcClientFactory( sp.GetRequiredService(), sp.GetRequiredService>(), sp.GetService())); services.AddSingleton(); // Aggregated live alarm cache transient, in-memory, shared // per-site aggregator backing the operator Alarm Summary page. Singleton so every // Blazor circuit shares one aggregator per site; started/stopped by viewer // reference-count. No persistence (locked [PERM]: no central alarm store). services.AddSingleton(); services.AddSingleton(sp => sp.GetRequiredService()); return services; } /// /// Registers the communication services that only a CENTRAL node can construct. /// Call alongside from the Central composition root only. /// /// /// /// is scoped and takes IDeploymentManagerRepository — /// an EF repository registered by AddConfigurationDatabase, which only central calls. /// It used to live in the shared on the argument that it was /// "harmless on site hosts: only the central actor ever resolves it". That is true of the /// runtime behaviour but not of the container: the registration was UNCONSTRUCTIBLE on a /// site node, so ValidateOnBuild failed the whole site graph and the site branch /// could not have scope validation turned on at all (deferred register row 50). A /// registration nothing can ever build is not harmless — it is the thing that stops the /// validator from being usable, and it hides whatever real defect the validator would /// otherwise have found. /// /// /// The DI service collection to register services into. /// The same to allow chaining. public static IServiceCollection AddCentralCommunication(this IServiceCollection services) { // Startup reconciliation handler — scoped (holds scoped repositories), resolved // per-request by CentralCommunicationActor inside a DI scope. services.AddScoped(); return services; } /// Hook for registering additional DI services needed by communication actors; actor creation itself happens inside AkkaHostedService. /// The DI service collection to register services into. /// The same to allow chaining. public static IServiceCollection AddCommunicationActors(this IServiceCollection services) { // Actor registration happens in AkkaHostedService.RegisterCentralActors/RegisterSiteActors. // This method is a hook for any additional DI registrations needed by the communication actors. return services; } }