/* ========================================================================
* 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.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Opc.Ua.Server.Historian;
using Opc.Ua.Server.NodeManager;
namespace Opc.Ua.Server
{
///
/// A sample implementation of the IAsyncNodeManager interface.
///
///
/// This node manager is a base class used in multiple samples. It implements the IAsyncNodeManager
/// interface and allows sub-classes to override only the methods that they need. This example
/// is not part of the SDK because most real implementations of a INodeManager will need to
/// modify the behavior of the base class.
///
public class AsyncCustomNodeManager :
IAsyncNodeManager,
INodeIdFactory,
IDisposable
{
///
/// Initializes the node manager.
///
protected AsyncCustomNodeManager(
IServerInternal server,
params string[] namespaceUris)
: this(server, null!, false, namespaceUris)
{
}
///
/// Initializes the node manager.
///
protected AsyncCustomNodeManager(
IServerInternal server,
ILogger logger,
params string[] namespaceUris)
: this(server, null!, logger, namespaceUris)
{
}
///
/// Initializes the node manager.
///
protected AsyncCustomNodeManager(
IServerInternal server,
ApplicationConfiguration configuration,
params string[] namespaceUris)
: this(server, configuration, false, namespaceUris)
{
}
///
/// Initializes the node manager.
///
protected AsyncCustomNodeManager(
IServerInternal server,
ApplicationConfiguration configuration,
ILogger logger,
params string[] namespaceUris)
: this(server, configuration, false, logger, namespaceUris)
{
}
///
/// Initializes the node manager.
///
protected AsyncCustomNodeManager(
IServerInternal server,
ApplicationConfiguration configuration,
bool useSamplingGroups,
params string[] namespaceUris)
: this(
server,
configuration,
useSamplingGroups,
server.Telemetry.CreateLogger(),
namespaceUris)
{
}
///
/// Initializes the node manager.
///
protected AsyncCustomNodeManager(
IServerInternal server,
ApplicationConfiguration configuration,
bool useSamplingGroups,
ILogger logger,
params string[] namespaceUris)
{
// set defaults.
MaxQueueSize = 1000;
MaxDurableQueueSize = 200000; //default value in deprecated Conformance Unit Subscription Durable StorageLevel High
if (configuration?.ServerConfiguration != null)
{
MaxQueueSize = (uint)configuration.ServerConfiguration.MaxNotificationQueueSize;
MaxDurableQueueSize = (uint)configuration.ServerConfiguration
.MaxDurableNotificationQueueSize;
}
// save a reference to the UA server instance that owns the node manager.
Server = server;
m_logger = logger;
// all operations require information about the system
SystemContext = Server.DefaultSystemContext.Copy();
// the node id factory assigns new node ids to new nodes.
// the strategy used by a NodeManager depends on what kind of information it provides.
SystemContext.NodeIdFactory = this;
m_lastUsedNodeId = (uint)DateTime.UtcNow.Ticks & 0x7FFFFFFF;
// add the uris to the server's namespace table and cache the indexes.
ushort[] namespaceIndexes = [];
if (namespaceUris != null)
{
namespaceIndexes = new ushort[namespaceUris.Length];
for (int ii = 0; ii < namespaceUris.Length; ii++)
{
namespaceIndexes[ii] = Server.NamespaceUris.GetIndexOrAppend(namespaceUris[ii]);
}
}
// add the table of namespaces that are used by the NodeManager.
m_namespaceUris = namespaceUris;
m_namespaceIndexes = namespaceIndexes;
m_syncNodeManager = (INodeManager3)this.ToSyncNodeManager();
// create a monitored item manager that owns sampling groups / monitoredNodes
if (useSamplingGroups)
{
m_monitoredItemManager = new SamplingGroupMonitoredItemManager(
this,
server,
configuration!);
}
else
{
m_monitoredItemManager = new MonitoredNodeMonitoredItemManager(this, server);
}
PredefinedNodes = [];
RootNotifiers = [];
}
///
/// Frees any unmanaged resources.
///
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
///
/// An overrideable version of the Dispose.
///
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
m_writeSemaphore.Wait(500);
try
{
PredefinedNodes.Clear();
}
finally
{
m_writeSemaphore.Release();
}
m_writeSemaphore.Dispose();
m_monitoredItemSemaphore.Wait(500);
try
{
m_monitoredItemManager?.Dispose();
}
finally
{
m_monitoredItemSemaphore.Release();
}
m_monitoredItemSemaphore.Dispose();
m_componentCacheSemaphore.Dispose();
}
}
///
/// Creates the NodeId for the specified node.
///
/// The context.
/// The node.
/// The new NodeId.
public virtual NodeId New(ISystemContext context, NodeState node)
{
if (node.NodeId.IsNull)
{
uint id = Utils.IncrementIdentifier(ref m_lastUsedNodeId);
return new NodeId(id, m_namespaceIndexes[0]);
}
return node.NodeId;
}
///
/// Gets the server that the node manager belongs to.
///
public IServerInternal Server { get; }
///
/// The default context to use.
///
public ServerSystemContext SystemContext { get; }
///
/// Gets the default index for the node manager's namespace.
///
public ushort NamespaceIndex => m_namespaceIndexes[0];
///
/// Gets the namespace indexes owned by the node manager.
///
/// The namespace indexes.
public IReadOnlyList NamespaceIndexes => m_namespaceIndexes;
///
/// Gets or sets the maximum size of a monitored item queue.
///
/// The maximum size of a monitored item queue.
public uint MaxQueueSize { get; set; }
///
/// Gets or sets the maximum size of a durable monitored item queue.
///
/// The maximum size of a durable monitored item queue.
public uint MaxDurableQueueSize { get; set; }
///
/// The root for the alias assigned to the node manager.
///
public string AliasRoot { get; set; } = null!;
///
/// The predefined nodes managed by the node manager.
///
protected NodeIdDictionary PredefinedNodes { get; }
///
/// The root notifiers for the node manager.
///
protected NodeIdDictionary RootNotifiers { get; }
///
/// Gets the table of nodes being monitored.
///
protected NodeIdDictionary MonitoredNodes
=> m_monitoredItemManager.MonitoredNodes;
///
/// Gets the table of monitored items managed by the node manager.
///
protected ConcurrentDictionary MonitoredItems
=> m_monitoredItemManager.MonitoredItems;
///
public INodeManager SyncNodeManager => m_syncNodeManager;
///
/// Marks a node for dynamic scaling of event consumer tasks. When a
/// monitored node is created for a node in this set, the
/// will scale its consumer tasks with
/// the number of event monitored items. The Server node
/// () is always auto-opted-in.
///
/// The of the node to opt in.
///
/// true to opt in (default); false to opt out.
///
protected void AllowMultipleEventConsumers(NodeId nodeId, bool enable = true)
{
if (nodeId.IsNull)
{
throw new ArgumentException("NodeId must not be null.", nameof(nodeId));
}
if (enable)
{
MultiConsumerNodeIds[nodeId] = true;
}
else
{
MultiConsumerNodeIds.Remove(nodeId);
}
}
///
public bool IsMultipleEventConsumerNode(NodeId nodeId)
{
return MultiConsumerNodeIds.ContainsKey(nodeId);
}
///
/// Sets the namespaces supported by the NodeManager.
///
/// The namespace uris.
protected void SetNamespaces(params string[] namespaceUris)
{
// add the uris to the server's namespace table and cache the indexes.
ushort[] namespaceIndexes = new ushort[namespaceUris.Length];
for (int ii = 0; ii < namespaceUris.Length; ii++)
{
namespaceIndexes[ii] = Server.NamespaceUris.GetIndexOrAppend(namespaceUris[ii]);
}
// create the immutable table of namespaces that are used by the NodeManager.
m_namespaceUris = namespaceUris;
m_namespaceIndexes = namespaceIndexes;
}
///
/// Sets the namespace indexes supported by the NodeManager.
///
protected void SetNamespaceIndexes(ushort[] namespaceIndexes)
{
string[] namespaceUris = new string[namespaceIndexes.Length];
for (int ii = 0; ii < namespaceIndexes.Length; ii++)
{
namespaceUris[ii] = Server.NamespaceUris.GetString(namespaceIndexes[ii])!;
}
// create the immutable table of namespaces that are used by the NodeManager.
m_namespaceUris = namespaceUris;
m_namespaceIndexes = namespaceIndexes;
}
///
/// Returns true if the namespace for the node id is one of the namespaces managed by the node manager.
///
///
/// It is thread safe to call this method outside the node manager lock.
///
/// The node id to check.
/// True if the namespace is one of the nodes.
protected virtual bool IsNodeIdInNamespace(NodeId nodeId)
{
// nulls are never a valid node.
if (nodeId.IsNull)
{
return false;
}
// quickly exclude nodes that not in the namespace.
return m_namespaceIndexes.Contains(nodeId.NamespaceIndex);
}
///
/// Returns the node if the handle refers to a node managed by this manager.
///
///
/// It is thread safe to call this method outside the node manager lock.
///
/// The handle to check.
/// Non-null if the handle belongs to the node manager.
protected virtual NodeHandle? IsHandleInNamespace(object managerHandle)
{
if (managerHandle is not NodeHandle source)
{
return null;
}
if (!IsNodeIdInNamespace(source.NodeId))
{
return null;
}
return source;
}
///
/// Returns the state object for the specified node if it exists.
///
public NodeState? Find(NodeId nodeId)
{
if (PredefinedNodes.TryGetValue(nodeId, out NodeState? node))
{
return node;
}
return null;
}
///
/// Creates a new instance and assigns unique identifiers to all children.
///
/// The operation context.
/// An optional parent identifier.
/// The reference type from the parent.
/// The browse name.
/// The instance to create.
/// The cancellation token.
/// The new node id.
///
public async ValueTask CreateNodeAsync(
ServerSystemContext context,
NodeId parentId,
NodeId referenceTypeId,
QualifiedName browseName,
BaseInstanceState instance,
CancellationToken cancellationToken = default)
{
ServerSystemContext contextToUse = SystemContext.Copy(context);
instance.ReferenceTypeId = referenceTypeId;
if (!parentId.IsNull)
{
if (!PredefinedNodes.TryGetValue(parentId, out NodeState? parent))
{
throw ServiceResultException.Create(
StatusCodes.BadNodeIdUnknown,
"Cannot find parent with id: {0}",
parentId);
}
parent.AddChild(instance);
}
instance.Create(contextToUse, default, browseName, default, true);
await AddPredefinedNodeAsync(contextToUse, instance, cancellationToken).ConfigureAwait(false);
NodeId resultId = instance.NodeId;
if (ModelChangeEmissionEnabled)
{
ModelChangeAggregator.RecordNodeAdded(resultId, instance.TypeDefinitionId);
if (!parentId.IsNull)
{
ModelChangeAggregator.RecordReferenceAdded(parentId);
}
EmitModelChange(contextToUse);
}
return resultId;
}
///
/// Add a created instance and its children to the NodeManagers AddressSpace. Assigns NodeIds if needed and fixes ReferenceTargets after assigning NodeIds.
///
/// The operation context.
/// An optional parent identifier.
/// The instance to create.
/// The cancellation token.
/// The node id of the Node that was added.
///
public async ValueTask AddNodeAsync(
ServerSystemContext context,
NodeId parentId,
BaseInstanceState instance,
CancellationToken cancellationToken = default)
{
ServerSystemContext contextToUse = SystemContext.Copy(context);
if (!parentId.IsNull)
{
if (!PredefinedNodes.TryGetValue(parentId, out NodeState? parent))
{
throw ServiceResultException.Create(
StatusCodes.BadNodeIdUnknown,
"Cannot find parent with id: {0}",
parentId);
}
parent.AddChild(instance);
}
var mappingTable = new Dictionary();
instance.AssignNodeIds(context, mappingTable);
instance.UpdateReferenceTargets(context, mappingTable);
await AddPredefinedNodeAsync(contextToUse, instance, cancellationToken).ConfigureAwait(false);
NodeId resultId = instance.NodeId;
if (ModelChangeEmissionEnabled)
{
ModelChangeAggregator.RecordNodeAdded(resultId, instance.TypeDefinitionId);
if (!parentId.IsNull)
{
ModelChangeAggregator.RecordReferenceAdded(parentId);
}
EmitModelChange(contextToUse);
}
return resultId;
}
///
/// Adds an already-constructed node subtree (a "predefined" subtree)
/// to this manager's address space while preserving the caller-supplied
/// NodeIds.
///
///
///
/// Use this when a node is materialized dynamically at runtime and the
/// caller has already allocated NodeIds for the root and every child
/// (e.g. via ). Unlike the
///
/// overload, this method does not
/// run AssignNodeIds โ the NodeIds set on the instance and its
/// children are kept verbatim and used as PredefinedNodes keys.
///
///
/// The supplied should already be attached to
/// its parent (via parent.AddChild) when invoked, if a parent
/// linkage is required for browse references.
///
///
/// The pre-built node subtree to register.
/// The cancellation token.
///
/// is null.
///
public async ValueTask AddPredefinedNodeAsync(NodeState node, CancellationToken cancellationToken = default)
{
if (node == null)
{
throw new ArgumentNullException(nameof(node));
}
await AddPredefinedNodeAsync(SystemContext, node, cancellationToken).ConfigureAwait(false);
}
///
/// Replaces an existing PredefinedNodes entry in place. Used by
/// runtime upgraders (e.g. ) that
/// swap a passive child with a typed proxy after the original
/// nodeset was loaded.
///
///
/// The replacement only affects the manager's address-space index.
/// Parent linkage and child traversal must already be wired up by
/// the caller via parent.ReplaceChild.
///
/// The NodeId whose entry should be replaced.
/// The replacement node state.
/// if an existing entry was replaced.
internal bool ReplacePredefinedNode(NodeId nodeId, NodeState node)
{
if (nodeId.IsNull || node == null)
{
return false;
}
if (!PredefinedNodes.ContainsKey(nodeId))
{
return false;
}
PredefinedNodes[nodeId] = node;
return true;
}
///
/// Deletes a node and all of its children.
///
public async ValueTask DeleteNodeAsync(ServerSystemContext context, NodeId nodeId, CancellationToken cancellationToken = default)
{
ServerSystemContext contextToUse = SystemContext.Copy(context);
var referencesToRemove = new List();
if (!PredefinedNodes.TryGetValue(nodeId, out NodeState? node))
{
return false;
}
NodeId? deletedTypeDefinition = (node as BaseInstanceState)?.TypeDefinitionId;
await RemovePredefinedNodeAsync(contextToUse, node!, referencesToRemove, cancellationToken).ConfigureAwait(false);
await RemoveRootNotifierAsync(node!, cancellationToken).ConfigureAwait(false);
if (referencesToRemove.Count > 0)
{
await Server.NodeManager.RemoveReferencesAsync(referencesToRemove, cancellationToken).ConfigureAwait(false);
}
if (ModelChangeEmissionEnabled)
{
ModelChangeAggregator.RecordNodeDeleted(nodeId, deletedTypeDefinition);
EmitModelChange(contextToUse);
}
return true;
}
///
///
/// Defaults to false so that existing node managers do not
/// accept NodeManagement service requests. Sub-classes override this
/// property to opt in.
///
public virtual bool AllowNodeManagement => false;
///
public virtual async ValueTask<(ServiceResult result, NodeId addedNodeId)> AddNodeAsync(
OperationContext context,
AddNodesItem item,
CancellationToken cancellationToken = default)
{
if (item == null)
{
return (new ServiceResult(StatusCodes.BadNothingToDo), NodeId.Null);
}
if (item.BrowseName.IsNull)
{
return (new ServiceResult(StatusCodes.BadBrowseNameInvalid), NodeId.Null);
}
ServerSystemContext systemContext = SystemContext.Copy(context);
var parentNodeId = ExpandedNodeId.ToNodeId(item.ParentNodeId, Server.NamespaceUris);
if (parentNodeId.IsNull)
{
return (new ServiceResult(StatusCodes.BadParentNodeIdInvalid), NodeId.Null);
}
var typeDefinitionId = ExpandedNodeId.ToNodeId(item.TypeDefinition, Server.NamespaceUris);
BaseInstanceState instance;
try
{
instance = CreateInstanceForAddNodes(item, typeDefinitionId);
}
catch (ServiceResultException ex)
{
return (new ServiceResult(ex), NodeId.Null);
}
instance.ReferenceTypeId = item.ReferenceTypeId;
instance.BrowseName = item.BrowseName;
if (instance.DisplayName.IsNull)
{
instance.DisplayName = new LocalizedText(item.BrowseName.Name);
}
// Resolve target NodeId: honour caller-supplied NodeId when present and
// valid, otherwise allocate a fresh one from this manager's namespace.
// Some derived classes override `New(...)` to produce hierarchical
// child identifiers from a parent; for service-set AddNodes the
// instance has no parent yet, so we fall back to the base
// identifier allocator to guarantee a non-null NodeId.
NodeId newNodeId;
if (item.RequestedNewNodeId.IsNull)
{
newNodeId = New(systemContext, instance);
if (newNodeId.IsNull)
{
newNodeId = new NodeId(
Utils.IncrementIdentifier(ref m_lastUsedNodeId),
NamespaceIndex);
}
}
else
{
newNodeId = ExpandedNodeId.ToNodeId(item.RequestedNewNodeId, Server.NamespaceUris);
if (newNodeId.IsNull || !IsNodeIdInNamespace(newNodeId))
{
return (new ServiceResult(StatusCodes.BadNodeIdRejected), NodeId.Null);
}
if (PredefinedNodes.ContainsKey(newNodeId))
{
return (new ServiceResult(StatusCodes.BadNodeIdExists), NodeId.Null);
}
}
instance.NodeId = newNodeId;
// Detect duplicate browse name beneath the parent. For local
// parents we use the in-memory child list. For cross-NodeManager
// parents we scan THIS manager's PredefinedNodes for siblings
// that hold an inverse reference to the same parent โ this
// catches duplicates among nodes created via the SDK in this
// NodeManager.
if (PredefinedNodes.TryGetValue(parentNodeId, out NodeState? parentNode))
{
var existingChildren = new List();
parentNode.GetChildren(systemContext, existingChildren);
foreach (BaseInstanceState child in existingChildren)
{
if (child.BrowseName == item.BrowseName)
{
return (new ServiceResult(StatusCodes.BadBrowseNameDuplicated), NodeId.Null);
}
}
parentNode.AddChild(instance);
}
else
{
foreach (NodeState existing in PredefinedNodes.Values)
{
if (existing is BaseInstanceState sibling &&
sibling.BrowseName == item.BrowseName &&
existing.ReferenceExists(item.ReferenceTypeId, true, parentNodeId))
{
return (new ServiceResult(StatusCodes.BadBrowseNameDuplicated), NodeId.Null);
}
}
// Cross-NodeManager parent: write the inverse edge so Browse from
// the child still resolves the parent. The forward edge from
// parent to child is written below via Server.NodeManager.
instance.AddReference(item.ReferenceTypeId, true, parentNodeId);
}
try
{
await AddPredefinedNodeAsync(systemContext, instance, cancellationToken).ConfigureAwait(false);
}
catch (ServiceResultException ex)
{
return (new ServiceResult(ex), NodeId.Null);
}
// Always add the forward edge from parent โ child via the master so
// the parent's owning NodeManager records it (works whether parent
// is local or remote).
try
{
var forward = new List
{
new NodeStateReference(item.ReferenceTypeId, false, instance.NodeId)
};
await Server.NodeManager.AddReferencesAsync(
parentNodeId, forward, cancellationToken).ConfigureAwait(false);
}
catch (ServiceResultException ex)
{
return (new ServiceResult(ex), NodeId.Null);
}
if (ModelChangeEmissionEnabled)
{
ModelChangeAggregator.RecordNodeAdded(instance.NodeId, instance.TypeDefinitionId);
ModelChangeAggregator.RecordReferenceAdded(parentNodeId);
EmitModelChange(systemContext);
}
return (ServiceResult.Good, instance.NodeId);
}
///
public virtual async ValueTask DeleteNodeAsync(
OperationContext context,
DeleteNodesItem item,
CancellationToken cancellationToken = default)
{
if (item == null)
{
return new ServiceResult(StatusCodes.BadNothingToDo);
}
if (item.NodeId.IsNull)
{
return new ServiceResult(StatusCodes.BadNodeIdInvalid);
}
ServerSystemContext systemContext = SystemContext.Copy(context);
if (!PredefinedNodes.TryGetValue(item.NodeId, out NodeState? node))
{
return new ServiceResult(StatusCodes.BadNodeIdUnknown);
}
NodeId? deletedTypeDefinition = (node as BaseInstanceState)?.TypeDefinitionId;
var referencesToRemove = new List();
await RemovePredefinedNodeAsync(systemContext, node!, referencesToRemove, cancellationToken)
.ConfigureAwait(false);
await RemoveRootNotifierAsync(node!, cancellationToken).ConfigureAwait(false);
if (item.DeleteTargetReferences && referencesToRemove.Count > 0)
{
await Server.NodeManager.RemoveReferencesAsync(referencesToRemove, cancellationToken)
.ConfigureAwait(false);
}
if (ModelChangeEmissionEnabled)
{
ModelChangeAggregator.RecordNodeDeleted(item.NodeId, deletedTypeDefinition);
EmitModelChange(systemContext);
}
return ServiceResult.Good;
}
///
public virtual ValueTask AddReferenceAsync(
OperationContext context,
AddReferencesItem item,
CancellationToken cancellationToken = default)
{
if (item == null)
{
return new ValueTask(new ServiceResult(StatusCodes.BadNothingToDo));
}
if (item.SourceNodeId.IsNull)
{
return new ValueTask(new ServiceResult(StatusCodes.BadSourceNodeIdInvalid));
}
if (item.ReferenceTypeId.IsNull)
{
return new ValueTask(new ServiceResult(StatusCodes.BadReferenceTypeIdInvalid));
}
if (item.TargetNodeId.IsNull)
{
return new ValueTask(new ServiceResult(StatusCodes.BadTargetNodeIdInvalid));
}
if (!PredefinedNodes.TryGetValue(item.SourceNodeId, out NodeState? source))
{
return new ValueTask(new ServiceResult(StatusCodes.BadSourceNodeIdInvalid));
}
bool isInverse = !item.IsForward;
if (source.ReferenceExists(item.ReferenceTypeId, isInverse, item.TargetNodeId))
{
return new ValueTask(new ServiceResult(StatusCodes.BadDuplicateReferenceNotAllowed));
}
source.AddReference(item.ReferenceTypeId, isInverse, item.TargetNodeId);
return new ValueTask(ServiceResult.Good);
}
///
public virtual ValueTask DeleteReferenceAsync(
OperationContext context,
DeleteReferencesItem item,
CancellationToken cancellationToken = default)
{
if (item == null)
{
return new ValueTask(new ServiceResult(StatusCodes.BadNothingToDo));
}
if (item.SourceNodeId.IsNull)
{
return new ValueTask(new ServiceResult(StatusCodes.BadSourceNodeIdInvalid));
}
if (item.ReferenceTypeId.IsNull)
{
return new ValueTask(new ServiceResult(StatusCodes.BadReferenceTypeIdInvalid));
}
if (!PredefinedNodes.TryGetValue(item.SourceNodeId, out NodeState? source))
{
return new ValueTask(new ServiceResult(StatusCodes.BadSourceNodeIdInvalid));
}
bool isInverse = !item.IsForward;
bool removed = source.RemoveReference(item.ReferenceTypeId, isInverse, item.TargetNodeId);
if (!removed)
{
return new ValueTask(new ServiceResult(StatusCodes.BadNoMatch));
}
return new ValueTask(ServiceResult.Good);
}
///
/// Creates a NodeState for an based on the
/// requested node class and any node attributes the caller provided.
///
///
/// Thrown when the supplied attributes are not valid for the node class
/// or when the node class is not supported.
///
private static BaseInstanceState CreateInstanceForAddNodes(
AddNodesItem item,
NodeId typeDefinitionId)
{
switch (item.NodeClass)
{
case NodeClass.Variable:
{
var variable = new BaseDataVariableState(null)
{
TypeDefinitionId = typeDefinitionId.IsNull
? VariableTypeIds.BaseDataVariableType
: typeDefinitionId,
AccessLevel = AccessLevels.CurrentReadOrWrite,
UserAccessLevel = AccessLevels.CurrentReadOrWrite,
DataType = DataTypeIds.BaseDataType,
ValueRank = ValueRanks.Scalar
};
if (!item.NodeAttributes.IsNull)
{
if (!item.NodeAttributes.TryGetValue(out VariableAttributes? attrs) || attrs == null)
{
throw new ServiceResultException(StatusCodes.BadNodeAttributesInvalid);
}
ApplyVariableAttributes(variable, attrs);
}
return variable;
}
case NodeClass.Object:
{
var instance = new BaseObjectState(null)
{
TypeDefinitionId = typeDefinitionId.IsNull
? ObjectTypeIds.BaseObjectType
: typeDefinitionId
};
if (!item.NodeAttributes.IsNull)
{
if (!item.NodeAttributes.TryGetValue(out ObjectAttributes? attrs) || attrs == null)
{
throw new ServiceResultException(StatusCodes.BadNodeAttributesInvalid);
}
ApplyObjectAttributes(instance, attrs);
}
return instance;
}
default:
throw new ServiceResultException(StatusCodes.BadNodeClassInvalid);
}
}
private static void ApplyVariableAttributes(
BaseDataVariableState variable,
VariableAttributes attributes)
{
uint mask = attributes.SpecifiedAttributes;
if ((mask & (uint)NodeAttributesMask.DisplayName) != 0 && !attributes.DisplayName.IsNull)
{
variable.DisplayName = attributes.DisplayName;
}
if ((mask & (uint)NodeAttributesMask.Description) != 0 && !attributes.Description.IsNull)
{
variable.Description = attributes.Description;
}
if ((mask & (uint)NodeAttributesMask.DataType) != 0 && !attributes.DataType.IsNull)
{
variable.DataType = attributes.DataType;
}
if ((mask & (uint)NodeAttributesMask.ValueRank) != 0)
{
variable.ValueRank = attributes.ValueRank;
}
if ((mask & (uint)NodeAttributesMask.AccessLevel) != 0)
{
variable.AccessLevel = attributes.AccessLevel;
}
if ((mask & (uint)NodeAttributesMask.UserAccessLevel) != 0)
{
variable.UserAccessLevel = attributes.UserAccessLevel;
}
if ((mask & (uint)NodeAttributesMask.Historizing) != 0)
{
variable.Historizing = attributes.Historizing;
}
if ((mask & (uint)NodeAttributesMask.MinimumSamplingInterval) != 0)
{
variable.MinimumSamplingInterval = attributes.MinimumSamplingInterval;
}
if ((mask & (uint)NodeAttributesMask.Value) != 0)
{
variable.Value = attributes.Value;
}
}
private static void ApplyObjectAttributes(
BaseObjectState instance,
ObjectAttributes attributes)
{
uint mask = attributes.SpecifiedAttributes;
if ((mask & (uint)NodeAttributesMask.DisplayName) != 0 && !attributes.DisplayName.IsNull)
{
instance.DisplayName = attributes.DisplayName;
}
if ((mask & (uint)NodeAttributesMask.Description) != 0 && !attributes.Description.IsNull)
{
instance.Description = attributes.Description;
}
if ((mask & (uint)NodeAttributesMask.EventNotifier) != 0)
{
instance.EventNotifier = attributes.EventNotifier;
}
}
///
/// Returns the namespaces used by the node manager.
///
///
/// All NodeIds exposed by the node manager must be qualified by a namespace URI. This property
/// returns the URIs used by the node manager. In this example all NodeIds use a single URI.
///
///
public virtual IEnumerable NamespaceUris
{
get => m_namespaceUris!;
protected set
{
if (value == null)
{
throw new ArgumentNullException(nameof(value));
}
var namespaceUris = new List(value);
SetNamespaces([.. namespaceUris]);
}
}
///
/// Does any initialization required before the address space can be used.
///
///
/// The externalReferences is an out parameter that allows the node manager to link to nodes
/// in other node managers. For example, the 'Objects' node is managed by the CoreNodeManager and
/// should have a reference to the root folder node(s) exposed by this node manager.
///
public virtual ValueTask CreateAddressSpaceAsync(
IDictionary> externalReferences,
CancellationToken cancellationToken = default)
{
return LoadPredefinedNodesAsync(SystemContext, externalReferences, cancellationToken);
}
///
/// Loads a node set from a file or resource and adds them to the set of predefined nodes.
///
public virtual async ValueTask LoadPredefinedNodesAsync(
ISystemContext context,
Assembly assembly,
string resourcePath,
IDictionary> externalReferences,
CancellationToken cancellationToken = default)
{
// load the predefined nodes from an XML document.
var predefinedNodes = new NodeStateCollection();
predefinedNodes.LoadFromResource(context, resourcePath, assembly, true);
// add the predefined nodes to the node manager.
for (int ii = 0; ii < predefinedNodes.Count; ii++)
{
await AddPredefinedNodeAsync(context, predefinedNodes[ii], cancellationToken).ConfigureAwait(false);
}
// ensure the reverse references exist.
await AddReverseReferencesAsync(externalReferences, cancellationToken).ConfigureAwait(false);
}
///
/// Loads a node set from a file or resource and adds them to the set of predefined nodes.
///
protected virtual ValueTask LoadPredefinedNodesAsync(ISystemContext context,
CancellationToken cancellationToken = default)
{
return new ValueTask([]);
}
///
/// Loads a node set from a file or resource and adds them to the set of predefined nodes.
///
protected virtual async ValueTask LoadPredefinedNodesAsync(
ISystemContext context,
IDictionary> externalReferences,
CancellationToken cancellationToken = default)
{
// load the predefined nodes from an XML document.
NodeStateCollection predefinedNodes = await LoadPredefinedNodesAsync(context, cancellationToken).ConfigureAwait(false);
// add the predefined nodes to the node manager.
for (int ii = 0; ii < predefinedNodes.Count; ii++)
{
await AddPredefinedNodeAsync(context, predefinedNodes[ii], cancellationToken).ConfigureAwait(false);
}
// ensure the reverse references exist.
await AddReverseReferencesAsync(externalReferences, cancellationToken).ConfigureAwait(false);
}
///
/// Replaces the generic node with a node specific to the model.
///
protected virtual ValueTask AddBehaviourToPredefinedNodeAsync(
ISystemContext context,
NodeState predefinedNode,
CancellationToken cancellationToken = default)
{
return new ValueTask(predefinedNode);
}
///
/// Recursively indexes the node and its children.
///
protected virtual async ValueTask AddPredefinedNodeAsync(ISystemContext context, NodeState node, CancellationToken cancellationToken = default)
{
// assign a default value to any variable in namespace 0
if (node is BaseVariableState nodeStateVar &&
nodeStateVar.NodeId.NamespaceIndex == 0 &&
nodeStateVar.Value.IsNull)
{
nodeStateVar.Value = TypeInfo.GetDefaultVariantValue(
nodeStateVar.DataType,
nodeStateVar.ValueRank,
Server.TypeTree);
}
NodeState activeNode = await AddBehaviourToPredefinedNodeAsync(context, node, cancellationToken).ConfigureAwait(false);
PredefinedNodes.AddOrUpdate(activeNode.NodeId, activeNode, (key, _) => activeNode);
if (activeNode is BaseTypeState type)
{
AddTypesToTypeTree(type);
}
// update the root notifiers.
if (RootNotifiers.TryGetValue(activeNode.NodeId, out NodeState? _))
{
RootNotifiers[activeNode.NodeId] = activeNode;
// need to prevent recursion with the server object.
if (activeNode.NodeId != ObjectIds.Server)
{
lock (activeNode)
{
activeNode.OnReportEvent = OnReportEvent;
if (!activeNode.ReferenceExists(
ReferenceTypeIds.HasNotifier,
true,
ObjectIds.Server))
{
activeNode.AddReference(
ReferenceTypeIds.HasNotifier,
true,
ObjectIds.Server);
}
}
}
}
var children = new List();
activeNode.GetChildren(context, children);
for (int ii = 0; ii < children.Count; ii++)
{
// Propagate type hierarchy flag from parent to children
if (activeNode.IsPartOfTypeHierarchy)
{
children[ii].IsPartOfTypeHierarchy = true;
}
await AddPredefinedNodeAsync(context, children[ii], cancellationToken).ConfigureAwait(false);
}
}
///
/// Recursively indexes the node and its children.
///
protected virtual async ValueTask RemovePredefinedNodeAsync(
ISystemContext context,
NodeState node,
List referencesToRemove,
CancellationToken cancellationToken = default)
{
if (!PredefinedNodes.TryRemove(node.NodeId, out _))
{
return;
}
node.UpdateChangeMasks(NodeStateChangeMasks.Deleted);
node.ClearChangeMasks(context, false);
await OnNodeRemovedAsync(node, cancellationToken).ConfigureAwait(false);
// remove from the parent.
if (node is BaseInstanceState instance && instance.Parent != null)
{
instance.Parent.RemoveChild(instance);
}
// remove children.
var children = new List();
node.GetChildren(context, children);
for (int ii = 0; ii < children.Count; ii++)
{
node.RemoveChild(children[ii]);
}
for (int ii = 0; ii < children.Count; ii++)
{
await RemovePredefinedNodeAsync(context, children[ii], referencesToRemove, cancellationToken).ConfigureAwait(false);
}
// remove from type table.
if (node is BaseTypeState type)
{
Server.TypeTree.Remove(type.NodeId);
}
// remove inverse references.
var references = new List();
node.GetReferences(context, references);
for (int ii = 0; ii < references.Count; ii++)
{
IReference reference = references[ii];
if (reference.TargetId.IsAbsolute)
{
continue;
}
var referenceToRemove = new LocalReference(
(NodeId)reference.TargetId,
reference.ReferenceTypeId,
!reference.IsInverse,
node.NodeId);
referencesToRemove.Add(referenceToRemove);
}
}
///
/// Called after a node has been deleted.
///
protected virtual ValueTask OnNodeRemovedAsync(NodeState node, CancellationToken cancellationToken = default)
{
// overridden by the sub-class.
return new ValueTask();
}
///
/// Ensures that all reverse references exist.
///
/// A list of references to add to external targets.
/// The cancellation token
protected virtual async ValueTask AddReverseReferencesAsync(
IDictionary> externalReferences,
CancellationToken cancellationToken = default)
{
foreach (KeyValuePair kvp in PredefinedNodes)
{
NodeState source = kvp.Value;
var references = new List();
lock (source)
{
source.GetReferences(SystemContext, references);
}
for (int ii = 0; ii < references.Count; ii++)
{
IReference reference = references[ii];
// nothing to do with external nodes.
if (reference.TargetId.IsNull || reference.TargetId.IsAbsolute)
{
continue;
}
// no need to add HasSubtype references since these are handled via the type table.
if (reference.ReferenceTypeId == ReferenceTypeIds.HasSubtype)
{
continue;
}
var targetId = (NodeId)reference.TargetId;
// check for data type encoding references.
if (reference.IsInverse &&
reference.ReferenceTypeId == ReferenceTypeIds.HasEncoding)
{
Server.TypeTree.AddEncoding(targetId, source.NodeId);
}
// add inverse reference to internal targets.
if (PredefinedNodes.TryGetValue(targetId, out NodeState? target))
{
lock (target)
{
if (!target.ReferenceExists(
reference.ReferenceTypeId,
!reference.IsInverse,
source.NodeId))
{
target.AddReference(
reference.ReferenceTypeId,
!reference.IsInverse,
source.NodeId);
}
}
continue;
}
// check for inverse references to external notifiers.
if (reference.IsInverse &&
reference.ReferenceTypeId == ReferenceTypeIds.HasNotifier)
{
await AddRootNotifierAsync(source, cancellationToken).ConfigureAwait(false);
}
// nothing more to do for references to nodes managed by this manager.
if (IsNodeIdInNamespace(targetId))
{
continue;
}
// add external reference.
AddExternalReference(
targetId,
reference.ReferenceTypeId,
!reference.IsInverse,
source.NodeId,
externalReferences);
}
}
}
///
/// Adds an external reference to the dictionary.
///
protected void AddExternalReference(
NodeId sourceId,
NodeId referenceTypeId,
bool isInverse,
NodeId targetId,
IDictionary> externalReferences)
{
// get list of references to external nodes.
if (!externalReferences.TryGetValue(sourceId, out IList? referencesToAdd))
{
externalReferences[sourceId] = referencesToAdd = [];
}
// add reserve reference from external node.
var referenceToAdd = new ReferenceNode
{
ReferenceTypeId = referenceTypeId,
IsInverse = isInverse,
TargetId = targetId
};
referencesToAdd.Add(referenceToAdd);
}
///
/// Recursively adds the types to the type tree.
///
protected void AddTypesToTypeTree(BaseTypeState type)
{
if (!type.SuperTypeId.IsNull && !Server.TypeTree.IsKnown(type.SuperTypeId))
{
AddTypesToTypeTree(type.SuperTypeId);
}
if (type.NodeClass != NodeClass.ReferenceType)
{
Server.TypeTree.AddSubtype(type.NodeId, type.SuperTypeId);
}
else
{
Server.TypeTree.AddReferenceSubtype(type.NodeId, type.SuperTypeId, type.BrowseName);
}
}
///
/// Recursively adds the types to the type tree.
///
protected void AddTypesToTypeTree(NodeId typeId)
{
if (!PredefinedNodes.TryGetValue(typeId, out NodeState? node))
{
return;
}
if (node is not BaseTypeState type)
{
return;
}
AddTypesToTypeTree(type);
}
///
/// Finds the specified and checks if it is of the expected type.
///
///
/// Returns null if not found or not of the correct type.
public T FindPredefinedNode(NodeId nodeId) where T : NodeState
{
if (nodeId.IsNull)
{
return null!;
}
if (!PredefinedNodes.TryGetValue(nodeId, out NodeState? node))
{
return null!;
}
if (typeof(T) != null && !typeof(T).IsInstanceOfType(node))
{
return null!;
}
return node is T typedNode ? typedNode : null!;
}
///
/// Searches for a method in the ObjectType hierarchy of an instance node.
/// Per OPC UA spec Part 4 section 5.12.2.2, the ObjectType of the Object or a super type
/// of that ObjectType may be the source of a HasComponent reference to the method.
///
/// The system context.
/// The TypeDefinitionId of the object instance.
/// The NodeId of the method to find.
/// The found method state, or null if not found.
private MethodState? FindMethodInTypeHierarchy(ISystemContext context, NodeId typeDefinitionId, NodeId methodId)
{
// A limit to prevent infinite loops in case of a circular type hierarchy in malformed address spaces.
const int maxHierarchyDepth = 100;
int depth = 0;
NodeId typeId = typeDefinitionId;
while (!typeId.IsNull && depth++ < maxHierarchyDepth)
{
NodeState typeNode = FindPredefinedNode(typeId);
if (typeNode != null)
{
MethodState? method = typeNode.FindMethod(context, methodId);
if (method != null)
{
return method;
}
// check for loose coupling via the type node.
if (typeNode.ReferenceExists(ReferenceTypeIds.HasComponent, false, methodId))
{
method = FindPredefinedNode(methodId);
if (method != null)
{
return method;
}
}
}
typeId = Server.TypeTree.FindSuperType(typeId);
}
return null;
}
///
/// Frees any resources allocated for the address space.
///
///
/// Iterates the predefined nodes and invokes on each
/// root node so the and
/// callbacks fire and
/// is reported to subscribers. Nodes that
/// are children of another predefined node are skipped because they are deleted
/// recursively by their parent (avoiding duplicate callbacks).
///
public virtual ValueTask DeleteAddressSpaceAsync(CancellationToken cancellationToken = default)
{
NodeState[] nodes = [.. PredefinedNodes.Values];
ISystemContext context = SystemContext;
foreach (NodeState node in nodes)
{
if (node is BaseInstanceState instance &&
instance.Parent != null &&
!instance.Parent.NodeId.IsNull &&
PredefinedNodes.ContainsKey(instance.Parent.NodeId))
{
continue;
}
node.Delete(context);
}
PredefinedNodes.Clear();
return default;
}
///
/// Returns a unique handle for the node.
///
///
/// This must efficiently determine whether the node belongs to the node manager. If it does belong to
/// NodeManager it should return a handle that does not require the NodeId to be validated again when
/// the handle is passed into other methods such as 'Read' or 'Write'.
///
public virtual async ValueTask