using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories; using ZB.MOM.WW.ScadaBridge.Commons.Types.Kpi; using ZB.MOM.WW.ScadaBridge.KpiHistory; namespace ZB.MOM.WW.ScadaBridge.CentralUI.Services; /// /// Default implementation (M6 K11) — fetches /// the raw series via and /// reduces it with to at most the requested /// (or configured-default) number of points. /// /// /// /// Mirrors : the production constructor takes an /// and opens a fresh DI scope per query — /// resolving a fresh (and therefore a fresh /// ScadaBridgeDbContext) — so a trend chart's auto-load never contends with /// the circuit-scoped context the rest of the page uses. /// /// /// A second constructor injects an directly — a /// test seam (same dual-ctor pattern as ) so unit /// tests can substitute a stub without standing up a DI container. Both ctors take /// for the default series-point ceiling. /// /// public sealed class KpiHistoryQueryService : IKpiHistoryQueryService { // Production path: open a fresh scope per operation. Null in the test-seam ctor. private readonly IServiceScopeFactory? _scopeFactory; // Test seam: a directly-injected repository whose lifetime the test owns. // Null in the production ctor. private readonly IKpiHistoryRepository? _injectedRepository; private readonly KpiHistoryOptions _options; /// /// Production constructor — resolves from a /// fresh DI scope on every call so each query gets its own /// ScadaBridgeDbContext and never contends with the circuit-scoped context. /// /// Factory used to open a fresh DI scope per query. /// KPI History options supplying the default series-point ceiling. public KpiHistoryQueryService( IServiceScopeFactory scopeFactory, IOptions options) { _scopeFactory = scopeFactory ?? throw new ArgumentNullException(nameof(scopeFactory)); ArgumentNullException.ThrowIfNull(options); _options = options.Value ?? throw new ArgumentNullException(nameof(options)); } /// /// Test-seam constructor — injects a repository instance whose lifetime the /// caller owns. Used by unit tests that substitute a stub repository. /// /// The KPI history repository instance to use directly. /// KPI History options supplying the default series-point ceiling. public KpiHistoryQueryService( IKpiHistoryRepository repository, IOptions options) { _injectedRepository = repository ?? throw new ArgumentNullException(nameof(repository)); ArgumentNullException.ThrowIfNull(options); _options = options.Value ?? throw new ArgumentNullException(nameof(options)); } /// public async Task> GetSeriesAsync( string source, string metric, string scope, string? scopeKey, DateTime fromUtc, DateTime toUtc, int? maxPoints = null, CancellationToken cancellationToken = default) { var effectiveMax = maxPoints ?? _options.DefaultMaxSeriesPoints; // Test-seam ctor: use the injected repository directly. if (_injectedRepository is not null) { var injectedRaw = await _injectedRepository.GetRawSeriesAsync( source, metric, scope, scopeKey, fromUtc, toUtc, cancellationToken); return KpiSeriesBucketer.Bucket(injectedRaw, fromUtc, toUtc, effectiveMax); } // Production: a fresh scope (and thus a fresh DbContext) per query so a // chart's auto-load never shares the circuit-scoped context. await using var serviceScope = _scopeFactory!.CreateAsyncScope(); var repository = serviceScope.ServiceProvider.GetRequiredService(); var raw = await repository.GetRawSeriesAsync( source, metric, scope, scopeKey, fromUtc, toUtc, cancellationToken); return KpiSeriesBucketer.Bucket(raw, fromUtc, toUtc, effectiveMax); } }