96 lines
4.6 KiB
C#
96 lines
4.6 KiB
C#
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;
|
|
|
|
/// <summary>
|
|
/// Default <see cref="IKpiHistoryQueryService"/> implementation (M6 K11) — fetches
|
|
/// the raw series via <see cref="IKpiHistoryRepository.GetRawSeriesAsync"/> and
|
|
/// reduces it with <see cref="KpiSeriesBucketer.Bucket"/> to at most the requested
|
|
/// (or configured-default) number of points.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// <para>
|
|
/// Mirrors <see cref="AuditLogQueryService"/>: the production constructor takes an
|
|
/// <see cref="IServiceScopeFactory"/> and opens a fresh DI scope per query —
|
|
/// resolving a fresh <see cref="IKpiHistoryRepository"/> (and therefore a fresh
|
|
/// <c>ScadaBridgeDbContext</c>) — so a trend chart's auto-load never contends with
|
|
/// the circuit-scoped context the rest of the page uses.
|
|
/// </para>
|
|
/// <para>
|
|
/// A second constructor injects an <see cref="IKpiHistoryRepository"/> directly — a
|
|
/// test seam (same dual-ctor pattern as <see cref="AuditLogQueryService"/>) so unit
|
|
/// tests can substitute a stub without standing up a DI container. Both ctors take
|
|
/// <see cref="IOptions{KpiHistoryOptions}"/> for the default series-point ceiling.
|
|
/// </para>
|
|
/// </remarks>
|
|
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;
|
|
|
|
/// <summary>
|
|
/// Production constructor — resolves <see cref="IKpiHistoryRepository"/> from a
|
|
/// fresh DI scope on every call so each query gets its own
|
|
/// <c>ScadaBridgeDbContext</c> and never contends with the circuit-scoped context.
|
|
/// </summary>
|
|
/// <param name="scopeFactory">Factory used to open a fresh DI scope per query.</param>
|
|
/// <param name="options">KPI History options supplying the default series-point ceiling.</param>
|
|
public KpiHistoryQueryService(
|
|
IServiceScopeFactory scopeFactory,
|
|
IOptions<KpiHistoryOptions> options)
|
|
{
|
|
_scopeFactory = scopeFactory ?? throw new ArgumentNullException(nameof(scopeFactory));
|
|
ArgumentNullException.ThrowIfNull(options);
|
|
_options = options.Value ?? throw new ArgumentNullException(nameof(options));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Test-seam constructor — injects a repository instance whose lifetime the
|
|
/// caller owns. Used by unit tests that substitute a stub repository.
|
|
/// </summary>
|
|
/// <param name="repository">The KPI history repository instance to use directly.</param>
|
|
/// <param name="options">KPI History options supplying the default series-point ceiling.</param>
|
|
public KpiHistoryQueryService(
|
|
IKpiHistoryRepository repository,
|
|
IOptions<KpiHistoryOptions> options)
|
|
{
|
|
_injectedRepository = repository ?? throw new ArgumentNullException(nameof(repository));
|
|
ArgumentNullException.ThrowIfNull(options);
|
|
_options = options.Value ?? throw new ArgumentNullException(nameof(options));
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public async Task<IReadOnlyList<KpiSeriesPoint>> 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<IKpiHistoryRepository>();
|
|
var raw = await repository.GetRawSeriesAsync(
|
|
source, metric, scope, scopeKey, fromUtc, toUtc, cancellationToken);
|
|
return KpiSeriesBucketer.Bucket(raw, fromUtc, toUtc, effectiveMax);
|
|
}
|
|
}
|