refactor: rename ScadaLink → ZB.MOM.WW.ScadaBridge (code + projects + namespaces)

Solution + 23 src projects + 26 test projects renamed; folders, csproj,
namespaces, and ScadaLinkDbContext/ScadaBridgeDbContext class updated.
ActorSystem "scadalink" → "scadabridge", Akka seed-node URLs migrated.
SQL roles/logins, LDAP domains, CLI command name, and CLI config dir
(~/.scadalink → ~/.scadabridge) also renamed.

Build green; 5 Host.Tests fail awaiting SQL login rename in next commit.
Pre-existing StaleTagMonitor timing flakes unchanged.

Rename script committed at tools/rename-to-scadabridge.sh.
This commit is contained in:
Joseph Doherty
2026-05-28 09:37:45 -04:00
parent 6d87ee3c3b
commit 7b0b9c7365
1531 changed files with 11180 additions and 11054 deletions
@@ -0,0 +1,36 @@
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Transport;
namespace ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Services;
/// <summary>
/// Holder for the active bundle import id, backed by an <see cref="AsyncLocal{T}"/>
/// so each logical asynchronous call chain observes its own value. AuditService
/// reads it while writing AuditLogEntry rows.
/// <para>
/// Thread-safety / concurrency contract (Transport-009): the previous Scoped
/// instance with a plain auto-property mutated by <c>BundleImporter.ApplyAsync</c>
/// was vulnerable to cross-contamination if two imports ran concurrently inside
/// a shared DI scope — either via <c>Task.WhenAll</c> on a single Blazor circuit
/// or via a misconfigured singleton registration. Backing the property with
/// <see cref="AsyncLocal{T}"/> means every fresh logical-call-context — every
/// distinct <c>ApplyAsync</c> invocation, even ones sharing the same DI scope —
/// gets its own independent value, and the value flows naturally through every
/// <c>await</c> in the chain. Concurrent imports no longer leak BundleImportIds
/// across audit rows.
/// </para>
/// <para>
/// The class is still registered as Scoped so injection works with the existing
/// DI graph, but its in-memory state is per-call-context regardless of lifetime.
/// </para>
/// </summary>
public sealed class AuditCorrelationContext : IAuditCorrelationContext
{
private static readonly AsyncLocal<Guid?> _bundleImportId = new();
/// <inheritdoc />
public Guid? BundleImportId
{
get => _bundleImportId.Value;
set => _bundleImportId.Value = value;
}
}
@@ -0,0 +1,86 @@
using System.Text.Json;
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Audit;
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Services;
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Transport;
namespace ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Services;
public class AuditService : IAuditService
{
private readonly ScadaBridgeDbContext _context;
private readonly IAuditCorrelationContext _correlationContext;
/// <summary>
/// Serializer options for audit <c>afterState</c> payloads. Audit writes commit in the
/// same transaction as the change they record, so a serialization exception here would
/// roll back the entire business operation. Reference cycles (common when an EF entity
/// with loaded navigations is passed in) are ignored rather than thrown, and depth is
/// bounded so a pathological graph cannot produce an unbounded payload.
/// </summary>
private static readonly JsonSerializerOptions AuditSerializerOptions = new()
{
ReferenceHandler = System.Text.Json.Serialization.ReferenceHandler.IgnoreCycles,
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(ScadaBridgeDbContext 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,
string entityType,
string entityId,
string entityName,
object? afterState,
CancellationToken cancellationToken = default)
{
var entry = new AuditLogEntry(user, action, entityType, entityId, entityName)
{
Timestamp = DateTimeOffset.UtcNow,
AfterStateJson = afterState != null
? SerializeAfterState(afterState)
: null,
// Stamp the active bundle import id (if any) so audit rows emitted during a
// bundle import are attributable to that import session. Null in the normal
// interactive code path.
BundleImportId = _correlationContext.BundleImportId
};
// Add to change tracker only — caller is responsible for calling SaveChangesAsync
// to ensure atomicity with the entity change.
await _context.AuditLogEntries.AddAsync(entry, cancellationToken);
}
/// <summary>
/// Serializes the caller-supplied after-state, tolerating arbitrary object shapes.
/// Reference cycles are ignored via <see cref="AuditSerializerOptions"/>. If serialization
/// still fails (e.g. <c>MaxDepth</c> exceeded), the audit entry is preserved with a
/// diagnostic placeholder rather than throwing — a serialization failure must never
/// roll back the business operation the audit entry is recording.
/// </summary>
private static string SerializeAfterState(object afterState)
{
try
{
return JsonSerializer.Serialize(afterState, AuditSerializerOptions);
}
catch (Exception ex) when (ex is JsonException or NotSupportedException)
{
return JsonSerializer.Serialize(new
{
AuditSerializationError = ex.Message,
StateType = afterState.GetType().FullName
});
}
}
}
@@ -0,0 +1,36 @@
using Microsoft.EntityFrameworkCore;
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Services;
namespace ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Services;
/// <summary>
/// Resolves instance unique names to site identifiers using the configuration database.
/// </summary>
public class InstanceLocator : IInstanceLocator
{
private readonly ScadaBridgeDbContext _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(ScadaBridgeDbContext context)
{
_context = context ?? throw new ArgumentNullException(nameof(context));
}
/// <inheritdoc />
public async Task<string?> GetSiteIdForInstanceAsync(
string instanceUniqueName,
CancellationToken cancellationToken = default)
{
var instance = await _context.Set<Commons.Entities.Instances.Instance>()
.FirstOrDefaultAsync(i => i.UniqueName == instanceUniqueName, cancellationToken);
if (instance == null)
return null;
var site = await _context.Set<Commons.Entities.Sites.Site>()
.FindAsync(new object[] { instance.SiteId }, cancellationToken);
return site?.SiteIdentifier;
}
}