using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Http; using Microsoft.Extensions.Options; using ScadaLink.Commons.Interfaces.Services; namespace ScadaLink.ExternalSystemGateway; public static class ServiceCollectionExtensions { /// /// Name prefix of the per-system clients /// created by (ExternalSystem_{systemName}). /// internal const string GatewayClientNamePrefix = "ExternalSystem_"; public static IServiceCollection AddExternalSystemGateway(this IServiceCollection services) { services.AddOptions() .BindConfiguration("ScadaLink:ExternalSystemGateway"); services.AddHttpClient(); // ExternalSystemGateway-013 / -016: wire MaxConcurrentConnectionsPerSystem // into the primary handler of the gateway's per-system named clients // ("ExternalSystem_{name}") only. The names are created dynamically, so a // static AddHttpClient("name") registration is not possible; instead a // post-configure on HttpClientFactoryOptions is applied, filtered by the // client-name prefix. ConfigureHttpClientDefaults is deliberately NOT used — // it is process-global and would replace the primary handler of every // HttpClient in the host (e.g. the Notification Service's OAuth2 token // client), silently capping and overriding unrelated components. services.AddSingleton>(sp => new GatewayHttpClientConfigurator( sp.GetRequiredService>())); services.AddScoped(); services.AddScoped(sp => sp.GetRequiredService()); services.AddScoped(); services.AddScoped(sp => sp.GetRequiredService()); return services; } public static IServiceCollection AddExternalSystemGatewayActors(this IServiceCollection services) { // WP-10: Actor registration happens in AkkaHostedService. // Script Execution Actors run on dedicated blocking I/O dispatcher. return services; } /// /// ExternalSystemGateway-016: configures the primary HTTP message handler with the /// gateway's /// cap, but only for the gateway's own named clients /// (). Clients owned by other host components /// are left untouched, so the cap does not leak process-wide. /// private sealed class GatewayHttpClientConfigurator : IConfigureNamedOptions { private readonly IOptionsMonitor _options; public GatewayHttpClientConfigurator(IOptionsMonitor options) { _options = options; } public void Configure(HttpClientFactoryOptions options) { // The default (unnamed) client is not a gateway client — do nothing. } public void Configure(string? name, HttpClientFactoryOptions options) { if (name == null || !name.StartsWith(GatewayClientNamePrefix, StringComparison.Ordinal)) { return; } options.HttpMessageHandlerBuilderActions.Add(builder => builder.PrimaryHandler = new SocketsHttpHandler { MaxConnectionsPerServer = _options.CurrentValue.MaxConcurrentConnectionsPerSystem, }); } } }