feat(kpi): K2 — KpiSample EF mapping + KpiHistoryRepository + AddKpiSampleTable migration

This commit is contained in:
Joseph Doherty
2026-06-17 19:44:51 -04:00
parent 460777bffa
commit cabc557629
8 changed files with 2151 additions and 0 deletions
@@ -0,0 +1,66 @@
using Microsoft.EntityFrameworkCore;
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Kpi;
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Kpi;
namespace ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Repositories;
/// <summary>
/// EF Core implementation of <see cref="IKpiHistoryRepository"/> over the central
/// <c>KpiSample</c> table (M6 "KPI History &amp; Trends"). See the interface for the
/// contract; this class adds notes on the data-access strategy per method.
/// </summary>
public class KpiHistoryRepository : IKpiHistoryRepository
{
private readonly ScadaBridgeDbContext _context;
/// <summary>
/// Initializes a new instance of the <see cref="KpiHistoryRepository"/> class.
/// </summary>
/// <param name="context">The EF Core database context.</param>
public KpiHistoryRepository(ScadaBridgeDbContext context)
{
_context = context ?? throw new ArgumentNullException(nameof(context));
}
/// <inheritdoc />
public async Task RecordSamplesAsync(
IReadOnlyCollection<KpiSample> samples, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(samples);
// Bulk-insert one sampling pass. AddRange + a single SaveChanges keeps the
// whole batch in one round-trip; the store assigns each row's identity.
_context.KpiSamples.AddRange(samples);
await _context.SaveChangesAsync(cancellationToken);
}
/// <inheritdoc />
public async Task<IReadOnlyList<KpiSeriesPoint>> GetRawSeriesAsync(
string source, string metric, string scope, string? scopeKey,
DateTime fromUtc, DateTime toUtc, CancellationToken cancellationToken = default)
{
// The ScopeKey == scopeKey comparison is intentional: when scopeKey is null
// EF translates it to "ScopeKey IS NULL", which matches the Global-scope rows
// (null key) and excludes the site/node-scoped rows that carry a non-null key.
return await _context.KpiSamples
.Where(s => s.Source == source
&& s.Metric == metric
&& s.Scope == scope
&& s.ScopeKey == scopeKey
&& s.CapturedAtUtc >= fromUtc
&& s.CapturedAtUtc <= toUtc)
.OrderBy(s => s.CapturedAtUtc)
.Select(s => new KpiSeriesPoint(s.CapturedAtUtc, s.Value))
.ToListAsync(cancellationToken);
}
/// <inheritdoc />
public async Task<int> PurgeOlderThanAsync(DateTime before, CancellationToken cancellationToken = default)
{
// Set-based delete — no entity materialisation; returns the rows affected.
return await _context.KpiSamples
.Where(s => s.CapturedAtUtc < before)
.ExecuteDeleteAsync(cancellationToken);
}
}