fix(dcl): feed ScadaBridge:MxGateway:ClientNamePrefix into the MxGateway client-name fallback (review 2026-10-21 02/U1, 08/O2)
This commit is contained in:
@@ -189,7 +189,7 @@ All settings are parsed from the data connection's configuration JSON dictionari
|
|||||||
|-----|------|---------|-------------|
|
|-----|------|---------|-------------|
|
||||||
| `Endpoint` | string | `http://localhost:5000` | Gateway base URL |
|
| `Endpoint` | string | `http://localhost:5000` | Gateway base URL |
|
||||||
| `ApiKey` | string | — | Sent to the gateway as `authorization: Bearer <key>` |
|
| `ApiKey` | string | — | Sent to the gateway as `authorization: Bearer <key>` |
|
||||||
| `ClientName` | string | `scadabridge` (when blank) | MXAccess client registration name |
|
| `ClientName` | string | `ScadaBridge:MxGateway:ClientNamePrefix` (when blank; that option defaults to `scadabridge`) | MXAccess client registration name. The site-wide `ClientNamePrefix` is the fallback for every connection that leaves this blank — e.g. to tell sites apart on a shared gateway; it must not be blank (validated at startup). |
|
||||||
| `WriteUserId` | int | `0` | MXAccess user id applied to every write-back (0 = no user context) |
|
| `WriteUserId` | int | `0` | MXAccess user id applied to every write-back (0 = no user context) |
|
||||||
| `UseTls` | bool | `false` | Use TLS to a secured gateway |
|
| `UseTls` | bool | `false` | Use TLS to a secured gateway |
|
||||||
| `CaFile` | string | — | Path to the CA certificate (TLS only) |
|
| `CaFile` | string | — | Path to the CA certificate (TLS only) |
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ public class MxGatewayDataConnection
|
|||||||
private readonly ILogger<MxGatewayDataConnection> _logger;
|
private readonly ILogger<MxGatewayDataConnection> _logger;
|
||||||
private readonly ISecretResolver? _secretResolver;
|
private readonly ISecretResolver? _secretResolver;
|
||||||
private readonly int _supervisoryAdviseParallelism;
|
private readonly int _supervisoryAdviseParallelism;
|
||||||
|
private readonly string _clientNamePrefix;
|
||||||
private IMxGatewayClient? _client;
|
private IMxGatewayClient? _client;
|
||||||
private ConnectionHealth _status = ConnectionHealth.Disconnected;
|
private ConnectionHealth _status = ConnectionHealth.Disconnected;
|
||||||
private CancellationTokenSource? _eventLoopCts;
|
private CancellationTokenSource? _eventLoopCts;
|
||||||
@@ -79,16 +80,25 @@ public class MxGatewayDataConnection
|
|||||||
/// Maximum in-flight supervisory advise commands on the bulk-subscribe path when the
|
/// Maximum in-flight supervisory advise commands on the bulk-subscribe path when the
|
||||||
/// endpoint has no write-user context (<c>DataConnectionOptions.MxSupervisoryAdviseParallelism</c>).
|
/// endpoint has no write-user context (<c>DataConnectionOptions.MxSupervisoryAdviseParallelism</c>).
|
||||||
/// </param>
|
/// </param>
|
||||||
|
/// <param name="clientNamePrefix">
|
||||||
|
/// Gateway client registration name used when the connection's own <c>ClientName</c> is blank
|
||||||
|
/// (<c>ScadaBridge:MxGateway:ClientNamePrefix</c>, default <c>scadabridge</c>). Null or blank
|
||||||
|
/// keeps the <c>scadabridge</c> default.
|
||||||
|
/// </param>
|
||||||
public MxGatewayDataConnection(
|
public MxGatewayDataConnection(
|
||||||
IMxGatewayClientFactory clientFactory,
|
IMxGatewayClientFactory clientFactory,
|
||||||
ILogger<MxGatewayDataConnection> logger,
|
ILogger<MxGatewayDataConnection> logger,
|
||||||
ISecretResolver? secretResolver = null,
|
ISecretResolver? secretResolver = null,
|
||||||
int supervisoryAdviseParallelism = 16)
|
int supervisoryAdviseParallelism = 16,
|
||||||
|
string? clientNamePrefix = null)
|
||||||
{
|
{
|
||||||
_clientFactory = clientFactory;
|
_clientFactory = clientFactory;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_secretResolver = secretResolver;
|
_secretResolver = secretResolver;
|
||||||
_supervisoryAdviseParallelism = Math.Max(1, supervisoryAdviseParallelism);
|
_supervisoryAdviseParallelism = Math.Max(1, supervisoryAdviseParallelism);
|
||||||
|
_clientNamePrefix = string.IsNullOrWhiteSpace(clientNamePrefix)
|
||||||
|
? new MxGatewayGlobalOptions().ClientNamePrefix
|
||||||
|
: clientNamePrefix.Trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
@@ -177,7 +187,7 @@ public class MxGatewayDataConnection
|
|||||||
await _client.ConnectAsync(new MxGatewayConnectionOptions(
|
await _client.ConnectAsync(new MxGatewayConnectionOptions(
|
||||||
cfg.Endpoint,
|
cfg.Endpoint,
|
||||||
apiKey,
|
apiKey,
|
||||||
string.IsNullOrWhiteSpace(cfg.ClientName) ? "scadabridge" : cfg.ClientName,
|
string.IsNullOrWhiteSpace(cfg.ClientName) ? _clientNamePrefix : cfg.ClientName,
|
||||||
cfg.WriteUserId,
|
cfg.WriteUserId,
|
||||||
cfg.UseTls,
|
cfg.UseTls,
|
||||||
string.IsNullOrWhiteSpace(cfg.CaFile) ? null : cfg.CaFile,
|
string.IsNullOrWhiteSpace(cfg.CaFile) ? null : cfg.CaFile,
|
||||||
|
|||||||
@@ -35,11 +35,13 @@ public class DataConnectionFactory : IDataConnectionFactory
|
|||||||
/// construction without it keeps literal-key behaviour.
|
/// construction without it keeps literal-key behaviour.
|
||||||
/// </param>
|
/// </param>
|
||||||
/// <param name="dataConnectionOptions">Optional data connection options, such as the MxGateway supervisory-advise parallelism; defaults apply when null.</param>
|
/// <param name="dataConnectionOptions">Optional data connection options, such as the MxGateway supervisory-advise parallelism; defaults apply when null.</param>
|
||||||
|
/// <param name="mxGatewayGlobalOptions">Optional MxGateway global options (the client-name fallback); defaults apply when null.</param>
|
||||||
public DataConnectionFactory(
|
public DataConnectionFactory(
|
||||||
ILoggerFactory loggerFactory,
|
ILoggerFactory loggerFactory,
|
||||||
IOptions<OpcUaGlobalOptions> opcUaGlobalOptions,
|
IOptions<OpcUaGlobalOptions> opcUaGlobalOptions,
|
||||||
ISecretResolver? secretResolver = null,
|
ISecretResolver? secretResolver = null,
|
||||||
IOptions<DataConnectionOptions>? dataConnectionOptions = null)
|
IOptions<DataConnectionOptions>? dataConnectionOptions = null,
|
||||||
|
IOptions<MxGatewayGlobalOptions>? mxGatewayGlobalOptions = null)
|
||||||
{
|
{
|
||||||
_loggerFactory = loggerFactory;
|
_loggerFactory = loggerFactory;
|
||||||
_secretResolver = secretResolver;
|
_secretResolver = secretResolver;
|
||||||
@@ -48,6 +50,9 @@ public class DataConnectionFactory : IDataConnectionFactory
|
|||||||
// DataConnectionOptions is consumed by the actor, which is constructed elsewhere.
|
// DataConnectionOptions is consumed by the actor, which is constructed elsewhere.
|
||||||
var supervisoryAdviseParallelism =
|
var supervisoryAdviseParallelism =
|
||||||
(dataConnectionOptions?.Value ?? new DataConnectionOptions()).MxSupervisoryAdviseParallelism;
|
(dataConnectionOptions?.Value ?? new DataConnectionOptions()).MxSupervisoryAdviseParallelism;
|
||||||
|
// The client registration name a connection with a blank ClientName falls back to
|
||||||
|
// (review 2026-10-21 02/U1 — the option was bound and validated but never read).
|
||||||
|
var mxClientNamePrefix = (mxGatewayGlobalOptions?.Value ?? new MxGatewayGlobalOptions()).ClientNamePrefix;
|
||||||
|
|
||||||
// Register built-in protocols.
|
// Register built-in protocols.
|
||||||
// Pass the ILoggerFactory into RealOpcUaClientFactory so
|
// Pass the ILoggerFactory into RealOpcUaClientFactory so
|
||||||
@@ -64,7 +69,8 @@ public class DataConnectionFactory : IDataConnectionFactory
|
|||||||
new RealMxGatewayClientFactory(_loggerFactory),
|
new RealMxGatewayClientFactory(_loggerFactory),
|
||||||
_loggerFactory.CreateLogger<MxGatewayDataConnection>(),
|
_loggerFactory.CreateLogger<MxGatewayDataConnection>(),
|
||||||
_secretResolver,
|
_secretResolver,
|
||||||
supervisoryAdviseParallelism));
|
supervisoryAdviseParallelism,
|
||||||
|
mxClientNamePrefix));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -6,6 +6,10 @@ namespace ZB.MOM.WW.ScadaBridge.DataConnectionLayer;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class MxGatewayGlobalOptions
|
public class MxGatewayGlobalOptions
|
||||||
{
|
{
|
||||||
/// <summary>Prefix used to derive a per-connection client registration name when the connection's ClientName is blank.</summary>
|
/// <summary>
|
||||||
|
/// Gateway client registration name used by any MxGateway connection whose own ClientName is
|
||||||
|
/// blank (e.g. set per site to tell sites apart on a shared gateway). The default keeps the
|
||||||
|
/// historical <c>scadabridge</c> name.
|
||||||
|
/// </summary>
|
||||||
public string ClientNamePrefix { get; set; } = "scadabridge";
|
public string ClientNamePrefix { get; set; } = "scadabridge";
|
||||||
}
|
}
|
||||||
|
|||||||
+19
@@ -59,6 +59,25 @@ public class MxGatewayDataConnectionTests
|
|||||||
Assert.Equal("scadabridge", fake.ConnectedWith!.ClientName);
|
Assert.Equal("scadabridge", fake.ConnectedWith!.ClientName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Review 2026-10-21 02/U1: a blank per-connection ClientName falls back to the configured
|
||||||
|
/// <c>ScadaBridge:MxGateway:ClientNamePrefix</c> rather than a hard-coded literal.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||||
|
[Fact]
|
||||||
|
public async Task ConnectAsync_blank_client_name_uses_configured_prefix()
|
||||||
|
{
|
||||||
|
var fake = new FakeMxGatewayClient();
|
||||||
|
var adapter = new MxGatewayDataConnection(
|
||||||
|
fake, NullLogger<MxGatewayDataConnection>.Instance, clientNamePrefix: "plant7-scadabridge");
|
||||||
|
var details = Details();
|
||||||
|
details["ClientName"] = " ";
|
||||||
|
|
||||||
|
await adapter.ConnectAsync(details);
|
||||||
|
|
||||||
|
Assert.Equal("plant7-scadabridge", fake.ConnectedWith!.ClientName);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>Verifies that the Disconnected event fires exactly once when the event loop faults.</summary>
|
/// <summary>Verifies that the Disconnected event fires exactly once when the event loop faults.</summary>
|
||||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
|
using ZB.MOM.WW.ScadaBridge.DataConnectionLayer.Adapters;
|
||||||
|
|
||||||
namespace ZB.MOM.WW.ScadaBridge.DataConnectionLayer.Tests;
|
namespace ZB.MOM.WW.ScadaBridge.DataConnectionLayer.Tests;
|
||||||
|
|
||||||
@@ -32,4 +35,27 @@ public class GlobalOptionsValidationTests
|
|||||||
using var provider = services.BuildServiceProvider();
|
using var provider = services.BuildServiceProvider();
|
||||||
provider.GetRequiredService<IStartupValidator>().Validate();
|
provider.GetRequiredService<IStartupValidator>().Validate();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Review 2026-10-21 02/U1: the bound <c>ScadaBridge:MxGateway:ClientNamePrefix</c> actually
|
||||||
|
/// reaches the MxGateway adapters the DI-built factory creates.
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public void ClientNamePrefix_FlowsIntoTheMxGatewayAdapter()
|
||||||
|
{
|
||||||
|
var services = new ServiceCollection();
|
||||||
|
services.AddSingleton<IConfiguration>(new ConfigurationBuilder().AddInMemoryCollection(
|
||||||
|
new Dictionary<string, string?> { ["ScadaBridge:MxGateway:ClientNamePrefix"] = "plant7" }).Build());
|
||||||
|
services.AddSingleton<ILoggerFactory>(NullLoggerFactory.Instance);
|
||||||
|
services.AddDataConnectionLayer();
|
||||||
|
using var provider = services.BuildServiceProvider();
|
||||||
|
|
||||||
|
var adapter = provider.GetRequiredService<IDataConnectionFactory>()
|
||||||
|
.Create("MxGateway", new Dictionary<string, string>());
|
||||||
|
|
||||||
|
var prefix = typeof(MxGatewayDataConnection)
|
||||||
|
.GetField("_clientNamePrefix", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic)!
|
||||||
|
.GetValue(adapter);
|
||||||
|
Assert.Equal("plant7", prefix);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user