/* ======================================================================== * Copyright (c) 2005-2025 The OPC Foundation, Inc. All rights reserved. * * OPC Foundation MIT License 1.00 * * Permission is hereby granted, free of charge, to any person * obtaining a copy of this software and associated documentation * files (the "Software"), to deal in the Software without * restriction, including without limitation the rights to use, * copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following * conditions: * * The above copyright notice and this permission notice shall be * included in all copies or substantial portions of the Software. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. * * The complete license agreement can be found here: * http://opcfoundation.org/License/MIT/1.00/ * ======================================================================*/ using System; using System.Runtime.CompilerServices; using Opc.Ua.Server.Historian; using Opc.Ua.Server.Historian.InMemory; namespace Opc.Ua.Server.Fluent { /// /// Bridges / /// with the Part 11 historian /// surface so historization can be wired in the same fluent chain as /// OnRead/OnWrite/Publish. /// /// /// /// Caching. Per- /// state is held in a /// keyed by the builder instance, so a single /// UseHistorian().UseInMemory().RegisterAsDefault() at the /// start of Configure is reused by every later /// Historize() call on the same builder. The table is /// weak-keyed; entries vanish when the user's /// Configure delegate completes and the builder is dropped. /// /// /// "Just works" path. A bare /// Variable<double>("X").Historize() — without a prior /// UseHistorian() call and without a per-call /// provider argument — lazily creates an /// and registers it as the /// server-wide default. Subsequent Historize() calls reuse it. /// /// /// Dispatch. The extensions register variables /// against the server-wide ; /// no GetHistorianProvider override is required on the node /// manager because HistorianDispatcher.ResolveProvider falls /// through from the per-manager override to the registry. /// /// public static class HistorianFluentExtensions { /// /// Returns the associated with this /// . The first call creates and /// caches a new builder bound to the manager's server; subsequent /// calls on the same return the same /// instance so the fluent chain can stack /// UseInMemory(), UseProvider(...), /// RegisterAsDefault(), RegisterForNamespace(...), /// and RegisterForNode(...) calls in the usual way. /// /// /// Raised when is null. /// /// /// Raised when the underlying server does not implement /// (i.e., when the host /// is not the standard ServerInternalData). /// public static HistorianBuilder UseHistorian(this INodeManagerBuilder builder) { if (builder == null) { throw new ArgumentNullException(nameof(builder)); } return GetOrCreateBuilder(builder); } /// /// Marks the wrapped variable as historizing and registers it /// with the per-manager . When no /// builder has been bound yet (and no /// is supplied) the call lazily creates an /// and installs it as the /// server-wide default. /// /// CLR value type carried by the variable. /// The variable builder to historize. /// /// Access-level bits OR-ed onto AccessLevel and /// UserAccessLevel. Defaults to /// HistoryRead | HistoryWrite. /// /// /// When true, attaches an /// handler that installs /// the HistoricalDataConfigurationType companion lazily on /// the first browse against the variable. /// /// /// Optional per-node capability set passed to /// HistorianBuilder.Historize. Defaults to the underlying /// provider's default (typically ReadWrite for the /// in-memory engine). /// /// /// Optional per-call override. When supplied, the call binds the /// variable to exclusively via /// RegisterForNode; the cached per-manager builder is left /// untouched. /// /// /// When true (the default), the call wires a /// handler that forwards /// every Value-mask change on the variable into a /// per-manager , which batches /// samples and flushes them to the historian. Pass false /// to disable automatic capture — only explicit /// HistoryUpdate Insert calls will populate the archive /// in that case. /// /// /// Optional buffering / batching tuning for the per-manager /// capture sink. Only the first Historize(autoCapture: true) /// call on a builder takes effect; subsequent calls share the /// same sink with its original options. null uses the /// defaults in . /// public static IVariableBuilder Historize( this IVariableBuilder variable, byte historyAccessLevel = AccessLevels.HistoryRead | AccessLevels.HistoryWrite, bool installConfigurationOnBrowse = false, HistorianNodeCapabilities? capabilities = null, IHistorianProvider? provider = null, bool autoCapture = true, HistorianCaptureOptions? captureOptions = null) { if (variable == null) { throw new ArgumentNullException(nameof(variable)); } ApplyHistorization( variable.Builder, variable.Node, historyAccessLevel, installConfigurationOnBrowse, capabilities, provider, autoCapture, captureOptions); return variable; } /// /// Untyped overload of /// /// for callers that already hold an /// view of a /// . /// public static INodeBuilder Historize( this INodeBuilder variable, byte historyAccessLevel = AccessLevels.HistoryRead | AccessLevels.HistoryWrite, bool installConfigurationOnBrowse = false, HistorianNodeCapabilities? capabilities = null, IHistorianProvider? provider = null, bool autoCapture = true, HistorianCaptureOptions? captureOptions = null) { if (variable == null) { throw new ArgumentNullException(nameof(variable)); } ApplyHistorization( variable.Builder, variable.Node, historyAccessLevel, installConfigurationOnBrowse, capabilities, provider, autoCapture, captureOptions); return variable; } /// /// Binds to the wrapped variable's /// NodeId in the server-wide registry. The next /// /// call without an explicit provider argument will dispatch /// through this binding instead of the per-manager default. /// /// /// Useful when a single node manager spans many historized /// variables that each live in a different time-series store /// (e.g., one provider per signal vendor). /// /// The CLR value type of the historized variable. public static IVariableBuilder WithHistorian( this IVariableBuilder variable, IHistorianProvider provider) { if (variable == null) { throw new ArgumentNullException(nameof(variable)); } if (provider == null) { throw new ArgumentNullException(nameof(provider)); } IServerInternal server = GetServer(variable.Builder); IHistorianRegistryProvider registryHost = RequireRegistryHost(server); registryHost.HistorianRegistry.RegisterForNode(variable.Node.NodeId, provider); return variable; } private static void ApplyHistorization( INodeManagerBuilder nodeManagerBuilder, BaseVariableState variable, byte historyAccessLevel, bool installConfigurationOnBrowse, HistorianNodeCapabilities? capabilities, IHistorianProvider? perCallProvider, bool autoCapture, HistorianCaptureOptions? captureOptions) { if (perCallProvider != null) { // Per-call override path — bypass the cached per-manager // builder so the override doesn't leak into other variables. IServerInternal serverForBinding = GetServer(nodeManagerBuilder); IHistorianRegistryProvider registryHost = RequireRegistryHost(serverForBinding); variable.Historizing = true; if (historyAccessLevel != 0) { variable.AccessLevel = (byte)(variable.AccessLevel | historyAccessLevel); variable.UserAccessLevel = (byte)(variable.UserAccessLevel | historyAccessLevel); } RegisterVariableOnProvider(perCallProvider, variable.NodeId, capabilities); registryHost.HistorianRegistry.RegisterForNode(variable.NodeId, perCallProvider); if (installConfigurationOnBrowse || autoCapture) { // Reuse the cached per-manager builder so the // configuration-install hook and the auto-capture // sink share infrastructure. Bind the per-call // provider into the builder if it has no provider // yet so auto-capture flows samples to the right // engine. HistorianBuilder manager = GetOrCreateBuilder(nodeManagerBuilder); HistorianBuilder scope = manager.Provider != null ? manager : manager.UseProvider(perCallProvider); scope.Historize( variable, historyAccessLevel: 0, setHistorizing: false, installConfigurationNode: false, installConfigurationOnBrowse: installConfigurationOnBrowse, systemContext: nodeManagerBuilder.Context, capabilities: capabilities, autoCapture: autoCapture, captureOptions: captureOptions); } return; } // Default path — share the per-manager HistorianBuilder. HistorianBuilder managerBuilder = GetOrCreateBuilder(nodeManagerBuilder); if (managerBuilder.Provider == null) { // Lazy default — install an in-memory engine and make it // the server-wide default so dispatch picks it up. managerBuilder.UseInMemory(); managerBuilder.RegisterAsDefault(); } managerBuilder.Historize( variable, historyAccessLevel: historyAccessLevel, setHistorizing: true, installConfigurationNode: false, installConfigurationOnBrowse: installConfigurationOnBrowse, systemContext: nodeManagerBuilder.Context, capabilities: capabilities, autoCapture: autoCapture, captureOptions: captureOptions); } private static void RegisterVariableOnProvider( IHistorianProvider provider, NodeId nodeId, HistorianNodeCapabilities? capabilities) { // The bundled InMemory engine exposes a typed Register(NodeId, caps); // other providers are responsible for their own internal mapping // when they observe HistoryRead/HistoryUpdate for the node. if (provider is InMemoryHistorianProvider inMemory) { inMemory.Register(nodeId, capabilities); } } private static HistorianBuilder GetOrCreateBuilder(INodeManagerBuilder builder) { return s_builders.GetValue( builder, static b => { IServerInternal server = GetServer(b); return new HistorianBuilder(server); }); } private static IServerInternal GetServer(INodeManagerBuilder builder) { if (builder.Context is ServerSystemContext serverContext && serverContext.Server != null) { return serverContext.Server; } throw new InvalidOperationException( "INodeManagerBuilder.Context does not expose an IServerInternal — " + "the fluent historian extensions require the standard ServerSystemContext host."); } private static IHistorianRegistryProvider RequireRegistryHost(IServerInternal server) { if (server is IHistorianRegistryProvider registryHost) { return registryHost; } throw new InvalidOperationException( "The server does not implement IHistorianRegistryProvider; " + "the fluent historian extensions require the standard ServerInternalData host."); } private static readonly ConditionalWeakTable s_builders = new(); } }