feat(localdb): wire AddOtOpcUaLocalDb into the driver role

This commit is contained in:
Joseph Doherty
2026-07-20 10:35:02 -04:00
parent 3d29be834e
commit 6aff9a8332
3 changed files with 119 additions and 0 deletions
@@ -0,0 +1,95 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using ZB.MOM.WW.LocalDb;
using ZB.MOM.WW.LocalDb.Replication;
namespace ZB.MOM.WW.OtOpcUa.Host.Configuration;
/// <summary>
/// Single registration point for the node-local LocalDb subsystem: the embedded SQLite store
/// that caches the deployed-configuration artifact, plus the optional gRPC replication that
/// mirrors it to the node's redundant pair peer.
/// </summary>
/// <remarks>
/// <para>
/// Exists as a named extension rather than inline <c>AddZbLocalDb</c> calls in
/// <c>Program.cs</c> so the wiring is covered by a DI resolution test — <c>Program.cs</c> is
/// top-level statements and cannot be exercised directly, which is exactly how a
/// "registered but never resolvable" defect ships unnoticed. This family has shipped that
/// defect three times (Secrets 0.2.0, Secrets 0.2.2, ScadaBridge #22).
/// </para>
/// <para>
/// <b>Driver-role nodes only.</b> Admin-only nodes have no deployed configuration to cache,
/// and registering here would impose the <c>LocalDb:Path</c>-required-or-no-boot constraint
/// on them for nothing.
/// </para>
/// <para>
/// <b>Storage is unconditional; replication is default-OFF.</b> A node with no
/// <c>PeerAddress</c> and no <c>SyncListenPort</c> is simply a fast local SQLite file —
/// <c>AddZbLocalDbReplication</c> registers the engine but it stays idle. Replication is
/// opt-in per pair because there is no production distribution story for the sync
/// <c>ApiKey</c> yet (the same open question the Secrets KEK has).
/// </para>
/// </remarks>
public static class LocalDbRegistration
{
/// <summary>Configuration section holding <c>LocalDbOptions</c>.</summary>
public const string LocalDbSectionPath = "LocalDb";
/// <summary>Configuration section holding <c>ReplicationOptions</c>.</summary>
public const string ReplicationSectionPath = "LocalDb:Replication";
/// <summary>
/// Configuration key carrying the dedicated h2c sync listener's port. Zero or absent means
/// no listener is bound and the passive sync endpoint is not mapped.
/// </summary>
public const string SyncListenPortKey = "LocalDb:SyncListenPort";
/// <summary>
/// The port the dedicated cleartext-HTTP/2 sync listener should bind, or <c>0</c> when the
/// node should not listen at all. Defaults to <c>0</c> — absent configuration must mean
/// "off".
/// </summary>
/// <param name="configuration">The application configuration.</param>
/// <returns>The configured sync port, or <c>0</c>.</returns>
public static int SyncListenPort(IConfiguration configuration)
{
ArgumentNullException.ThrowIfNull(configuration);
return configuration.GetValue(SyncListenPortKey, defaultValue: 0);
}
/// <summary>
/// Registers the local database (with the deployment-cache schema applied and registered for
/// replication) and the replication engine.
/// </summary>
/// <remarks>
/// <para>
/// <see cref="LocalDbSetup.OnReady"/> runs inside <c>AddZbLocalDb</c>'s singleton factory,
/// before the first caller receives the <c>ILocalDb</c> — so every consumer is guaranteed
/// to see the tables created and their capture triggers installed.
/// </para>
/// <para>
/// <c>AddZbLocalDbReplication</c> is called unconditionally rather than behind an
/// <c>Enabled</c> flag because the library already models "off" as "no
/// <c>PeerAddress</c>": the initiator background service idles and the passive endpoint
/// is only reachable if <c>Program.cs</c> maps it, which it does only when
/// <see cref="SyncListenPortKey"/> is set. Adding a second gate on top would give two
/// ways to express the same thing and a way for them to disagree.
/// </para>
/// </remarks>
/// <param name="services">The service collection to add to.</param>
/// <param name="configuration">The application configuration.</param>
/// <returns>The same <paramref name="services"/> instance, for chaining.</returns>
public static IServiceCollection AddOtOpcUaLocalDb(
this IServiceCollection services,
IConfiguration configuration)
{
ArgumentNullException.ThrowIfNull(services);
ArgumentNullException.ThrowIfNull(configuration);
services.AddZbLocalDb(configuration, LocalDbSetup.OnReady);
services.AddZbLocalDbReplication(configuration);
return services;
}
}