feat(historian): backend registry, ServerHistorian:Backend, Host wiring via provider

Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
This commit is contained in:
Joseph Doherty
2026-10-09 13:05:53 -04:00
parent 65e6352dad
commit 31d342eff3
7 changed files with 186 additions and 9 deletions
@@ -0,0 +1,40 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Options;
using ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway;
using ZB.MOM.WW.OtOpcUa.Runtime.Historian;
namespace ZB.MOM.WW.OtOpcUa.Host.Configuration;
/// <summary>
/// Maps <c>ServerHistorian:Backend</c> to an <see cref="IHistorianBackendProvider"/>. Names are
/// case-insensitive. To add a backend, add one entry to <see cref="Factories"/>.
/// <see cref="Resolve"/> only constructs the provider; option validation belongs to
/// <see cref="ServerHistorianOptionsValidator"/>.
/// </summary>
public static class HistorianBackendRegistry
{
private static readonly IReadOnlyDictionary<string, Func<ServerHistorianOptions, IConfiguration, IHistorianBackendProvider>> Factories =
new Dictionary<string, Func<ServerHistorianOptions, IConfiguration, IHistorianBackendProvider>>(StringComparer.OrdinalIgnoreCase)
{
[ServerHistorianOptions.GatewayBackendName] = (o, _) => new GatewayHistorianProvider(o),
};
/// <summary>The registered backend names, for validation messages.</summary>
public static IReadOnlyCollection<string> KnownBackends { get; } = Factories.Keys.ToArray();
/// <summary>True when <paramref name="backend"/> names a registered backend.</summary>
public static bool IsKnown(string? backend) =>
!string.IsNullOrWhiteSpace(backend) && Factories.ContainsKey(backend.Trim());
/// <summary>Builds the provider for <c>options.Backend</c>; throws <see cref="OptionsValidationException"/> if unknown.</summary>
public static IHistorianBackendProvider Resolve(ServerHistorianOptions options, IConfiguration configuration)
{
ArgumentNullException.ThrowIfNull(options);
ArgumentNullException.ThrowIfNull(configuration);
if (!IsKnown(options.Backend))
throw new OptionsValidationException(
ServerHistorianOptions.SectionName, typeof(ServerHistorianOptions),
[$"ServerHistorian:Backend '{options.Backend}' is not a known historian backend. Allowed values: {string.Join(", ", KnownBackends)}."]);
return Factories[options.Backend.Trim()](options, configuration);
}
}
@@ -57,12 +57,22 @@ public sealed class ServerHistorianOptionsValidator : OptionsValidatorBase<Serve
/// <inheritdoc />
protected override void Validate(ValidationBuilder builder, ServerHistorianOptions options)
{
// Unknown backend always fails, with no I/O and regardless of the consumer gate.
builder.RequireThat(
HistorianBackendRegistry.IsKnown(options.Backend),
$"ServerHistorian:Backend '{options.Backend}' is not a known historian backend. "
+ $"Allowed values: {string.Join(", ", HistorianBackendRegistry.KnownBackends)}.");
var alarmHistorianEnabled = _configuration.GetValue<bool>("AlarmHistorian:Enabled");
var consumed = options.Enabled || alarmHistorianEnabled;
// A section nobody consumes may legitimately be empty (the docker-dev default) — no checks.
if (!consumed) return;
// Gateway connection checks apply only to the Gateway backend; other backends validate their own
// options (added with the backend) here, under the same consumer gate.
if (!options.IsGatewayBackend) return;
var endpointValid = Uri.TryCreate(options.Endpoint, UriKind.Absolute, out var uri)
&& (uri.Scheme == Uri.UriSchemeHttp || uri.Scheme == Uri.UriSchemeHttps);