docs: add XML doc comments across src + Sister Projects section in CLAUDE.md
Bulk CommentChecker pass: fills in <param>/<inheritdoc> tags on public APIs across all 23 src/ projects so the doc-coverage gate is green. Also adds a Sister Projects section to CLAUDE.md pointing at the MxAccess Gateway and OtOpcUa sibling repos, and gitignores local credential captures (*login*.txt) and the wonder-app-vd03 deploy/ artifacts.
This commit is contained in:
@@ -6,6 +6,10 @@ namespace ScadaLink.ConfigurationDatabase.Configurations;
|
||||
|
||||
public class AuditLogEntryConfiguration : IEntityTypeConfiguration<AuditLogEntry>
|
||||
{
|
||||
/// <summary>
|
||||
/// Configures the EF Core entity mapping for <see cref="AuditLogEntry"/>.
|
||||
/// </summary>
|
||||
/// <param name="builder">The entity type builder for <see cref="AuditLogEntry"/>.</param>
|
||||
public void Configure(EntityTypeBuilder<AuditLogEntry> builder)
|
||||
{
|
||||
builder.HasKey(a => a.Id);
|
||||
|
||||
@@ -11,6 +11,8 @@ namespace ScadaLink.ConfigurationDatabase.Configurations;
|
||||
/// </summary>
|
||||
public class AuditLogEntityTypeConfiguration : IEntityTypeConfiguration<AuditEvent>
|
||||
{
|
||||
/// <summary>Applies the EF Core type configuration for <see cref="AuditEvent"/> to the model builder.</summary>
|
||||
/// <param name="builder">The entity type builder to configure.</param>
|
||||
public void Configure(EntityTypeBuilder<AuditEvent> builder)
|
||||
{
|
||||
builder.ToTable("AuditLog");
|
||||
|
||||
@@ -7,6 +7,8 @@ namespace ScadaLink.ConfigurationDatabase.Configurations;
|
||||
|
||||
public class DeploymentRecordConfiguration : IEntityTypeConfiguration<DeploymentRecord>
|
||||
{
|
||||
/// <summary>Configures the EF Core mapping for <see cref="DeploymentRecord"/>.</summary>
|
||||
/// <param name="builder">The entity type builder.</param>
|
||||
public void Configure(EntityTypeBuilder<DeploymentRecord> builder)
|
||||
{
|
||||
builder.HasKey(d => d.Id);
|
||||
@@ -43,6 +45,8 @@ public class DeploymentRecordConfiguration : IEntityTypeConfiguration<Deployment
|
||||
|
||||
public class DeployedConfigSnapshotConfiguration : IEntityTypeConfiguration<DeployedConfigSnapshot>
|
||||
{
|
||||
/// <summary>Configures the EF Core mapping for <see cref="DeployedConfigSnapshot"/>.</summary>
|
||||
/// <param name="builder">The entity type builder.</param>
|
||||
public void Configure(EntityTypeBuilder<DeployedConfigSnapshot> builder)
|
||||
{
|
||||
builder.HasKey(s => s.Id);
|
||||
@@ -70,6 +74,8 @@ public class DeployedConfigSnapshotConfiguration : IEntityTypeConfiguration<Depl
|
||||
|
||||
public class SystemArtifactDeploymentRecordConfiguration : IEntityTypeConfiguration<SystemArtifactDeploymentRecord>
|
||||
{
|
||||
/// <summary>Configures the EF Core mapping for <see cref="SystemArtifactDeploymentRecord"/>.</summary>
|
||||
/// <param name="builder">The entity type builder.</param>
|
||||
public void Configure(EntityTypeBuilder<SystemArtifactDeploymentRecord> builder)
|
||||
{
|
||||
builder.HasKey(d => d.Id);
|
||||
|
||||
@@ -6,6 +6,8 @@ namespace ScadaLink.ConfigurationDatabase.Configurations;
|
||||
|
||||
public class ExternalSystemDefinitionConfiguration : IEntityTypeConfiguration<ExternalSystemDefinition>
|
||||
{
|
||||
/// <summary>Applies the EF Core entity type configuration for <see cref="ExternalSystemDefinition"/>.</summary>
|
||||
/// <param name="builder">The entity type builder to configure.</param>
|
||||
public void Configure(EntityTypeBuilder<ExternalSystemDefinition> builder)
|
||||
{
|
||||
builder.HasKey(e => e.Id);
|
||||
@@ -38,6 +40,8 @@ public class ExternalSystemDefinitionConfiguration : IEntityTypeConfiguration<Ex
|
||||
|
||||
public class ExternalSystemMethodConfiguration : IEntityTypeConfiguration<ExternalSystemMethod>
|
||||
{
|
||||
/// <summary>Applies the EF Core entity type configuration for <see cref="ExternalSystemMethod"/>.</summary>
|
||||
/// <param name="builder">The entity type builder to configure.</param>
|
||||
public void Configure(EntityTypeBuilder<ExternalSystemMethod> builder)
|
||||
{
|
||||
builder.HasKey(m => m.Id);
|
||||
@@ -66,6 +70,8 @@ public class ExternalSystemMethodConfiguration : IEntityTypeConfiguration<Extern
|
||||
|
||||
public class DatabaseConnectionDefinitionConfiguration : IEntityTypeConfiguration<DatabaseConnectionDefinition>
|
||||
{
|
||||
/// <summary>Applies the EF Core entity type configuration for <see cref="DatabaseConnectionDefinition"/>.</summary>
|
||||
/// <param name="builder">The entity type builder to configure.</param>
|
||||
public void Configure(EntityTypeBuilder<DatabaseConnectionDefinition> builder)
|
||||
{
|
||||
builder.HasKey(d => d.Id);
|
||||
|
||||
@@ -6,6 +6,8 @@ namespace ScadaLink.ConfigurationDatabase.Configurations;
|
||||
|
||||
public class ApiKeyConfiguration : IEntityTypeConfiguration<ApiKey>
|
||||
{
|
||||
/// <summary>Configures the EF Core mapping for the <see cref="ApiKey"/> entity.</summary>
|
||||
/// <param name="builder">Entity type builder used to apply the configuration.</param>
|
||||
public void Configure(EntityTypeBuilder<ApiKey> builder)
|
||||
{
|
||||
builder.HasKey(k => k.Id);
|
||||
@@ -28,6 +30,8 @@ public class ApiKeyConfiguration : IEntityTypeConfiguration<ApiKey>
|
||||
|
||||
public class ApiMethodConfiguration : IEntityTypeConfiguration<ApiMethod>
|
||||
{
|
||||
/// <summary>Configures the EF Core mapping for the <see cref="ApiMethod"/> entity.</summary>
|
||||
/// <param name="builder">Entity type builder used to apply the configuration.</param>
|
||||
public void Configure(EntityTypeBuilder<ApiMethod> builder)
|
||||
{
|
||||
builder.HasKey(m => m.Id);
|
||||
|
||||
@@ -8,6 +8,8 @@ namespace ScadaLink.ConfigurationDatabase.Configurations;
|
||||
|
||||
public class InstanceConfiguration : IEntityTypeConfiguration<Instance>
|
||||
{
|
||||
/// <summary>Configures the EF Core mapping for <see cref="Instance"/>.</summary>
|
||||
/// <param name="builder">The entity type builder.</param>
|
||||
public void Configure(EntityTypeBuilder<Instance> builder)
|
||||
{
|
||||
builder.HasKey(i => i.Id);
|
||||
@@ -57,6 +59,8 @@ public class InstanceConfiguration : IEntityTypeConfiguration<Instance>
|
||||
|
||||
public class InstanceAttributeOverrideConfiguration : IEntityTypeConfiguration<InstanceAttributeOverride>
|
||||
{
|
||||
/// <summary>Configures the EF Core mapping for <see cref="InstanceAttributeOverride"/>.</summary>
|
||||
/// <param name="builder">The entity type builder.</param>
|
||||
public void Configure(EntityTypeBuilder<InstanceAttributeOverride> builder)
|
||||
{
|
||||
builder.HasKey(o => o.Id);
|
||||
@@ -74,6 +78,8 @@ public class InstanceAttributeOverrideConfiguration : IEntityTypeConfiguration<I
|
||||
|
||||
public class InstanceAlarmOverrideConfiguration : IEntityTypeConfiguration<InstanceAlarmOverride>
|
||||
{
|
||||
/// <summary>Configures the EF Core mapping for <see cref="InstanceAlarmOverride"/>.</summary>
|
||||
/// <param name="builder">The entity type builder.</param>
|
||||
public void Configure(EntityTypeBuilder<InstanceAlarmOverride> builder)
|
||||
{
|
||||
builder.HasKey(o => o.Id);
|
||||
@@ -91,6 +97,8 @@ public class InstanceAlarmOverrideConfiguration : IEntityTypeConfiguration<Insta
|
||||
|
||||
public class InstanceConnectionBindingConfiguration : IEntityTypeConfiguration<InstanceConnectionBinding>
|
||||
{
|
||||
/// <summary>Configures the EF Core mapping for <see cref="InstanceConnectionBinding"/>.</summary>
|
||||
/// <param name="builder">The entity type builder.</param>
|
||||
public void Configure(EntityTypeBuilder<InstanceConnectionBinding> builder)
|
||||
{
|
||||
builder.HasKey(b => b.Id);
|
||||
@@ -110,6 +118,8 @@ public class InstanceConnectionBindingConfiguration : IEntityTypeConfiguration<I
|
||||
|
||||
public class AreaConfiguration : IEntityTypeConfiguration<Area>
|
||||
{
|
||||
/// <summary>Configures the EF Core mapping for <see cref="Area"/>.</summary>
|
||||
/// <param name="builder">The entity type builder.</param>
|
||||
public void Configure(EntityTypeBuilder<Area> builder)
|
||||
{
|
||||
builder.HasKey(a => a.Id);
|
||||
|
||||
@@ -6,6 +6,8 @@ namespace ScadaLink.ConfigurationDatabase.Configurations;
|
||||
|
||||
public class NotificationListConfiguration : IEntityTypeConfiguration<NotificationList>
|
||||
{
|
||||
/// <summary>Configures the EF Core mapping for <see cref="NotificationList"/>.</summary>
|
||||
/// <param name="builder">The entity type builder.</param>
|
||||
public void Configure(EntityTypeBuilder<NotificationList> builder)
|
||||
{
|
||||
builder.HasKey(n => n.Id);
|
||||
@@ -30,6 +32,8 @@ public class NotificationListConfiguration : IEntityTypeConfiguration<Notificati
|
||||
|
||||
public class NotificationRecipientConfiguration : IEntityTypeConfiguration<NotificationRecipient>
|
||||
{
|
||||
/// <summary>Configures the EF Core mapping for <see cref="NotificationRecipient"/>.</summary>
|
||||
/// <param name="builder">The entity type builder.</param>
|
||||
public void Configure(EntityTypeBuilder<NotificationRecipient> builder)
|
||||
{
|
||||
builder.HasKey(r => r.Id);
|
||||
@@ -46,6 +50,8 @@ public class NotificationRecipientConfiguration : IEntityTypeConfiguration<Notif
|
||||
|
||||
public class SmtpConfigurationConfiguration : IEntityTypeConfiguration<SmtpConfiguration>
|
||||
{
|
||||
/// <summary>Configures the EF Core mapping for <see cref="SmtpConfiguration"/>.</summary>
|
||||
/// <param name="builder">The entity type builder.</param>
|
||||
public void Configure(EntityTypeBuilder<SmtpConfiguration> builder)
|
||||
{
|
||||
builder.HasKey(s => s.Id);
|
||||
|
||||
@@ -11,6 +11,8 @@ namespace ScadaLink.ConfigurationDatabase.Configurations;
|
||||
/// </summary>
|
||||
public class NotificationOutboxConfiguration : IEntityTypeConfiguration<Notification>
|
||||
{
|
||||
/// <summary>Configures the EF Core entity type mapping for <see cref="Notification"/>.</summary>
|
||||
/// <param name="builder">The entity type builder to configure.</param>
|
||||
public void Configure(EntityTypeBuilder<Notification> builder)
|
||||
{
|
||||
builder.HasKey(n => n.NotificationId);
|
||||
|
||||
@@ -6,6 +6,8 @@ namespace ScadaLink.ConfigurationDatabase.Configurations;
|
||||
|
||||
public class SharedScriptConfiguration : IEntityTypeConfiguration<SharedScript>
|
||||
{
|
||||
/// <summary>Configures the EF Core mapping for the <see cref="SharedScript"/> entity.</summary>
|
||||
/// <param name="builder">Entity type builder used to apply the configuration.</param>
|
||||
public void Configure(EntityTypeBuilder<SharedScript> builder)
|
||||
{
|
||||
builder.HasKey(s => s.Id);
|
||||
|
||||
@@ -7,6 +7,10 @@ namespace ScadaLink.ConfigurationDatabase.Configurations;
|
||||
|
||||
public class LdapGroupMappingConfiguration : IEntityTypeConfiguration<LdapGroupMapping>
|
||||
{
|
||||
/// <summary>
|
||||
/// Configures the EF Core entity type mapping for <see cref="LdapGroupMapping"/>.
|
||||
/// </summary>
|
||||
/// <param name="builder">The entity type builder to configure.</param>
|
||||
public void Configure(EntityTypeBuilder<LdapGroupMapping> builder)
|
||||
{
|
||||
builder.HasKey(m => m.Id);
|
||||
@@ -32,6 +36,10 @@ public class LdapGroupMappingConfiguration : IEntityTypeConfiguration<LdapGroupM
|
||||
|
||||
public class SiteScopeRuleConfiguration : IEntityTypeConfiguration<SiteScopeRule>
|
||||
{
|
||||
/// <summary>
|
||||
/// Configures the EF Core entity type mapping for <see cref="SiteScopeRule"/>.
|
||||
/// </summary>
|
||||
/// <param name="builder">The entity type builder to configure.</param>
|
||||
public void Configure(EntityTypeBuilder<SiteScopeRule> builder)
|
||||
{
|
||||
builder.HasKey(r => r.Id);
|
||||
|
||||
@@ -14,6 +14,10 @@ namespace ScadaLink.ConfigurationDatabase.Configurations;
|
||||
/// </summary>
|
||||
public class SiteCallEntityTypeConfiguration : IEntityTypeConfiguration<SiteCall>
|
||||
{
|
||||
/// <summary>
|
||||
/// Configures the EF Core entity type mapping for <see cref="SiteCall"/>.
|
||||
/// </summary>
|
||||
/// <param name="builder">The entity type builder to configure.</param>
|
||||
public void Configure(EntityTypeBuilder<SiteCall> builder)
|
||||
{
|
||||
builder.ToTable("SiteCalls");
|
||||
|
||||
@@ -6,6 +6,8 @@ namespace ScadaLink.ConfigurationDatabase.Configurations;
|
||||
|
||||
public class SiteConfiguration : IEntityTypeConfiguration<Site>
|
||||
{
|
||||
/// <summary>Configures the EF Core mapping for <see cref="Site"/>.</summary>
|
||||
/// <param name="builder">The entity type builder.</param>
|
||||
public void Configure(EntityTypeBuilder<Site> builder)
|
||||
{
|
||||
builder.HasKey(s => s.Id);
|
||||
@@ -33,6 +35,8 @@ public class SiteConfiguration : IEntityTypeConfiguration<Site>
|
||||
|
||||
public class DataConnectionConfiguration : IEntityTypeConfiguration<DataConnection>
|
||||
{
|
||||
/// <summary>Configures the EF Core mapping for <see cref="DataConnection"/>.</summary>
|
||||
/// <param name="builder">The entity type builder.</param>
|
||||
public void Configure(EntityTypeBuilder<DataConnection> builder)
|
||||
{
|
||||
builder.HasKey(d => d.Id);
|
||||
|
||||
@@ -6,6 +6,8 @@ namespace ScadaLink.ConfigurationDatabase.Configurations;
|
||||
|
||||
public class TemplateConfiguration : IEntityTypeConfiguration<Template>
|
||||
{
|
||||
/// <summary>Configures the EF Core mapping for <see cref="Template"/>.</summary>
|
||||
/// <param name="builder">The entity type builder.</param>
|
||||
public void Configure(EntityTypeBuilder<Template> builder)
|
||||
{
|
||||
builder.HasKey(t => t.Id);
|
||||
@@ -61,6 +63,8 @@ public class TemplateConfiguration : IEntityTypeConfiguration<Template>
|
||||
|
||||
public class TemplateAttributeConfiguration : IEntityTypeConfiguration<TemplateAttribute>
|
||||
{
|
||||
/// <summary>Configures the EF Core mapping for <see cref="TemplateAttribute"/>.</summary>
|
||||
/// <param name="builder">The entity type builder.</param>
|
||||
public void Configure(EntityTypeBuilder<TemplateAttribute> builder)
|
||||
{
|
||||
builder.HasKey(a => a.Id);
|
||||
@@ -88,6 +92,8 @@ public class TemplateAttributeConfiguration : IEntityTypeConfiguration<TemplateA
|
||||
|
||||
public class TemplateAlarmConfiguration : IEntityTypeConfiguration<TemplateAlarm>
|
||||
{
|
||||
/// <summary>Configures the EF Core mapping for <see cref="TemplateAlarm"/>.</summary>
|
||||
/// <param name="builder">The entity type builder.</param>
|
||||
public void Configure(EntityTypeBuilder<TemplateAlarm> builder)
|
||||
{
|
||||
builder.HasKey(a => a.Id);
|
||||
@@ -112,6 +118,8 @@ public class TemplateAlarmConfiguration : IEntityTypeConfiguration<TemplateAlarm
|
||||
|
||||
public class TemplateScriptConfiguration : IEntityTypeConfiguration<TemplateScript>
|
||||
{
|
||||
/// <summary>Configures the EF Core mapping for <see cref="TemplateScript"/>.</summary>
|
||||
/// <param name="builder">The entity type builder.</param>
|
||||
public void Configure(EntityTypeBuilder<TemplateScript> builder)
|
||||
{
|
||||
builder.HasKey(s => s.Id);
|
||||
@@ -141,6 +149,8 @@ public class TemplateScriptConfiguration : IEntityTypeConfiguration<TemplateScri
|
||||
|
||||
public class TemplateCompositionConfiguration : IEntityTypeConfiguration<TemplateComposition>
|
||||
{
|
||||
/// <summary>Configures the EF Core mapping for <see cref="TemplateComposition"/>.</summary>
|
||||
/// <param name="builder">The entity type builder.</param>
|
||||
public void Configure(EntityTypeBuilder<TemplateComposition> builder)
|
||||
{
|
||||
builder.HasKey(c => c.Id);
|
||||
@@ -161,6 +171,8 @@ public class TemplateCompositionConfiguration : IEntityTypeConfiguration<Templat
|
||||
|
||||
public class TemplateFolderConfiguration : IEntityTypeConfiguration<TemplateFolder>
|
||||
{
|
||||
/// <summary>Configures the EF Core mapping for <see cref="TemplateFolder"/>.</summary>
|
||||
/// <param name="builder">The entity type builder.</param>
|
||||
public void Configure(EntityTypeBuilder<TemplateFolder> builder)
|
||||
{
|
||||
builder.HasKey(f => f.Id);
|
||||
|
||||
@@ -22,6 +22,10 @@ public class DesignTimeDbContextFactory : IDesignTimeDbContextFactory<ScadaLinkD
|
||||
private const string EnvironmentVariableName = "SCADALINK_DESIGNTIME_CONNECTIONSTRING";
|
||||
private const string ConfigurationKey = "ScadaLink:Database:ConfigurationDb";
|
||||
|
||||
/// <summary>
|
||||
/// Creates a <see cref="ScadaLinkDbContext"/> for design-time EF Core tooling.
|
||||
/// </summary>
|
||||
/// <param name="args">Arguments passed by the EF tooling (unused).</param>
|
||||
public ScadaLinkDbContext CreateDbContext(string[] args)
|
||||
{
|
||||
var configurationBuilder = new ConfigurationBuilder();
|
||||
|
||||
@@ -22,6 +22,10 @@ public sealed class EncryptedStringConverter : ValueConverter<string?, string?>
|
||||
/// <summary>The Data Protection purpose string shared by all encrypted configuration columns.</summary>
|
||||
public const string ProtectorPurpose = "ScadaLink.ConfigurationDatabase.EncryptedColumn";
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the converter with the data protector used to encrypt and decrypt column values.
|
||||
/// </summary>
|
||||
/// <param name="protector">The data protector scoped to the encrypted column purpose.</param>
|
||||
public EncryptedStringConverter(IDataProtector protector)
|
||||
: base(
|
||||
plaintext => plaintext == null ? null : protector.Protect(plaintext),
|
||||
|
||||
@@ -54,6 +54,9 @@ public sealed class AuditLogPartitionMaintenance : IPartitionMaintenance
|
||||
private readonly ScadaLinkDbContext _context;
|
||||
private readonly ILogger<AuditLogPartitionMaintenance> _logger;
|
||||
|
||||
/// <summary>Initializes the maintenance implementation with the database context and optional logger.</summary>
|
||||
/// <param name="context">The EF Core database context used to execute partition-management SQL.</param>
|
||||
/// <param name="logger">Optional logger; defaults to <see cref="NullLogger{T}"/> when null.</param>
|
||||
public AuditLogPartitionMaintenance(
|
||||
ScadaLinkDbContext context,
|
||||
ILogger<AuditLogPartitionMaintenance>? logger = null)
|
||||
|
||||
@@ -28,20 +28,16 @@ public class AuditLogRepository : IAuditLogRepository
|
||||
private readonly ScadaLinkDbContext _context;
|
||||
private readonly ILogger<AuditLogRepository> _logger;
|
||||
|
||||
/// <summary>Initializes a new instance of the AuditLogRepository class.</summary>
|
||||
/// <param name="context">The database context.</param>
|
||||
/// <param name="logger">Optional logger instance.</param>
|
||||
public AuditLogRepository(ScadaLinkDbContext context, ILogger<AuditLogRepository>? logger = null)
|
||||
{
|
||||
_context = context ?? throw new ArgumentNullException(nameof(context));
|
||||
_logger = logger ?? NullLogger<AuditLogRepository>.Instance;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Issues a single <c>IF NOT EXISTS … INSERT INTO dbo.AuditLog (…) VALUES (…)</c>
|
||||
/// via <see cref="Microsoft.EntityFrameworkCore.RelationalDatabaseFacadeExtensions.ExecuteSqlInterpolatedAsync"/>.
|
||||
/// Bypasses the EF change tracker so the row never enters a tracked state and
|
||||
/// the enum-as-string conversion is done explicitly in C# (the columns are
|
||||
/// declared <c>varchar(32)</c> via <c>HasConversion<string>()</c> in
|
||||
/// <see cref="ScadaLink.ConfigurationDatabase.Configurations.AuditLogEntityTypeConfiguration"/>).
|
||||
/// </summary>
|
||||
/// <inheritdoc />
|
||||
public async Task InsertIfNotExistsAsync(AuditEvent evt, CancellationToken ct = default)
|
||||
{
|
||||
if (evt is null)
|
||||
@@ -93,14 +89,7 @@ VALUES
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds an <c>AsNoTracking</c> queryable over <see cref="AuditEvent"/>, applies
|
||||
/// every non-null filter predicate, and pages by keyset on
|
||||
/// <c>(OccurredAtUtc DESC, EventId DESC)</c>. The keyset clause is expressed
|
||||
/// directly (<c>occurred < after || (occurred == after && eventId.CompareTo(afterId) < 0)</c>)
|
||||
/// — EF Core 10 translates <see cref="Guid.CompareTo(Guid)"/> against SQL Server's
|
||||
/// <c>uniqueidentifier</c> sort order.
|
||||
/// </summary>
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<AuditEvent>> QueryAsync(
|
||||
AuditLogQueryFilter filter, AuditLogPaging paging, CancellationToken ct = default)
|
||||
{
|
||||
@@ -199,30 +188,7 @@ VALUES
|
||||
.ToListAsync(ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// M6-T4 production implementation of the drop-and-rebuild dance documented
|
||||
/// on <see cref="IAuditLogRepository.SwitchOutPartitionAsync"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// The staging table name is GUID-suffixed so concurrent purge attempts on
|
||||
/// different boundaries cannot collide. The staging schema is byte-identical
|
||||
/// to the live <c>AuditLog</c> table (same column types, lengths,
|
||||
/// nullability, and clustered-key shape) — SQL Server's
|
||||
/// <c>ALTER TABLE … SWITCH PARTITION</c> rejects any drift. Keep this CREATE
|
||||
/// in sync with both the migration that ships the live table
|
||||
/// (<c>20260520142214_AddAuditLogTable</c>) and
|
||||
/// <c>AuditLogEntityTypeConfiguration</c>.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// All five steps run inside an explicit transaction so the SWITCH +
|
||||
/// staging-DROP are atomic from the perspective of a consumer reading via
|
||||
/// snapshot isolation; the CATCH rolls back and runs an idempotent
|
||||
/// "rebuild UX_AuditLog_EventId if it doesn't exist" so a partial failure
|
||||
/// never leaves the live table without its idempotency-supporting unique
|
||||
/// index.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
/// <inheritdoc />
|
||||
public async Task<long> SwitchOutPartitionAsync(DateTime monthBoundary, CancellationToken ct = default)
|
||||
{
|
||||
// GUID-suffixed staging name: prevents collision with any concurrent
|
||||
@@ -371,27 +337,7 @@ VALUES
|
||||
return rowsDeleted;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the set of <c>pf_AuditLog_Month</c> boundaries whose partition's
|
||||
/// <c>MAX(OccurredAtUtc)</c> is strictly older than <paramref name="threshold"/>.
|
||||
/// Boundaries with empty partitions are excluded — purging an empty
|
||||
/// partition is wasted I/O.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// The CTE pulls every boundary value defined by the partition function and
|
||||
/// joins it (via <c>$PARTITION.pf_AuditLog_Month</c>) to the live AuditLog
|
||||
/// to compute per-partition <c>MAX(OccurredAtUtc)</c>. The outer filter
|
||||
/// keeps only those whose MAX is non-NULL (partition has rows) AND strictly
|
||||
/// less than the threshold (every row is past retention).
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Note: the query scans the live <c>OccurredAtUtc</c> column to compute
|
||||
/// the MAX per partition. With <c>IX_AuditLog_OccurredAtUtc</c> on the
|
||||
/// partition-aligned scheme this is a single index seek per partition; for
|
||||
/// 24 partitions and a daily purge cadence the cost is negligible.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<DateTime>> GetPartitionBoundariesOlderThanAsync(
|
||||
DateTime threshold,
|
||||
CancellationToken ct = default)
|
||||
@@ -451,31 +397,7 @@ VALUES
|
||||
return results;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// M7-T13 Bundle E — Health-dashboard Audit KPI tiles aggregate query.
|
||||
/// Single round-trip
|
||||
/// (<c>SELECT COUNT_BIG(*) AS Total, SUM(CASE WHEN Status IN (...) THEN 1 ELSE 0 END) AS Errors</c>)
|
||||
/// over the trailing <paramref name="window"/> anchored at
|
||||
/// <paramref name="nowUtc"/>. Returns a snapshot with
|
||||
/// <see cref="AuditLogKpiSnapshot.BacklogTotal"/> left at zero — the service
|
||||
/// layer composes that in from
|
||||
/// <see cref="ScadaLink.HealthMonitoring.ICentralHealthAggregator"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// Why one query, not two: keeping the numerator + denominator in the same
|
||||
/// scan means the error rate the UI displays is computed from a consistent
|
||||
/// snapshot. With two separate queries a row could be inserted between
|
||||
/// them, inflating the denominator past the numerator (or vice-versa) and
|
||||
/// briefly producing a misleading percentage.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// "Error" rows are <c>Failed</c>, <c>Parked</c>, or <c>Discarded</c> — see
|
||||
/// <see cref="IAuditLogRepository.GetKpiSnapshotAsync"/> for the rationale.
|
||||
/// We pass the three discriminator strings as separate parameters rather
|
||||
/// than building an IN-list to keep the prepared statement cache-friendly.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
/// <inheritdoc />
|
||||
public async Task<AuditLogKpiSnapshot> GetKpiSnapshotAsync(
|
||||
TimeSpan window,
|
||||
DateTime? nowUtc = null,
|
||||
@@ -573,37 +495,7 @@ VALUES
|
||||
// practice.
|
||||
private const int ExecutionChainMaxDepth = 32;
|
||||
|
||||
/// <summary>
|
||||
/// Audit Log ParentExecutionId (Task 8) — returns the whole execution chain
|
||||
/// containing <paramref name="executionId"/>, regardless of entry point.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// Two phases. <b>Walk up:</b> an iterative
|
||||
/// <c>SELECT TOP 1 ParentExecutionId … WHERE ExecutionId = @cur AND ParentExecutionId IS NOT NULL</c>
|
||||
/// climbs from the supplied node to the root — the last execution id with no
|
||||
/// parent. The loop is capped at <see cref="ExecutionChainMaxDepth"/>
|
||||
/// iterations; a purged/missing parent simply ends the climb early. <b>Walk
|
||||
/// down:</b> a recursive CTE over a DISTINCT
|
||||
/// <c>(ExecutionId, ParentExecutionId)</c> edge set, seeded at the root edge
|
||||
/// and joining <c>edge.ParentExecutionId = chain.ExecutionId</c> to
|
||||
/// enumerate every descendant. Recursing over edges rather than raw rows
|
||||
/// keeps the recursion one path wide per execution. It is bounded by
|
||||
/// <c>OPTION (MAXRECURSION ...)</c> at <see cref="ExecutionChainMaxDepth"/>
|
||||
/// — corrupt cyclic data raises a <see cref="SqlException"/> (msg 530)
|
||||
/// rather than spinning.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The chain's full execution-id set is every edge's <c>ExecutionId</c>
|
||||
/// unioned with its non-null <c>ParentExecutionId</c>, so an execution
|
||||
/// referenced only as a parent — a "stub" that emitted no rows of its own,
|
||||
/// and therefore owns no edge of its own — is still included. The final
|
||||
/// projection LEFT JOINs that id set back to <c>AuditLog</c> and
|
||||
/// <c>GROUP BY</c>s, so a stub yields a node with <c>RowCount = 0</c> and
|
||||
/// empty/null aggregates. The query is SELECT-only
|
||||
/// (the audit writer role grants no UPDATE/DELETE — reads are unrestricted).
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<ExecutionTreeNode>> GetExecutionTreeAsync(
|
||||
Guid executionId,
|
||||
CancellationToken ct = default)
|
||||
@@ -768,14 +660,7 @@ VALUES
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Distinct non-null <c>SourceNode</c> values for the Audit Log page's
|
||||
/// Node filter dropdown. EF Core translates this to
|
||||
/// <c>SELECT DISTINCT SourceNode FROM AuditLog WHERE SourceNode IS NOT NULL ORDER BY SourceNode</c>
|
||||
/// — a single index-less scan, but the column is bounded (one entry per
|
||||
/// node in the cluster, currently <10) and the Central UI caches the
|
||||
/// result for ~60s, so a periodic scan is acceptable.
|
||||
/// </summary>
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<string>> GetDistinctSourceNodesAsync(CancellationToken ct = default)
|
||||
{
|
||||
return await _context.Set<AuditEvent>()
|
||||
|
||||
@@ -12,11 +12,16 @@ public class CentralUiRepository : ICentralUiRepository
|
||||
{
|
||||
private readonly ScadaLinkDbContext _context;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CentralUiRepository"/> class.
|
||||
/// </summary>
|
||||
/// <param name="context">The EF Core database context.</param>
|
||||
public CentralUiRepository(ScadaLinkDbContext context)
|
||||
{
|
||||
_context = context ?? throw new ArgumentNullException(nameof(context));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<Site>> GetAllSitesAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _context.Sites
|
||||
@@ -25,6 +30,7 @@ public class CentralUiRepository : ICentralUiRepository
|
||||
.ToListAsync(cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<DataConnection>> GetDataConnectionsBySiteIdAsync(int siteId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _context.DataConnections
|
||||
@@ -34,6 +40,7 @@ public class CentralUiRepository : ICentralUiRepository
|
||||
.ToListAsync(cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<DataConnection>> GetAllDataConnectionsAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _context.DataConnections
|
||||
@@ -42,6 +49,7 @@ public class CentralUiRepository : ICentralUiRepository
|
||||
.ToListAsync(cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<Template>> GetTemplateTreeAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _context.Templates
|
||||
@@ -55,6 +63,7 @@ public class CentralUiRepository : ICentralUiRepository
|
||||
.ToListAsync(cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<Instance>> GetInstancesFilteredAsync(
|
||||
int? siteId = null,
|
||||
int? templateId = null,
|
||||
@@ -77,6 +86,7 @@ public class CentralUiRepository : ICentralUiRepository
|
||||
.ToListAsync(cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<DeploymentRecord>> GetRecentDeploymentsAsync(int count, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _context.DeploymentRecords
|
||||
@@ -86,6 +96,7 @@ public class CentralUiRepository : ICentralUiRepository
|
||||
.ToListAsync(cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<Area>> GetAreaTreeBySiteIdAsync(int siteId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _context.Areas
|
||||
@@ -96,6 +107,7 @@ public class CentralUiRepository : ICentralUiRepository
|
||||
.ToListAsync(cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<(IReadOnlyList<AuditLogEntry> Entries, int TotalCount)> GetAuditLogEntriesAsync(
|
||||
string? user = null,
|
||||
string? entityType = null,
|
||||
@@ -146,6 +158,7 @@ public class CentralUiRepository : ICentralUiRepository
|
||||
return (entries, totalCount);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _context.SaveChangesAsync(cancellationToken);
|
||||
|
||||
@@ -16,6 +16,10 @@ public class DeploymentManagerRepository : IDeploymentManagerRepository
|
||||
{
|
||||
private readonly ScadaLinkDbContext _dbContext;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the DeploymentManagerRepository class.
|
||||
/// </summary>
|
||||
/// <param name="dbContext">The database context for accessing deployment data.</param>
|
||||
public DeploymentManagerRepository(ScadaLinkDbContext dbContext)
|
||||
{
|
||||
_dbContext = dbContext ?? throw new ArgumentNullException(nameof(dbContext));
|
||||
@@ -23,11 +27,13 @@ public class DeploymentManagerRepository : IDeploymentManagerRepository
|
||||
|
||||
// --- DeploymentRecord ---
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<DeploymentRecord?> GetDeploymentRecordByIdAsync(int id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _dbContext.DeploymentRecords.FindAsync([id], cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<DeploymentRecord>> GetAllDeploymentRecordsAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _dbContext.DeploymentRecords
|
||||
@@ -35,6 +41,7 @@ public class DeploymentManagerRepository : IDeploymentManagerRepository
|
||||
.ToListAsync(cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<DeploymentRecord>> GetDeploymentsByInstanceIdAsync(int instanceId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _dbContext.DeploymentRecords
|
||||
@@ -43,10 +50,7 @@ public class DeploymentManagerRepository : IDeploymentManagerRepository
|
||||
.ToListAsync(cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the most recent deployment record for an instance (current deployment status).
|
||||
/// Used for staleness detection by comparing revision hashes.
|
||||
/// </summary>
|
||||
/// <inheritdoc />
|
||||
public async Task<DeploymentRecord?> GetCurrentDeploymentStatusAsync(int instanceId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _dbContext.DeploymentRecords
|
||||
@@ -55,28 +59,27 @@ public class DeploymentManagerRepository : IDeploymentManagerRepository
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<DeploymentRecord?> GetDeploymentByDeploymentIdAsync(string deploymentId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _dbContext.DeploymentRecords
|
||||
.FirstOrDefaultAsync(d => d.DeploymentId == deploymentId, cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task AddDeploymentRecordAsync(DeploymentRecord record, CancellationToken cancellationToken = default)
|
||||
{
|
||||
await _dbContext.DeploymentRecords.AddAsync(record, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates a deployment record. Uses optimistic concurrency on deployment status records —
|
||||
/// EF Core's change tracking will detect concurrent modifications via the row version/concurrency token
|
||||
/// configured in the entity configuration.
|
||||
/// </summary>
|
||||
/// <inheritdoc />
|
||||
public Task UpdateDeploymentRecordAsync(DeploymentRecord record, CancellationToken cancellationToken = default)
|
||||
{
|
||||
_dbContext.DeploymentRecords.Update(record);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task DeleteDeploymentRecordAsync(int id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var record = _dbContext.DeploymentRecords.Local.FirstOrDefault(d => d.Id == id);
|
||||
@@ -95,11 +98,13 @@ public class DeploymentManagerRepository : IDeploymentManagerRepository
|
||||
|
||||
// --- SystemArtifactDeploymentRecord ---
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<SystemArtifactDeploymentRecord?> GetSystemArtifactDeploymentByIdAsync(int id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _dbContext.SystemArtifactDeploymentRecords.FindAsync([id], cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<SystemArtifactDeploymentRecord>> GetAllSystemArtifactDeploymentsAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _dbContext.SystemArtifactDeploymentRecords
|
||||
@@ -107,17 +112,20 @@ public class DeploymentManagerRepository : IDeploymentManagerRepository
|
||||
.ToListAsync(cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task AddSystemArtifactDeploymentAsync(SystemArtifactDeploymentRecord record, CancellationToken cancellationToken = default)
|
||||
{
|
||||
await _dbContext.SystemArtifactDeploymentRecords.AddAsync(record, cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task UpdateSystemArtifactDeploymentAsync(SystemArtifactDeploymentRecord record, CancellationToken cancellationToken = default)
|
||||
{
|
||||
_dbContext.SystemArtifactDeploymentRecords.Update(record);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task DeleteSystemArtifactDeploymentAsync(int id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var record = _dbContext.SystemArtifactDeploymentRecords.Local.FirstOrDefault(d => d.Id == id);
|
||||
@@ -136,23 +144,27 @@ public class DeploymentManagerRepository : IDeploymentManagerRepository
|
||||
|
||||
// --- WP-8: DeployedConfigSnapshot ---
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<DeployedConfigSnapshot?> GetDeployedSnapshotByInstanceIdAsync(int instanceId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _dbContext.Set<DeployedConfigSnapshot>()
|
||||
.FirstOrDefaultAsync(s => s.InstanceId == instanceId, cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task AddDeployedSnapshotAsync(DeployedConfigSnapshot snapshot, CancellationToken cancellationToken = default)
|
||||
{
|
||||
await _dbContext.Set<DeployedConfigSnapshot>().AddAsync(snapshot, cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task UpdateDeployedSnapshotAsync(DeployedConfigSnapshot snapshot, CancellationToken cancellationToken = default)
|
||||
{
|
||||
_dbContext.Set<DeployedConfigSnapshot>().Update(snapshot);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task DeleteDeployedSnapshotAsync(int instanceId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var snapshot = await _dbContext.Set<DeployedConfigSnapshot>()
|
||||
@@ -165,6 +177,7 @@ public class DeploymentManagerRepository : IDeploymentManagerRepository
|
||||
|
||||
// --- Instance lookups for deployment pipeline ---
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<Instance?> GetInstanceByIdAsync(int instanceId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _dbContext.Set<Instance>()
|
||||
@@ -175,6 +188,7 @@ public class DeploymentManagerRepository : IDeploymentManagerRepository
|
||||
.FirstOrDefaultAsync(i => i.Id == instanceId, cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<Instance?> GetInstanceByUniqueNameAsync(string uniqueName, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _dbContext.Set<Instance>()
|
||||
@@ -185,12 +199,14 @@ public class DeploymentManagerRepository : IDeploymentManagerRepository
|
||||
.FirstOrDefaultAsync(i => i.UniqueName == uniqueName, cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task UpdateInstanceAsync(Instance instance, CancellationToken cancellationToken = default)
|
||||
{
|
||||
_dbContext.Set<Instance>().Update(instance);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task DeleteInstanceAsync(int instanceId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
// DeploymentRecords have a Restrict FK to Instance — remove them
|
||||
@@ -212,6 +228,7 @@ public class DeploymentManagerRepository : IDeploymentManagerRepository
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _dbContext.SaveChangesAsync(cancellationToken);
|
||||
|
||||
@@ -8,38 +8,50 @@ public class ExternalSystemRepository : IExternalSystemRepository
|
||||
{
|
||||
private readonly ScadaLinkDbContext _context;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the ExternalSystemRepository class.
|
||||
/// </summary>
|
||||
/// <param name="context">The database context for accessing external system data.</param>
|
||||
public ExternalSystemRepository(ScadaLinkDbContext context)
|
||||
{
|
||||
_context = context ?? throw new ArgumentNullException(nameof(context));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<ExternalSystemDefinition?> GetExternalSystemByIdAsync(int id, CancellationToken cancellationToken = default)
|
||||
=> await _context.Set<ExternalSystemDefinition>().FindAsync(new object[] { id }, cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
// ExternalSystemGateway-011: genuine name-keyed query (server-side WHERE) so the
|
||||
// gateway's hot-path resolution does not fetch every system and filter in memory.
|
||||
public async Task<ExternalSystemDefinition?> GetExternalSystemByNameAsync(string name, CancellationToken cancellationToken = default)
|
||||
=> await _context.Set<ExternalSystemDefinition>()
|
||||
.FirstOrDefaultAsync(s => s.Name == name, cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<ExternalSystemDefinition>> GetAllExternalSystemsAsync(CancellationToken cancellationToken = default)
|
||||
=> await _context.Set<ExternalSystemDefinition>().ToListAsync(cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task AddExternalSystemAsync(ExternalSystemDefinition definition, CancellationToken cancellationToken = default)
|
||||
=> await _context.Set<ExternalSystemDefinition>().AddAsync(definition, cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task UpdateExternalSystemAsync(ExternalSystemDefinition definition, CancellationToken cancellationToken = default)
|
||||
{ _context.Set<ExternalSystemDefinition>().Update(definition); return Task.CompletedTask; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task DeleteExternalSystemAsync(int id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var entity = await GetExternalSystemByIdAsync(id, cancellationToken);
|
||||
if (entity != null) _context.Set<ExternalSystemDefinition>().Remove(entity);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<ExternalSystemMethod?> GetExternalSystemMethodByIdAsync(int id, CancellationToken cancellationToken = default)
|
||||
=> await _context.Set<ExternalSystemMethod>().FindAsync(new object[] { id }, cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
// ExternalSystemGateway-011: genuine name-keyed query scoped to the parent system.
|
||||
public async Task<ExternalSystemMethod?> GetMethodByNameAsync(int externalSystemId, string methodName, CancellationToken cancellationToken = default)
|
||||
=> await _context.Set<ExternalSystemMethod>()
|
||||
@@ -47,44 +59,55 @@ public class ExternalSystemRepository : IExternalSystemRepository
|
||||
m => m.ExternalSystemDefinitionId == externalSystemId && m.Name == methodName,
|
||||
cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<ExternalSystemMethod>> GetMethodsByExternalSystemIdAsync(int externalSystemId, CancellationToken cancellationToken = default)
|
||||
=> await _context.Set<ExternalSystemMethod>().Where(m => m.ExternalSystemDefinitionId == externalSystemId).ToListAsync(cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task AddExternalSystemMethodAsync(ExternalSystemMethod method, CancellationToken cancellationToken = default)
|
||||
=> await _context.Set<ExternalSystemMethod>().AddAsync(method, cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task UpdateExternalSystemMethodAsync(ExternalSystemMethod method, CancellationToken cancellationToken = default)
|
||||
{ _context.Set<ExternalSystemMethod>().Update(method); return Task.CompletedTask; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task DeleteExternalSystemMethodAsync(int id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var entity = await GetExternalSystemMethodByIdAsync(id, cancellationToken);
|
||||
if (entity != null) _context.Set<ExternalSystemMethod>().Remove(entity);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<DatabaseConnectionDefinition?> GetDatabaseConnectionByIdAsync(int id, CancellationToken cancellationToken = default)
|
||||
=> await _context.Set<DatabaseConnectionDefinition>().FindAsync(new object[] { id }, cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
// ExternalSystemGateway-011: genuine name-keyed query (server-side WHERE).
|
||||
public async Task<DatabaseConnectionDefinition?> GetDatabaseConnectionByNameAsync(string name, CancellationToken cancellationToken = default)
|
||||
=> await _context.Set<DatabaseConnectionDefinition>()
|
||||
.FirstOrDefaultAsync(c => c.Name == name, cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<DatabaseConnectionDefinition>> GetAllDatabaseConnectionsAsync(CancellationToken cancellationToken = default)
|
||||
=> await _context.Set<DatabaseConnectionDefinition>().ToListAsync(cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task AddDatabaseConnectionAsync(DatabaseConnectionDefinition definition, CancellationToken cancellationToken = default)
|
||||
=> await _context.Set<DatabaseConnectionDefinition>().AddAsync(definition, cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task UpdateDatabaseConnectionAsync(DatabaseConnectionDefinition definition, CancellationToken cancellationToken = default)
|
||||
{ _context.Set<DatabaseConnectionDefinition>().Update(definition); return Task.CompletedTask; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task DeleteDatabaseConnectionAsync(int id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var entity = await GetDatabaseConnectionByIdAsync(id, cancellationToken);
|
||||
if (entity != null) _context.Set<DatabaseConnectionDefinition>().Remove(entity);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
|
||||
=> await _context.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
|
||||
@@ -12,54 +12,60 @@ public class InboundApiRepository : IInboundApiRepository
|
||||
private readonly ScadaLinkDbContext _context;
|
||||
private readonly ILogger<InboundApiRepository> _logger;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the InboundApiRepository class.
|
||||
/// </summary>
|
||||
/// <param name="context">The database context for accessing inbound API data.</param>
|
||||
/// <param name="logger">Optional logger instance for warnings and diagnostics.</param>
|
||||
public InboundApiRepository(ScadaLinkDbContext context, ILogger<InboundApiRepository>? logger = null)
|
||||
{
|
||||
_context = context ?? throw new ArgumentNullException(nameof(context));
|
||||
_logger = logger ?? NullLogger<InboundApiRepository>.Instance;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<ApiKey?> GetApiKeyByIdAsync(int id, CancellationToken cancellationToken = default)
|
||||
=> await _context.Set<ApiKey>().FindAsync(new object[] { id }, cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<ApiKey>> GetAllApiKeysAsync(CancellationToken cancellationToken = default)
|
||||
=> await _context.Set<ApiKey>().ToListAsync(cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// ConfigurationDatabase-012: API keys are persisted only as a deterministic hash,
|
||||
/// never as plaintext, so this lookup hashes the supplied plaintext value and
|
||||
/// matches it against the stored <see cref="ApiKey.KeyHash"/> column. The
|
||||
/// unpeppered default hasher is used here because the repository has no access to
|
||||
/// the deployment pepper; the inbound-API authentication path does not use this
|
||||
/// method — it loads all keys and compares hashes constant-time in
|
||||
/// <c>ApiKeyValidator</c> with the configured (peppered) hasher.
|
||||
/// </summary>
|
||||
/// <inheritdoc />
|
||||
public async Task<ApiKey?> GetApiKeyByValueAsync(string keyValue, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var keyHash = ApiKeyHasher.Default.Hash(keyValue);
|
||||
return await _context.Set<ApiKey>().FirstOrDefaultAsync(k => k.KeyHash == keyHash, cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task AddApiKeyAsync(ApiKey apiKey, CancellationToken cancellationToken = default)
|
||||
=> await _context.Set<ApiKey>().AddAsync(apiKey, cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task UpdateApiKeyAsync(ApiKey apiKey, CancellationToken cancellationToken = default)
|
||||
{ _context.Set<ApiKey>().Update(apiKey); return Task.CompletedTask; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task DeleteApiKeyAsync(int id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var entity = await GetApiKeyByIdAsync(id, cancellationToken);
|
||||
if (entity != null) _context.Set<ApiKey>().Remove(entity);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<ApiMethod?> GetApiMethodByIdAsync(int id, CancellationToken cancellationToken = default)
|
||||
=> await _context.Set<ApiMethod>().FindAsync(new object[] { id }, cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<ApiMethod>> GetAllApiMethodsAsync(CancellationToken cancellationToken = default)
|
||||
=> await _context.Set<ApiMethod>().ToListAsync(cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<ApiMethod?> GetMethodByNameAsync(string name, CancellationToken cancellationToken = default)
|
||||
=> await _context.Set<ApiMethod>().FirstOrDefaultAsync(m => m.Name == name, cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<ApiKey>> GetApprovedKeysForMethodAsync(int methodId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var method = await _context.Set<ApiMethod>().FindAsync(new object[] { methodId }, cancellationToken);
|
||||
@@ -90,18 +96,22 @@ public class InboundApiRepository : IInboundApiRepository
|
||||
return await _context.Set<ApiKey>().Where(k => keyIds.Contains(k.Id)).ToListAsync(cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task AddApiMethodAsync(ApiMethod method, CancellationToken cancellationToken = default)
|
||||
=> await _context.Set<ApiMethod>().AddAsync(method, cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task UpdateApiMethodAsync(ApiMethod method, CancellationToken cancellationToken = default)
|
||||
{ _context.Set<ApiMethod>().Update(method); return Task.CompletedTask; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task DeleteApiMethodAsync(int id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var entity = await GetApiMethodByIdAsync(id, cancellationToken);
|
||||
if (entity != null) _context.Set<ApiMethod>().Remove(entity);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
|
||||
=> await _context.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
|
||||
@@ -22,11 +22,14 @@ public class NotificationOutboxRepository : INotificationOutboxRepository
|
||||
NotificationStatus.Discarded,
|
||||
};
|
||||
|
||||
/// <summary>Initializes a new instance of <see cref="NotificationOutboxRepository"/> with the given EF Core context.</summary>
|
||||
/// <param name="context">The EF Core database context.</param>
|
||||
public NotificationOutboxRepository(ScadaLinkDbContext context)
|
||||
{
|
||||
_context = context ?? throw new ArgumentNullException(nameof(context));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<bool> InsertIfNotExistsAsync(Notification n, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var exists = await _context.Notifications
|
||||
@@ -41,6 +44,7 @@ public class NotificationOutboxRepository : INotificationOutboxRepository
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<Notification>> GetDueAsync(
|
||||
DateTimeOffset now, int batchSize, CancellationToken cancellationToken = default)
|
||||
{
|
||||
@@ -54,15 +58,18 @@ public class NotificationOutboxRepository : INotificationOutboxRepository
|
||||
.ToListAsync(cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<Notification?> GetByIdAsync(string notificationId, CancellationToken cancellationToken = default)
|
||||
=> await _context.Notifications.FindAsync(new object[] { notificationId }, cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task UpdateAsync(Notification n, CancellationToken cancellationToken = default)
|
||||
{
|
||||
_context.Notifications.Update(n);
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<(IReadOnlyList<Notification> Rows, int TotalCount)> QueryAsync(
|
||||
NotificationOutboxFilter filter, int pageNumber, int pageSize, CancellationToken cancellationToken = default)
|
||||
{
|
||||
@@ -128,6 +135,7 @@ public class NotificationOutboxRepository : INotificationOutboxRepository
|
||||
return (rows, totalCount);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<int> DeleteTerminalOlderThanAsync(DateTimeOffset cutoff, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _context.Notifications
|
||||
@@ -135,6 +143,7 @@ public class NotificationOutboxRepository : INotificationOutboxRepository
|
||||
.ExecuteDeleteAsync(cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<NotificationKpiSnapshot> ComputeKpisAsync(
|
||||
DateTimeOffset stuckCutoff, DateTimeOffset deliveredSince, CancellationToken cancellationToken = default)
|
||||
{
|
||||
@@ -181,6 +190,7 @@ public class NotificationOutboxRepository : INotificationOutboxRepository
|
||||
OldestPendingAge: oldestPendingAge);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<SiteNotificationKpiSnapshot>> ComputePerSiteKpisAsync(
|
||||
DateTimeOffset stuckCutoff, DateTimeOffset deliveredSince, CancellationToken cancellationToken = default)
|
||||
{
|
||||
@@ -243,6 +253,7 @@ public class NotificationOutboxRepository : INotificationOutboxRepository
|
||||
.ToDictionaryAsync(x => x.Site, x => x.Count, cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
|
||||
=> await _context.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
|
||||
@@ -8,68 +8,87 @@ public class NotificationRepository : INotificationRepository
|
||||
{
|
||||
private readonly ScadaLinkDbContext _context;
|
||||
|
||||
/// <summary>Initializes a new instance of the NotificationRepository class.</summary>
|
||||
/// <param name="context">The database context.</param>
|
||||
public NotificationRepository(ScadaLinkDbContext context)
|
||||
{
|
||||
_context = context ?? throw new ArgumentNullException(nameof(context));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<NotificationList?> GetNotificationListByIdAsync(int id, CancellationToken cancellationToken = default)
|
||||
=> await _context.Set<NotificationList>().FindAsync(new object[] { id }, cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<NotificationList>> GetAllNotificationListsAsync(CancellationToken cancellationToken = default)
|
||||
=> await _context.Set<NotificationList>().Include(n => n.Recipients).ToListAsync(cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<NotificationList?> GetListByNameAsync(string name, CancellationToken cancellationToken = default)
|
||||
=> await _context.Set<NotificationList>().FirstOrDefaultAsync(l => l.Name == name, cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task AddNotificationListAsync(NotificationList list, CancellationToken cancellationToken = default)
|
||||
=> await _context.Set<NotificationList>().AddAsync(list, cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task UpdateNotificationListAsync(NotificationList list, CancellationToken cancellationToken = default)
|
||||
{ _context.Set<NotificationList>().Update(list); return Task.CompletedTask; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task DeleteNotificationListAsync(int id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var entity = await GetNotificationListByIdAsync(id, cancellationToken);
|
||||
if (entity != null) _context.Set<NotificationList>().Remove(entity);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<NotificationRecipient?> GetRecipientByIdAsync(int id, CancellationToken cancellationToken = default)
|
||||
=> await _context.Set<NotificationRecipient>().FindAsync(new object[] { id }, cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<NotificationRecipient>> GetRecipientsByListIdAsync(int notificationListId, CancellationToken cancellationToken = default)
|
||||
=> await _context.Set<NotificationRecipient>().Where(r => r.NotificationListId == notificationListId).ToListAsync(cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task AddRecipientAsync(NotificationRecipient recipient, CancellationToken cancellationToken = default)
|
||||
=> await _context.Set<NotificationRecipient>().AddAsync(recipient, cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task UpdateRecipientAsync(NotificationRecipient recipient, CancellationToken cancellationToken = default)
|
||||
{ _context.Set<NotificationRecipient>().Update(recipient); return Task.CompletedTask; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task DeleteRecipientAsync(int id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var entity = await GetRecipientByIdAsync(id, cancellationToken);
|
||||
if (entity != null) _context.Set<NotificationRecipient>().Remove(entity);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<SmtpConfiguration?> GetSmtpConfigurationByIdAsync(int id, CancellationToken cancellationToken = default)
|
||||
=> await _context.Set<SmtpConfiguration>().FindAsync(new object[] { id }, cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<SmtpConfiguration>> GetAllSmtpConfigurationsAsync(CancellationToken cancellationToken = default)
|
||||
=> await _context.Set<SmtpConfiguration>().ToListAsync(cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task AddSmtpConfigurationAsync(SmtpConfiguration configuration, CancellationToken cancellationToken = default)
|
||||
=> await _context.Set<SmtpConfiguration>().AddAsync(configuration, cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task UpdateSmtpConfigurationAsync(SmtpConfiguration configuration, CancellationToken cancellationToken = default)
|
||||
{ _context.Set<SmtpConfiguration>().Update(configuration); return Task.CompletedTask; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task DeleteSmtpConfigurationAsync(int id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var entity = await GetSmtpConfigurationByIdAsync(id, cancellationToken);
|
||||
if (entity != null) _context.Set<SmtpConfiguration>().Remove(entity);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
|
||||
=> await _context.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
|
||||
@@ -8,6 +8,10 @@ public class SecurityRepository : ISecurityRepository
|
||||
{
|
||||
private readonly ScadaLinkDbContext _context;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the SecurityRepository.
|
||||
/// </summary>
|
||||
/// <param name="context">The database context.</param>
|
||||
public SecurityRepository(ScadaLinkDbContext context)
|
||||
{
|
||||
_context = context ?? throw new ArgumentNullException(nameof(context));
|
||||
@@ -15,16 +19,19 @@ public class SecurityRepository : ISecurityRepository
|
||||
|
||||
// LdapGroupMapping
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<LdapGroupMapping?> GetMappingByIdAsync(int id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _context.LdapGroupMappings.FindAsync(new object[] { id }, cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<LdapGroupMapping>> GetAllMappingsAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _context.LdapGroupMappings.ToListAsync(cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<LdapGroupMapping>> GetMappingsByRoleAsync(string role, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _context.LdapGroupMappings
|
||||
@@ -32,17 +39,20 @@ public class SecurityRepository : ISecurityRepository
|
||||
.ToListAsync(cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task AddMappingAsync(LdapGroupMapping mapping, CancellationToken cancellationToken = default)
|
||||
{
|
||||
await _context.LdapGroupMappings.AddAsync(mapping, cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task UpdateMappingAsync(LdapGroupMapping mapping, CancellationToken cancellationToken = default)
|
||||
{
|
||||
_context.LdapGroupMappings.Update(mapping);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task DeleteMappingAsync(int id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var mapping = await _context.LdapGroupMappings.FindAsync(new object[] { id }, cancellationToken);
|
||||
@@ -54,11 +64,13 @@ public class SecurityRepository : ISecurityRepository
|
||||
|
||||
// SiteScopeRule
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<SiteScopeRule?> GetScopeRuleByIdAsync(int id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _context.SiteScopeRules.FindAsync(new object[] { id }, cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<SiteScopeRule>> GetScopeRulesForMappingAsync(int ldapGroupMappingId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _context.SiteScopeRules
|
||||
@@ -66,17 +78,20 @@ public class SecurityRepository : ISecurityRepository
|
||||
.ToListAsync(cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task AddScopeRuleAsync(SiteScopeRule rule, CancellationToken cancellationToken = default)
|
||||
{
|
||||
await _context.SiteScopeRules.AddAsync(rule, cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task UpdateScopeRuleAsync(SiteScopeRule rule, CancellationToken cancellationToken = default)
|
||||
{
|
||||
_context.SiteScopeRules.Update(rule);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task DeleteScopeRuleAsync(int id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var rule = await _context.SiteScopeRules.FindAsync(new object[] { id }, cancellationToken);
|
||||
@@ -86,6 +101,7 @@ public class SecurityRepository : ISecurityRepository
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _context.SaveChangesAsync(cancellationToken);
|
||||
|
||||
@@ -42,20 +42,18 @@ public class SiteCallAuditRepository : ISiteCallAuditRepository
|
||||
private readonly ScadaLinkDbContext _context;
|
||||
private readonly ILogger<SiteCallAuditRepository> _logger;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SiteCallAuditRepository"/> class.
|
||||
/// </summary>
|
||||
/// <param name="context">The EF Core database context.</param>
|
||||
/// <param name="logger">Optional logger for diagnostic information.</param>
|
||||
public SiteCallAuditRepository(ScadaLinkDbContext context, ILogger<SiteCallAuditRepository>? logger = null)
|
||||
{
|
||||
_context = context ?? throw new ArgumentNullException(nameof(context));
|
||||
_logger = logger ?? NullLogger<SiteCallAuditRepository>.Instance;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Two-step: <c>IF NOT EXISTS INSERT</c> then conditional <c>UPDATE</c> with
|
||||
/// an inline <c>CASE</c> rank comparison. Both go through
|
||||
/// <see cref="Microsoft.EntityFrameworkCore.RelationalDatabaseFacadeExtensions.ExecuteSqlInterpolatedAsync"/>
|
||||
/// so the change tracker is bypassed and the value-converted PK column is
|
||||
/// written as the canonical "D"-format GUID string. Duplicate-key violations
|
||||
/// from the insert race are swallowed.
|
||||
/// </summary>
|
||||
/// <inheritdoc />
|
||||
public async Task UpsertAsync(SiteCall siteCall, CancellationToken ct = default)
|
||||
{
|
||||
if (siteCall is null)
|
||||
@@ -141,25 +139,13 @@ WHERE TrackedOperationId = {idText}
|
||||
ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Single <c>FindAsync</c> against the PK. Returns <c>null</c> for unknown ids.
|
||||
/// </summary>
|
||||
/// <inheritdoc />
|
||||
public async Task<SiteCall?> GetAsync(TrackedOperationId id, CancellationToken ct = default)
|
||||
{
|
||||
return await _context.Set<SiteCall>().FindAsync(new object?[] { id }, ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds a parameterised SQL query against <c>dbo.SiteCalls</c> ordered by
|
||||
/// <c>(CreatedAtUtc DESC, TrackedOperationId DESC)</c>, with keyset paging.
|
||||
/// Raw SQL is used here (rather than LINQ) because EF Core 10 cannot
|
||||
/// translate the lexicographic string comparison against the value-converted
|
||||
/// <see cref="TrackedOperationId"/> column inside an expression tree — the
|
||||
/// converter is applied to equality but not to inequality comparisons
|
||||
/// against the underlying Guid. The keyset tiebreaker is varchar lex order,
|
||||
/// which is deterministic and gives "no overlap, every row exactly once"
|
||||
/// paging without depending on Guid byte ordering.
|
||||
/// </summary>
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<SiteCall>> QueryAsync(
|
||||
SiteCallQueryFilter filter, SiteCallPaging paging, CancellationToken ct = default)
|
||||
{
|
||||
@@ -223,11 +209,7 @@ ORDER BY CreatedAtUtc DESC, TrackedOperationId DESC;";
|
||||
return rows;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deletes rows whose <see cref="SiteCall.TerminalAtUtc"/> is non-null AND
|
||||
/// strictly less than <paramref name="olderThanUtc"/>. Non-terminal rows are
|
||||
/// never touched. Returns the number of rows removed.
|
||||
/// </summary>
|
||||
/// <inheritdoc />
|
||||
public async Task<int> PurgeTerminalAsync(DateTime olderThanUtc, CancellationToken ct = default)
|
||||
{
|
||||
return await _context.Database.ExecuteSqlInterpolatedAsync(
|
||||
@@ -252,16 +234,7 @@ ORDER BY CreatedAtUtc DESC, TrackedOperationId DESC;";
|
||||
private const string StatusDelivered = "Delivered";
|
||||
private const string StatusFailed = "Failed";
|
||||
|
||||
/// <summary>
|
||||
/// Computes the global KPI snapshot with five server-side aggregate queries
|
||||
/// against <c>dbo.SiteCalls</c>. No rows are materialised — every count is a
|
||||
/// translated <c>COUNT</c> and the oldest-pending age is a translated
|
||||
/// <c>MIN(CreatedAtUtc)</c>. The <c>Status</c> and <c>CreatedAtUtc</c>/<c>TerminalAtUtc</c>
|
||||
/// columns have no value converter, so the aggregates translate cleanly to
|
||||
/// SQL Server (unlike the NotificationOutbox's <c>DateTimeOffset</c>-converted
|
||||
/// column, which forces an order-and-take). "Buffered" / "stuck" key off
|
||||
/// <c>TerminalAtUtc IS NULL</c> — see the field comments above.
|
||||
/// </summary>
|
||||
/// <inheritdoc />
|
||||
public async Task<SiteCallKpiSnapshot> ComputeKpisAsync(
|
||||
DateTime stuckCutoff, DateTime intervalSince, CancellationToken ct = default)
|
||||
{
|
||||
@@ -304,14 +277,7 @@ ORDER BY CreatedAtUtc DESC, TrackedOperationId DESC;";
|
||||
StuckCount: stuckCount);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes the per-source-site KPI breakdown. The five counts are
|
||||
/// <c>GROUP BY SourceSite</c> aggregates; the oldest-pending age is a
|
||||
/// per-site <c>MIN(CreatedAtUtc)</c> over the (bounded) non-terminal set —
|
||||
/// all run server-side. A site appears in the result only if it has at
|
||||
/// least one row matched by one of the count queries. "Buffered" / "stuck"
|
||||
/// key off <c>TerminalAtUtc IS NULL</c> — see <see cref="ComputeKpisAsync"/>.
|
||||
/// </summary>
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<SiteCallSiteKpiSnapshot>> ComputePerSiteKpisAsync(
|
||||
DateTime stuckCutoff, DateTime intervalSince, CancellationToken ct = default)
|
||||
{
|
||||
|
||||
@@ -12,6 +12,10 @@ public class SiteRepository : ISiteRepository
|
||||
{
|
||||
private readonly ScadaLinkDbContext _dbContext;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the SiteRepository.
|
||||
/// </summary>
|
||||
/// <param name="dbContext">The database context.</param>
|
||||
public SiteRepository(ScadaLinkDbContext dbContext)
|
||||
{
|
||||
_dbContext = dbContext ?? throw new ArgumentNullException(nameof(dbContext));
|
||||
@@ -19,33 +23,39 @@ public class SiteRepository : ISiteRepository
|
||||
|
||||
// --- Sites ---
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<Site?> GetSiteByIdAsync(int id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _dbContext.Sites.FindAsync([id], cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<Site?> GetSiteByIdentifierAsync(string siteIdentifier, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _dbContext.Sites
|
||||
.FirstOrDefaultAsync(s => s.SiteIdentifier == siteIdentifier, cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<Site>> GetAllSitesAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _dbContext.Sites.OrderBy(s => s.Name).ToListAsync(cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task AddSiteAsync(Site site, CancellationToken cancellationToken = default)
|
||||
{
|
||||
await _dbContext.Sites.AddAsync(site, cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task UpdateSiteAsync(Site site, CancellationToken cancellationToken = default)
|
||||
{
|
||||
_dbContext.Sites.Update(site);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task DeleteSiteAsync(int id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var entity = _dbContext.Sites.Local.FirstOrDefault(s => s.Id == id);
|
||||
@@ -64,16 +74,19 @@ public class SiteRepository : ISiteRepository
|
||||
|
||||
// --- Data Connections ---
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<DataConnection?> GetDataConnectionByIdAsync(int id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _dbContext.DataConnections.FindAsync([id], cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<DataConnection>> GetAllDataConnectionsAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _dbContext.DataConnections.OrderBy(c => c.Name).ToListAsync(cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<DataConnection>> GetDataConnectionsBySiteIdAsync(int siteId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _dbContext.DataConnections
|
||||
@@ -82,17 +95,20 @@ public class SiteRepository : ISiteRepository
|
||||
.ToListAsync(cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task AddDataConnectionAsync(DataConnection connection, CancellationToken cancellationToken = default)
|
||||
{
|
||||
await _dbContext.DataConnections.AddAsync(connection, cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task UpdateDataConnectionAsync(DataConnection connection, CancellationToken cancellationToken = default)
|
||||
{
|
||||
_dbContext.DataConnections.Update(connection);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task DeleteDataConnectionAsync(int id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var entity = _dbContext.DataConnections.Local.FirstOrDefault(c => c.Id == id);
|
||||
@@ -111,6 +127,7 @@ public class SiteRepository : ISiteRepository
|
||||
|
||||
// --- Instances (for deletion constraint checks) ---
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<Instance>> GetInstancesBySiteIdAsync(int siteId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _dbContext.Instances
|
||||
@@ -118,6 +135,7 @@ public class SiteRepository : ISiteRepository
|
||||
.ToListAsync(cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _dbContext.SaveChangesAsync(cancellationToken);
|
||||
|
||||
@@ -10,6 +10,10 @@ public class TemplateEngineRepository : ITemplateEngineRepository
|
||||
{
|
||||
private readonly ScadaLinkDbContext _context;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the TemplateEngineRepository class.
|
||||
/// </summary>
|
||||
/// <param name="context">The database context used to access template and instance data.</param>
|
||||
public TemplateEngineRepository(ScadaLinkDbContext context)
|
||||
{
|
||||
_context = context ?? throw new ArgumentNullException(nameof(context));
|
||||
@@ -17,6 +21,7 @@ public class TemplateEngineRepository : ITemplateEngineRepository
|
||||
|
||||
// Template
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<Template?> GetTemplateByIdAsync(int id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _context.Templates
|
||||
@@ -28,17 +33,13 @@ public class TemplateEngineRepository : ITemplateEngineRepository
|
||||
.FirstOrDefaultAsync(t => t.Id == id, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loads a template together with its child members — Attributes, Alarms,
|
||||
/// Scripts and Compositions — eager-loaded so callers get the full template
|
||||
/// aggregate in a single round-trip. "Children" here refers to the template's
|
||||
/// member collections, not derived/sub templates.
|
||||
/// </summary>
|
||||
/// <inheritdoc />
|
||||
public async Task<Template?> GetTemplateWithChildrenAsync(int id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await GetTemplateByIdAsync(id, cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<Template>> GetAllTemplatesAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _context.Templates
|
||||
@@ -50,6 +51,7 @@ public class TemplateEngineRepository : ITemplateEngineRepository
|
||||
.ToListAsync(cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<Template>> GetTemplatesComposingAsync(int composedTemplateId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _context.Templates
|
||||
@@ -61,17 +63,20 @@ public class TemplateEngineRepository : ITemplateEngineRepository
|
||||
.ToListAsync(cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task AddTemplateAsync(Template template, CancellationToken cancellationToken = default)
|
||||
{
|
||||
await _context.Templates.AddAsync(template, cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task UpdateTemplateAsync(Template template, CancellationToken cancellationToken = default)
|
||||
{
|
||||
_context.Templates.Update(template);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task DeleteTemplateAsync(int id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var template = await _context.Templates.FindAsync(new object[] { id }, cancellationToken);
|
||||
@@ -83,11 +88,13 @@ public class TemplateEngineRepository : ITemplateEngineRepository
|
||||
|
||||
// TemplateAttribute
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<TemplateAttribute?> GetTemplateAttributeByIdAsync(int id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _context.TemplateAttributes.FindAsync(new object[] { id }, cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<TemplateAttribute>> GetAttributesByTemplateIdAsync(int templateId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _context.TemplateAttributes
|
||||
@@ -95,17 +102,20 @@ public class TemplateEngineRepository : ITemplateEngineRepository
|
||||
.ToListAsync(cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task AddTemplateAttributeAsync(TemplateAttribute attribute, CancellationToken cancellationToken = default)
|
||||
{
|
||||
await _context.TemplateAttributes.AddAsync(attribute, cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task UpdateTemplateAttributeAsync(TemplateAttribute attribute, CancellationToken cancellationToken = default)
|
||||
{
|
||||
_context.TemplateAttributes.Update(attribute);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task DeleteTemplateAttributeAsync(int id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var attribute = await _context.TemplateAttributes.FindAsync(new object[] { id }, cancellationToken);
|
||||
@@ -117,11 +127,13 @@ public class TemplateEngineRepository : ITemplateEngineRepository
|
||||
|
||||
// TemplateAlarm
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<TemplateAlarm?> GetTemplateAlarmByIdAsync(int id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _context.TemplateAlarms.FindAsync(new object[] { id }, cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<TemplateAlarm>> GetAlarmsByTemplateIdAsync(int templateId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _context.TemplateAlarms
|
||||
@@ -129,17 +141,20 @@ public class TemplateEngineRepository : ITemplateEngineRepository
|
||||
.ToListAsync(cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task AddTemplateAlarmAsync(TemplateAlarm alarm, CancellationToken cancellationToken = default)
|
||||
{
|
||||
await _context.TemplateAlarms.AddAsync(alarm, cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task UpdateTemplateAlarmAsync(TemplateAlarm alarm, CancellationToken cancellationToken = default)
|
||||
{
|
||||
_context.TemplateAlarms.Update(alarm);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task DeleteTemplateAlarmAsync(int id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var alarm = await _context.TemplateAlarms.FindAsync(new object[] { id }, cancellationToken);
|
||||
@@ -151,11 +166,13 @@ public class TemplateEngineRepository : ITemplateEngineRepository
|
||||
|
||||
// TemplateScript
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<TemplateScript?> GetTemplateScriptByIdAsync(int id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _context.TemplateScripts.FindAsync(new object[] { id }, cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<TemplateScript>> GetScriptsByTemplateIdAsync(int templateId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _context.TemplateScripts
|
||||
@@ -163,17 +180,20 @@ public class TemplateEngineRepository : ITemplateEngineRepository
|
||||
.ToListAsync(cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task AddTemplateScriptAsync(TemplateScript script, CancellationToken cancellationToken = default)
|
||||
{
|
||||
await _context.TemplateScripts.AddAsync(script, cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task UpdateTemplateScriptAsync(TemplateScript script, CancellationToken cancellationToken = default)
|
||||
{
|
||||
_context.TemplateScripts.Update(script);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task DeleteTemplateScriptAsync(int id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var script = await _context.TemplateScripts.FindAsync(new object[] { id }, cancellationToken);
|
||||
@@ -185,11 +205,13 @@ public class TemplateEngineRepository : ITemplateEngineRepository
|
||||
|
||||
// TemplateComposition
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<TemplateComposition?> GetTemplateCompositionByIdAsync(int id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _context.TemplateCompositions.FindAsync(new object[] { id }, cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<TemplateComposition>> GetCompositionsByTemplateIdAsync(int templateId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _context.TemplateCompositions
|
||||
@@ -197,17 +219,20 @@ public class TemplateEngineRepository : ITemplateEngineRepository
|
||||
.ToListAsync(cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task AddTemplateCompositionAsync(TemplateComposition composition, CancellationToken cancellationToken = default)
|
||||
{
|
||||
await _context.TemplateCompositions.AddAsync(composition, cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task UpdateTemplateCompositionAsync(TemplateComposition composition, CancellationToken cancellationToken = default)
|
||||
{
|
||||
_context.TemplateCompositions.Update(composition);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task DeleteTemplateCompositionAsync(int id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var composition = await _context.TemplateCompositions.FindAsync(new object[] { id }, cancellationToken);
|
||||
@@ -219,6 +244,7 @@ public class TemplateEngineRepository : ITemplateEngineRepository
|
||||
|
||||
// Instance
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<Instance?> GetInstanceByIdAsync(int id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _context.Instances
|
||||
@@ -229,6 +255,7 @@ public class TemplateEngineRepository : ITemplateEngineRepository
|
||||
.FirstOrDefaultAsync(i => i.Id == id, cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<Instance>> GetAllInstancesAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _context.Instances
|
||||
@@ -239,6 +266,7 @@ public class TemplateEngineRepository : ITemplateEngineRepository
|
||||
.ToListAsync(cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<Instance>> GetInstancesByTemplateIdAsync(int templateId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _context.Instances
|
||||
@@ -246,6 +274,7 @@ public class TemplateEngineRepository : ITemplateEngineRepository
|
||||
.ToListAsync(cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<Instance>> GetInstancesBySiteIdAsync(int siteId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _context.Instances
|
||||
@@ -257,6 +286,7 @@ public class TemplateEngineRepository : ITemplateEngineRepository
|
||||
.ToListAsync(cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<Instance?> GetInstanceByUniqueNameAsync(string uniqueName, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _context.Instances
|
||||
@@ -267,17 +297,20 @@ public class TemplateEngineRepository : ITemplateEngineRepository
|
||||
.FirstOrDefaultAsync(i => i.UniqueName == uniqueName, cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task AddInstanceAsync(Instance instance, CancellationToken cancellationToken = default)
|
||||
{
|
||||
await _context.Instances.AddAsync(instance, cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task UpdateInstanceAsync(Instance instance, CancellationToken cancellationToken = default)
|
||||
{
|
||||
_context.Instances.Update(instance);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task DeleteInstanceAsync(int id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var instance = await _context.Instances.FindAsync(new object[] { id }, cancellationToken);
|
||||
@@ -289,6 +322,7 @@ public class TemplateEngineRepository : ITemplateEngineRepository
|
||||
|
||||
// InstanceAttributeOverride
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<InstanceAttributeOverride>> GetOverridesByInstanceIdAsync(int instanceId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _context.InstanceAttributeOverrides
|
||||
@@ -296,17 +330,20 @@ public class TemplateEngineRepository : ITemplateEngineRepository
|
||||
.ToListAsync(cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task AddInstanceAttributeOverrideAsync(InstanceAttributeOverride attributeOverride, CancellationToken cancellationToken = default)
|
||||
{
|
||||
await _context.InstanceAttributeOverrides.AddAsync(attributeOverride, cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task UpdateInstanceAttributeOverrideAsync(InstanceAttributeOverride attributeOverride, CancellationToken cancellationToken = default)
|
||||
{
|
||||
_context.InstanceAttributeOverrides.Update(attributeOverride);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task DeleteInstanceAttributeOverrideAsync(int id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var attributeOverride = await _context.InstanceAttributeOverrides.FindAsync(new object[] { id }, cancellationToken);
|
||||
@@ -318,6 +355,7 @@ public class TemplateEngineRepository : ITemplateEngineRepository
|
||||
|
||||
// InstanceAlarmOverride
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<InstanceAlarmOverride>> GetAlarmOverridesByInstanceIdAsync(int instanceId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _context.InstanceAlarmOverrides
|
||||
@@ -325,6 +363,7 @@ public class TemplateEngineRepository : ITemplateEngineRepository
|
||||
.ToListAsync(cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<InstanceAlarmOverride?> GetAlarmOverrideAsync(int instanceId, string alarmCanonicalName, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _context.InstanceAlarmOverrides
|
||||
@@ -333,17 +372,20 @@ public class TemplateEngineRepository : ITemplateEngineRepository
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task AddInstanceAlarmOverrideAsync(InstanceAlarmOverride alarmOverride, CancellationToken cancellationToken = default)
|
||||
{
|
||||
await _context.InstanceAlarmOverrides.AddAsync(alarmOverride, cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task UpdateInstanceAlarmOverrideAsync(InstanceAlarmOverride alarmOverride, CancellationToken cancellationToken = default)
|
||||
{
|
||||
_context.InstanceAlarmOverrides.Update(alarmOverride);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task DeleteInstanceAlarmOverrideAsync(int id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var alarmOverride = await _context.InstanceAlarmOverrides.FindAsync(new object[] { id }, cancellationToken);
|
||||
@@ -355,6 +397,7 @@ public class TemplateEngineRepository : ITemplateEngineRepository
|
||||
|
||||
// InstanceConnectionBinding
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<InstanceConnectionBinding>> GetBindingsByInstanceIdAsync(int instanceId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _context.InstanceConnectionBindings
|
||||
@@ -362,17 +405,20 @@ public class TemplateEngineRepository : ITemplateEngineRepository
|
||||
.ToListAsync(cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task AddInstanceConnectionBindingAsync(InstanceConnectionBinding binding, CancellationToken cancellationToken = default)
|
||||
{
|
||||
await _context.InstanceConnectionBindings.AddAsync(binding, cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task UpdateInstanceConnectionBindingAsync(InstanceConnectionBinding binding, CancellationToken cancellationToken = default)
|
||||
{
|
||||
_context.InstanceConnectionBindings.Update(binding);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task DeleteInstanceConnectionBindingAsync(int id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var binding = await _context.InstanceConnectionBindings.FindAsync(new object[] { id }, cancellationToken);
|
||||
@@ -384,6 +430,7 @@ public class TemplateEngineRepository : ITemplateEngineRepository
|
||||
|
||||
// Area
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<Area?> GetAreaByIdAsync(int id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _context.Areas
|
||||
@@ -391,6 +438,7 @@ public class TemplateEngineRepository : ITemplateEngineRepository
|
||||
.FirstOrDefaultAsync(a => a.Id == id, cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<Area>> GetAreasBySiteIdAsync(int siteId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _context.Areas
|
||||
@@ -399,17 +447,20 @@ public class TemplateEngineRepository : ITemplateEngineRepository
|
||||
.ToListAsync(cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task AddAreaAsync(Area area, CancellationToken cancellationToken = default)
|
||||
{
|
||||
await _context.Areas.AddAsync(area, cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task UpdateAreaAsync(Area area, CancellationToken cancellationToken = default)
|
||||
{
|
||||
_context.Areas.Update(area);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task DeleteAreaAsync(int id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var area = await _context.Areas.FindAsync(new object[] { id }, cancellationToken);
|
||||
@@ -421,33 +472,39 @@ public class TemplateEngineRepository : ITemplateEngineRepository
|
||||
|
||||
// SharedScript
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<SharedScript?> GetSharedScriptByIdAsync(int id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _context.SharedScripts.FindAsync(new object[] { id }, cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<SharedScript?> GetSharedScriptByNameAsync(string name, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _context.SharedScripts
|
||||
.FirstOrDefaultAsync(s => s.Name == name, cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<SharedScript>> GetAllSharedScriptsAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _context.SharedScripts.ToListAsync(cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task AddSharedScriptAsync(SharedScript sharedScript, CancellationToken cancellationToken = default)
|
||||
{
|
||||
await _context.SharedScripts.AddAsync(sharedScript, cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task UpdateSharedScriptAsync(SharedScript sharedScript, CancellationToken cancellationToken = default)
|
||||
{
|
||||
_context.SharedScripts.Update(sharedScript);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task DeleteSharedScriptAsync(int id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var sharedScript = await _context.SharedScripts.FindAsync(new object[] { id }, cancellationToken);
|
||||
@@ -459,21 +516,26 @@ public class TemplateEngineRepository : ITemplateEngineRepository
|
||||
|
||||
// TemplateFolder
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<TemplateFolder?> GetFolderByIdAsync(int id, CancellationToken cancellationToken = default)
|
||||
=> await _context.TemplateFolders.FindAsync(new object[] { id }, cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<TemplateFolder>> GetAllFoldersAsync(CancellationToken cancellationToken = default)
|
||||
=> await _context.TemplateFolders.ToListAsync(cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task AddFolderAsync(TemplateFolder folder, CancellationToken cancellationToken = default)
|
||||
=> await _context.TemplateFolders.AddAsync(folder, cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task UpdateFolderAsync(TemplateFolder folder, CancellationToken cancellationToken = default)
|
||||
{
|
||||
_context.TemplateFolders.Update(folder);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task DeleteFolderAsync(int id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var folder = await _context.TemplateFolders.FindAsync(new object[] { id }, cancellationToken);
|
||||
@@ -483,6 +545,7 @@ public class TemplateEngineRepository : ITemplateEngineRepository
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _context.SaveChangesAsync(cancellationToken);
|
||||
|
||||
@@ -20,6 +20,10 @@ public class ScadaLinkDbContext : DbContext, IDataProtectionKeyContext
|
||||
{
|
||||
private readonly IDataProtectionProvider? _dataProtectionProvider;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ScadaLinkDbContext"/> class for schema-only access (design-time).
|
||||
/// </summary>
|
||||
/// <param name="options">Database context options.</param>
|
||||
public ScadaLinkDbContext(DbContextOptions<ScadaLinkDbContext> options) : base(options)
|
||||
{
|
||||
}
|
||||
@@ -29,6 +33,8 @@ public class ScadaLinkDbContext : DbContext, IDataProtectionKeyContext
|
||||
/// secret-bearing configuration columns at rest. The runtime resolves this overload
|
||||
/// via DI; design-time tooling uses the single-argument overload.
|
||||
/// </summary>
|
||||
/// <param name="options">Database context options.</param>
|
||||
/// <param name="dataProtectionProvider">Data Protection provider for encrypting secrets at rest.</param>
|
||||
public ScadaLinkDbContext(DbContextOptions<ScadaLinkDbContext> options, IDataProtectionProvider dataProtectionProvider)
|
||||
: base(options)
|
||||
{
|
||||
@@ -37,59 +43,92 @@ public class ScadaLinkDbContext : DbContext, IDataProtectionKeyContext
|
||||
}
|
||||
|
||||
// Templates
|
||||
/// <summary>Gets the set of templates.</summary>
|
||||
public DbSet<Template> Templates => Set<Template>();
|
||||
/// <summary>Gets the set of template attributes.</summary>
|
||||
public DbSet<TemplateAttribute> TemplateAttributes => Set<TemplateAttribute>();
|
||||
/// <summary>Gets the set of template alarms.</summary>
|
||||
public DbSet<TemplateAlarm> TemplateAlarms => Set<TemplateAlarm>();
|
||||
/// <summary>Gets the set of template scripts.</summary>
|
||||
public DbSet<TemplateScript> TemplateScripts => Set<TemplateScript>();
|
||||
/// <summary>Gets the set of template compositions.</summary>
|
||||
public DbSet<TemplateComposition> TemplateCompositions => Set<TemplateComposition>();
|
||||
/// <summary>Gets the set of template folders.</summary>
|
||||
public DbSet<TemplateFolder> TemplateFolders => Set<TemplateFolder>();
|
||||
|
||||
// Instances
|
||||
/// <summary>Gets the set of instances.</summary>
|
||||
public DbSet<Instance> Instances => Set<Instance>();
|
||||
/// <summary>Gets the set of instance attribute overrides.</summary>
|
||||
public DbSet<InstanceAttributeOverride> InstanceAttributeOverrides => Set<InstanceAttributeOverride>();
|
||||
/// <summary>Gets the set of instance alarm overrides.</summary>
|
||||
public DbSet<InstanceAlarmOverride> InstanceAlarmOverrides => Set<InstanceAlarmOverride>();
|
||||
/// <summary>Gets the set of instance connection bindings.</summary>
|
||||
public DbSet<InstanceConnectionBinding> InstanceConnectionBindings => Set<InstanceConnectionBinding>();
|
||||
/// <summary>Gets the set of areas.</summary>
|
||||
public DbSet<Area> Areas => Set<Area>();
|
||||
|
||||
// Sites
|
||||
/// <summary>Gets the set of sites.</summary>
|
||||
public DbSet<Site> Sites => Set<Site>();
|
||||
/// <summary>Gets the set of data connections.</summary>
|
||||
public DbSet<DataConnection> DataConnections => Set<DataConnection>();
|
||||
|
||||
// Deployment
|
||||
/// <summary>Gets the set of deployment records.</summary>
|
||||
public DbSet<DeploymentRecord> DeploymentRecords => Set<DeploymentRecord>();
|
||||
/// <summary>Gets the set of system artifact deployment records.</summary>
|
||||
public DbSet<SystemArtifactDeploymentRecord> SystemArtifactDeploymentRecords => Set<SystemArtifactDeploymentRecord>();
|
||||
/// <summary>Gets the set of deployed configuration snapshots.</summary>
|
||||
public DbSet<DeployedConfigSnapshot> DeployedConfigSnapshots => Set<DeployedConfigSnapshot>();
|
||||
|
||||
// External Systems
|
||||
/// <summary>Gets the set of external system definitions.</summary>
|
||||
public DbSet<ExternalSystemDefinition> ExternalSystemDefinitions => Set<ExternalSystemDefinition>();
|
||||
/// <summary>Gets the set of external system methods.</summary>
|
||||
public DbSet<ExternalSystemMethod> ExternalSystemMethods => Set<ExternalSystemMethod>();
|
||||
/// <summary>Gets the set of database connection definitions.</summary>
|
||||
public DbSet<DatabaseConnectionDefinition> DatabaseConnectionDefinitions => Set<DatabaseConnectionDefinition>();
|
||||
|
||||
// Notifications
|
||||
/// <summary>Gets the set of notification lists.</summary>
|
||||
public DbSet<NotificationList> NotificationLists => Set<NotificationList>();
|
||||
/// <summary>Gets the set of notification recipients.</summary>
|
||||
public DbSet<NotificationRecipient> NotificationRecipients => Set<NotificationRecipient>();
|
||||
/// <summary>Gets the set of SMTP configurations.</summary>
|
||||
public DbSet<SmtpConfiguration> SmtpConfigurations => Set<SmtpConfiguration>();
|
||||
/// <summary>Gets the set of notifications.</summary>
|
||||
public DbSet<Notification> Notifications => Set<Notification>();
|
||||
|
||||
// Scripts
|
||||
/// <summary>Gets the set of shared scripts.</summary>
|
||||
public DbSet<SharedScript> SharedScripts => Set<SharedScript>();
|
||||
|
||||
// Security
|
||||
/// <summary>Gets the set of LDAP group mappings.</summary>
|
||||
public DbSet<LdapGroupMapping> LdapGroupMappings => Set<LdapGroupMapping>();
|
||||
/// <summary>Gets the set of site scope rules.</summary>
|
||||
public DbSet<SiteScopeRule> SiteScopeRules => Set<SiteScopeRule>();
|
||||
|
||||
// Inbound API
|
||||
/// <summary>Gets the set of API keys.</summary>
|
||||
public DbSet<ApiKey> ApiKeys => Set<ApiKey>();
|
||||
/// <summary>Gets the set of API methods.</summary>
|
||||
public DbSet<ApiMethod> ApiMethods => Set<ApiMethod>();
|
||||
|
||||
// Audit
|
||||
/// <summary>Gets the set of audit log entries.</summary>
|
||||
public DbSet<AuditLogEntry> AuditLogEntries => Set<AuditLogEntry>();
|
||||
/// <summary>Gets the set of audit logs.</summary>
|
||||
public DbSet<AuditEvent> AuditLogs => Set<AuditEvent>();
|
||||
/// <summary>Gets the set of site calls.</summary>
|
||||
public DbSet<SiteCall> SiteCalls => Set<SiteCall>();
|
||||
|
||||
// Data Protection Keys (for shared ASP.NET Data Protection across nodes)
|
||||
/// <summary>Gets the set of data protection keys.</summary>
|
||||
public DbSet<DataProtectionKey> DataProtectionKeys => Set<DataProtectionKey>();
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
||||
{
|
||||
base.OnConfiguring(optionsBuilder);
|
||||
@@ -103,6 +142,7 @@ public class ScadaLinkDbContext : DbContext, IDataProtectionKeyContext
|
||||
optionsBuilder.ReplaceService<IModelCacheKeyFactory, SecretAwareModelCacheKeyFactory>();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
modelBuilder.ApplyConfigurationsFromAssembly(typeof(ScadaLinkDbContext).Assembly);
|
||||
@@ -110,12 +150,14 @@ public class ScadaLinkDbContext : DbContext, IDataProtectionKeyContext
|
||||
ApplySecretColumnEncryption(modelBuilder);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override int SaveChanges(bool acceptAllChangesOnSuccess)
|
||||
{
|
||||
GuardSecretWritesHaveAKeyRing();
|
||||
return base.SaveChanges(acceptAllChangesOnSuccess);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task<int> SaveChangesAsync(
|
||||
bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = default)
|
||||
{
|
||||
@@ -173,11 +215,22 @@ public class ScadaLinkDbContext : DbContext, IDataProtectionKeyContext
|
||||
/// </summary>
|
||||
private sealed class SecretAwareModelCacheKeyFactory : IModelCacheKeyFactory
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a model cache key that includes the Data Protection provider state.
|
||||
/// </summary>
|
||||
/// <param name="context">The database context.</param>
|
||||
/// <param name="designTime">Whether the model is being created at design-time.</param>
|
||||
/// <returns>A cache key tuple.</returns>
|
||||
public object Create(DbContext context, bool designTime)
|
||||
=> (context.GetType(),
|
||||
designTime,
|
||||
(context as ScadaLinkDbContext)?.HasSecretEncryptionProvider ?? false);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a model cache key for run-time contexts.
|
||||
/// </summary>
|
||||
/// <param name="context">The database context.</param>
|
||||
/// <returns>A cache key tuple.</returns>
|
||||
public object Create(DbContext context) => Create(context, false);
|
||||
}
|
||||
|
||||
@@ -240,10 +293,25 @@ public class ScadaLinkDbContext : DbContext, IDataProtectionKeyContext
|
||||
"columns cannot be read or written through it. Construct the context with the " +
|
||||
"DI-registered IDataProtectionProvider (AddConfigurationDatabase wires this up).";
|
||||
|
||||
/// <summary>
|
||||
/// Creates a schema-only protector that raises when called.
|
||||
/// </summary>
|
||||
/// <param name="purpose">The protector purpose string.</param>
|
||||
/// <returns>This instance.</returns>
|
||||
public IDataProtector CreateProtector(string purpose) => this;
|
||||
|
||||
/// <summary>
|
||||
/// Protects plaintext (schema-only version always throws).
|
||||
/// </summary>
|
||||
/// <param name="plaintext">The data to protect.</param>
|
||||
/// <returns>Never returns.</returns>
|
||||
public byte[] Protect(byte[] plaintext) => throw new InvalidOperationException(Message);
|
||||
|
||||
/// <summary>
|
||||
/// Unprotects ciphertext (schema-only version always throws).
|
||||
/// </summary>
|
||||
/// <param name="protectedData">The protected data.</param>
|
||||
/// <returns>Never returns.</returns>
|
||||
public byte[] Unprotect(byte[] protectedData) => throw new InvalidOperationException(Message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,8 @@ public static class ServiceCollectionExtensions
|
||||
/// <summary>
|
||||
/// Registers the ScadaLinkDbContext with the provided SQL Server connection string.
|
||||
/// </summary>
|
||||
/// <param name="services">The service collection to register into.</param>
|
||||
/// <param name="connectionString">SQL Server connection string for the central configuration database.</param>
|
||||
public static IServiceCollection AddConfigurationDatabase(this IServiceCollection services, string connectionString)
|
||||
{
|
||||
// The DbContext is constructed via the (options, IDataProtectionProvider) overload so
|
||||
@@ -76,6 +78,7 @@ public static class ServiceCollectionExtensions
|
||||
/// <see cref="AddConfigurationDatabase(IServiceCollection, string)"/> and pass the
|
||||
/// configured connection string.
|
||||
/// </summary>
|
||||
/// <param name="services">The service collection (unused; this overload always throws).</param>
|
||||
/// <exception cref="InvalidOperationException">
|
||||
/// Always thrown. The connection string is required; there is no valid no-op registration.
|
||||
/// </exception>
|
||||
|
||||
@@ -10,5 +10,6 @@ namespace ScadaLink.ConfigurationDatabase.Services;
|
||||
/// </summary>
|
||||
public sealed class AuditCorrelationContext : IAuditCorrelationContext
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public Guid? BundleImportId { get; set; }
|
||||
}
|
||||
|
||||
@@ -23,12 +23,18 @@ public class AuditService : IAuditService
|
||||
MaxDepth = 32
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the audit service with the EF Core context and correlation context.
|
||||
/// </summary>
|
||||
/// <param name="context">The EF Core database context used to stage audit entries.</param>
|
||||
/// <param name="correlationContext">Provides the active bundle import id for audit row stamping.</param>
|
||||
public AuditService(ScadaLinkDbContext context, IAuditCorrelationContext correlationContext)
|
||||
{
|
||||
_context = context ?? throw new ArgumentNullException(nameof(context));
|
||||
_correlationContext = correlationContext ?? throw new ArgumentNullException(nameof(correlationContext));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task LogAsync(
|
||||
string user,
|
||||
string action,
|
||||
|
||||
@@ -10,11 +10,14 @@ public class InstanceLocator : IInstanceLocator
|
||||
{
|
||||
private readonly ScadaLinkDbContext _context;
|
||||
|
||||
/// <summary>Initializes the locator with the EF Core database context.</summary>
|
||||
/// <param name="context">The database context used to look up instances and sites.</param>
|
||||
public InstanceLocator(ScadaLinkDbContext context)
|
||||
{
|
||||
_context = context ?? throw new ArgumentNullException(nameof(context));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<string?> GetSiteIdForInstanceAsync(
|
||||
string instanceUniqueName,
|
||||
CancellationToken cancellationToken = default)
|
||||
|
||||
Reference in New Issue
Block a user