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:
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user