Files
Marc Schier a81d981755 dotnet format sweep (whitespace, IDE, RCS) + code-style enforcer agent fix (#3865)
# Description

Pure code-style sweep across the repo — no behavioral changes. Drives
`dotnet format` info-level diagnostics down on a large slice of the
codebase (Stack, Libraries, Tests, Tools).

Six commits, applied bottom-up in safety order so each batch leaves the
build green:

1. **`style: fix whitespace and final-newline formatting (WHITESPACE,
FINALNEWLINE)`**
   197 hits across ~30 files; pure mechanical cleanup.

2. **`style: apply dotnet format style fixes (IDE rules)`**
IDE-rule batch via `dotnet format style --severity info` —
`var`/explicit type, simplify names, collection expressions, expression
bodies, unused usings, etc.

3. **`style: apply dotnet format analyzers fixes (RCS blank-line
formatting)`**
   Roslynator formatting (RCS0xxx) blank-line and punctuation rules.

4. **`style: convert comments to doc comments and inline local vars
(RCS1181, RCS1124)`**
24 files. RCS1181 promotes `// summary`-style comments above
public/internal members to `/// <summary>` doc comments; RCS1124 inlines
locals used exactly once. Generated `Tools/` files were reverted to keep
the source-generator outputs deterministic.

5. **`docs(agent): correct --diagnostics flag form and verify Roslynator
4.x code fixers`**
Updates `.github/agents/opc-ua-codestyle-enforcer.agent.md` with two
corrections discovered while running this sweep:
- `dotnet format --diagnostics "RULE1 RULE2"` (single quoted,
space-separated) silently fixes nothing on `dotnet format` 10.x — it
parses as one unknown ID and exits 0. The correct form is `--diagnostics
RULE1 --diagnostics RULE2 …`, repeated per rule.
- Roslynator 4.15.0 **does** ship code fixers for
RCS1078/1118/1124/1140/1142/1181/1221/1260 (the previous "no auto-fixer"
list was outdated). RCS1140/1142 default to `Hidden` so need `--severity
info` to surface and fire; RCS1078 requires
`roslynator_empty_string_style = field` (already set in
`.editorconfig`). Only NUnit4002/2046/2010 genuinely lack auto-fixers in
this stack.

6. **`style: format new code from master merge (dotnet format sweep)`**
After merging master into the branch (commit `e3ebc8742`), re-ran the
same three-phase format sweep on the 151 .cs files brought in by the
merge, scoped per project via repeated `--include`. 81 files changed;
same rule families as commits 1–4.
- Hand-fixed `Tests/Opc.Ua.MigrationAnalyzer.Tests/AnalyzerHarness.cs`
for `RCS1174` (redundant `async`/`await` removal) because the auto-fixer
didn't apply for that specific case.
   - Reverted files that the formatter would break:
- `Libraries/Opc.Ua.Server/Server/ServerInternalData.cs` — multi-TFM
analysis disagreement injected `<<<<<<<` / `=======` / `>>>>>>>`
markers.
-
`Tools/Opc.Ua.MigrationAnalyzer.Core/{Core/Stack/Client/ChannelBase.cs,
Core/Stack/Types/IUserIdentityTokenHandler.cs,
Client/Session/Session.cs}` — IDE0005 removed `using
Opc.Ua.Security.Certificates` that IS used (the files are 1.5.378
reference fixtures for the analyzer).
- `Tests/Opc.Ua.Gds.Tests/PushTest.cs` — IDE0002 over-simplified
`OpcUa.ObjectTypeIds` to `ObjectTypeIds`, which then resolves to a
conflicting type.

### What this PR does **not** touch

- No behavioral or semantic logic changes.
- No `.editorconfig` rule promotions (rules stay at their current
severities; this is a backlog-burndown pass, not a rule tightening).
- Generated source under `Tools/` is intentionally excluded.
- Higher-risk rule families (`RCS1090`/`CA2007` `ConfigureAwait`,
`RCS1249` null-forgiving cleanup, `IDE0270`/`IDE0059`) are left for
follow-up sweeps because they need per-TFM build verification or
semantic review.

## Related Issues

_No tracking issue — this is a non-functional style sweep and the
agent-doc correction is a follow-up to the same work. Happy to open one
if the maintainers prefer._

## Checklist

- [ ] I have signed the
[CLA](https://opcfoundation.org/license/cla/ContributorLicenseAgreementv1.0.pdf)
and read the
[CONTRIBUTING](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/CONTRIBUTING.md)
doc.
- [x] I have added tests that prove my fix is effective or that my
feature works and increased code coverage. _(N/A — style-only, no
behavioral change)_
- [x] I have added all necessary documentation. _(agent doc updated with
the two corrections)_
- [x] I have verified that my changes do not introduce (new) build or
analyzer warnings. _(verified clean on `net10.0` and `net48`; only
pre-existing `NETSDK1005`/`NETSDK1207` failures on Tools/AOT projects)_
- [ ] I ran **all** tests locally using the **UA.slnx** solution against
at least .net **framework** and .net **10**, and all passed. _(build
verified on net10 and net48 per batch; full test suite not yet run
locally — relying on CI)_
- [ ] I fixed **all** failing and flaky tests in the CI pipelines and
**all** CodeQL warnings.
- [ ] I have addressed **all** PR feedback received.

---------

Co-authored-by: agent <agent@local>
2026-06-10 08:35:44 +02:00

6479 lines
246 KiB
C#

/* ========================================================================
* 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
{
/// <summary>
/// A sample implementation of the IAsyncNodeManager interface.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
public class AsyncCustomNodeManager :
IAsyncNodeManager,
INodeIdFactory,
IDisposable
{
/// <summary>
/// Initializes the node manager.
/// </summary>
protected AsyncCustomNodeManager(
IServerInternal server,
params string[] namespaceUris)
: this(server, null!, false, namespaceUris)
{
}
/// <summary>
/// Initializes the node manager.
/// </summary>
protected AsyncCustomNodeManager(
IServerInternal server,
ILogger logger,
params string[] namespaceUris)
: this(server, null!, logger, namespaceUris)
{
}
/// <summary>
/// Initializes the node manager.
/// </summary>
protected AsyncCustomNodeManager(
IServerInternal server,
ApplicationConfiguration configuration,
params string[] namespaceUris)
: this(server, configuration, false, namespaceUris)
{
}
/// <summary>
/// Initializes the node manager.
/// </summary>
protected AsyncCustomNodeManager(
IServerInternal server,
ApplicationConfiguration configuration,
ILogger logger,
params string[] namespaceUris)
: this(server, configuration, false, logger, namespaceUris)
{
}
/// <summary>
/// Initializes the node manager.
/// </summary>
protected AsyncCustomNodeManager(
IServerInternal server,
ApplicationConfiguration configuration,
bool useSamplingGroups,
params string[] namespaceUris)
: this(
server,
configuration,
useSamplingGroups,
server.Telemetry.CreateLogger<CustomNodeManager2>(),
namespaceUris)
{
}
/// <summary>
/// Initializes the node manager.
/// </summary>
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 = [];
}
/// <summary>
/// Frees any unmanaged resources.
/// </summary>
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
/// <summary>
/// An overrideable version of the Dispose.
/// </summary>
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();
}
}
/// <summary>
/// Creates the NodeId for the specified node.
/// </summary>
/// <param name="context">The context.</param>
/// <param name="node">The node.</param>
/// <returns>The new NodeId.</returns>
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;
}
/// <summary>
/// Gets the server that the node manager belongs to.
/// </summary>
public IServerInternal Server { get; }
/// <summary>
/// The default context to use.
/// </summary>
public ServerSystemContext SystemContext { get; }
/// <summary>
/// Gets the default index for the node manager's namespace.
/// </summary>
public ushort NamespaceIndex => m_namespaceIndexes[0];
/// <summary>
/// Gets the namespace indexes owned by the node manager.
/// </summary>
/// <value>The namespace indexes.</value>
public IReadOnlyList<ushort> NamespaceIndexes => m_namespaceIndexes;
/// <summary>
/// Gets or sets the maximum size of a monitored item queue.
/// </summary>
/// <value>The maximum size of a monitored item queue.</value>
public uint MaxQueueSize { get; set; }
/// <summary>
/// Gets or sets the maximum size of a durable monitored item queue.
/// </summary>
/// <value>The maximum size of a durable monitored item queue.</value>
public uint MaxDurableQueueSize { get; set; }
/// <summary>
/// The root for the alias assigned to the node manager.
/// </summary>
public string AliasRoot { get; set; } = null!;
/// <summary>
/// The predefined nodes managed by the node manager.
/// </summary>
protected NodeIdDictionary<NodeState> PredefinedNodes { get; }
/// <summary>
/// The root notifiers for the node manager.
/// </summary>
protected NodeIdDictionary<NodeState> RootNotifiers { get; }
/// <summary>
/// Gets the table of nodes being monitored.
/// </summary>
protected NodeIdDictionary<MonitoredNode2> MonitoredNodes
=> m_monitoredItemManager.MonitoredNodes;
/// <summary>
/// Gets the table of monitored items managed by the node manager.
/// </summary>
protected ConcurrentDictionary<uint, IMonitoredItem> MonitoredItems
=> m_monitoredItemManager.MonitoredItems;
/// <inheritdoc/>
public INodeManager SyncNodeManager => m_syncNodeManager;
/// <summary>
/// Marks a node for dynamic scaling of event consumer tasks. When a
/// monitored node is created for a node in this set, the
/// <see cref="MonitoredNode2"/> will scale its consumer tasks with
/// the number of event monitored items. The Server node
/// (<see cref="ObjectIds.Server"/>) is always auto-opted-in.
/// </summary>
/// <param name="nodeId">The <see cref="NodeId"/> of the node to opt in.</param>
/// <param name="enable">
/// <c>true</c> to opt in (default); <c>false</c> to opt out.
/// </param>
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);
}
}
/// <inheritdoc/>
public bool IsMultipleEventConsumerNode(NodeId nodeId)
{
return MultiConsumerNodeIds.ContainsKey(nodeId);
}
/// <summary>
/// Sets the namespaces supported by the NodeManager.
/// </summary>
/// <param name="namespaceUris">The namespace uris.</param>
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;
}
/// <summary>
/// Sets the namespace indexes supported by the NodeManager.
/// </summary>
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;
}
/// <summary>
/// Returns true if the namespace for the node id is one of the namespaces managed by the node manager.
/// </summary>
/// <remarks>
/// It is thread safe to call this method outside the node manager lock.
/// </remarks>
/// <param name="nodeId">The node id to check.</param>
/// <returns>True if the namespace is one of the nodes.</returns>
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);
}
/// <summary>
/// Returns the node if the handle refers to a node managed by this manager.
/// </summary>
/// <remarks>
/// It is thread safe to call this method outside the node manager lock.
/// </remarks>
/// <param name="managerHandle">The handle to check.</param>
/// <returns>Non-null if the handle belongs to the node manager.</returns>
protected virtual NodeHandle? IsHandleInNamespace(object managerHandle)
{
if (managerHandle is not NodeHandle source)
{
return null;
}
if (!IsNodeIdInNamespace(source.NodeId))
{
return null;
}
return source;
}
/// <summary>
/// Returns the state object for the specified node if it exists.
/// </summary>
public NodeState? Find(NodeId nodeId)
{
if (PredefinedNodes.TryGetValue(nodeId, out NodeState? node))
{
return node;
}
return null;
}
/// <summary>
/// Creates a new instance and assigns unique identifiers to all children.
/// </summary>
/// <param name="context">The operation context.</param>
/// <param name="parentId">An optional parent identifier.</param>
/// <param name="referenceTypeId">The reference type from the parent.</param>
/// <param name="browseName">The browse name.</param>
/// <param name="instance">The instance to create.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The new node id.</returns>
/// <exception cref="ServiceResultException"></exception>
public async ValueTask<NodeId> 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;
}
/// <summary>
/// Add a created instance and its children to the NodeManagers AddressSpace. Assigns NodeIds if needed and fixes ReferenceTargets after assigning NodeIds.
/// </summary>
/// <param name="context">The operation context.</param>
/// <param name="parentId">An optional parent identifier.</param>
/// <param name="instance">The instance to create.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The node id of the Node that was added.</returns>
/// <exception cref="ServiceResultException"></exception>
public async ValueTask<NodeId> 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<NodeId, NodeId>();
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;
}
/// <summary>
/// Adds an already-constructed node subtree (a "predefined" subtree)
/// to this manager's address space while preserving the caller-supplied
/// NodeIds.
/// </summary>
/// <remarks>
/// <para>
/// 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 <see cref="IRoleManager.AddRole"/>). Unlike the
/// <see cref="AddNodeAsync(ServerSystemContext, NodeId, BaseInstanceState, CancellationToken)"/>
/// overload, this method does <strong>not</strong>
/// run <c>AssignNodeIds</c> — the NodeIds set on the instance and its
/// children are kept verbatim and used as PredefinedNodes keys.
/// </para>
/// <para>
/// The supplied <paramref name="node"/> should already be attached to
/// its parent (via <c>parent.AddChild</c>) when invoked, if a parent
/// linkage is required for browse references.
/// </para>
/// </remarks>
/// <param name="node">The pre-built node subtree to register.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <exception cref="ArgumentNullException">
/// <paramref name="node"/> is null.
/// </exception>
public async ValueTask AddPredefinedNodeAsync(NodeState node, CancellationToken cancellationToken = default)
{
if (node == null)
{
throw new ArgumentNullException(nameof(node));
}
await AddPredefinedNodeAsync(SystemContext, node, cancellationToken).ConfigureAwait(false);
}
/// <summary>
/// Replaces an existing PredefinedNodes entry in place. Used by
/// runtime upgraders (e.g. <see cref="RoleStateBinding"/>) that
/// swap a passive child with a typed proxy after the original
/// nodeset was loaded.
/// </summary>
/// <remarks>
/// The replacement only affects the manager's address-space index.
/// Parent linkage and child traversal must already be wired up by
/// the caller via <c>parent.ReplaceChild</c>.
/// </remarks>
/// <param name="nodeId">The NodeId whose entry should be replaced.</param>
/// <param name="node">The replacement node state.</param>
/// <returns><see langword="true"/> if an existing entry was replaced.</returns>
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;
}
/// <summary>
/// Deletes a node and all of its children.
/// </summary>
public async ValueTask<bool> DeleteNodeAsync(ServerSystemContext context, NodeId nodeId, CancellationToken cancellationToken = default)
{
ServerSystemContext contextToUse = SystemContext.Copy(context);
var referencesToRemove = new List<LocalReference>();
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;
}
/// <inheritdoc/>
/// <remarks>
/// Defaults to <c>false</c> so that existing node managers do not
/// accept NodeManagement service requests. Sub-classes override this
/// property to opt in.
/// </remarks>
public virtual bool AllowNodeManagement => false;
/// <inheritdoc/>
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<BaseInstanceState>();
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<IReference>
{
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);
}
/// <inheritdoc/>
public virtual async ValueTask<ServiceResult> 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<LocalReference>();
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;
}
/// <inheritdoc/>
public virtual ValueTask<ServiceResult> AddReferenceAsync(
OperationContext context,
AddReferencesItem item,
CancellationToken cancellationToken = default)
{
if (item == null)
{
return new ValueTask<ServiceResult>(new ServiceResult(StatusCodes.BadNothingToDo));
}
if (item.SourceNodeId.IsNull)
{
return new ValueTask<ServiceResult>(new ServiceResult(StatusCodes.BadSourceNodeIdInvalid));
}
if (item.ReferenceTypeId.IsNull)
{
return new ValueTask<ServiceResult>(new ServiceResult(StatusCodes.BadReferenceTypeIdInvalid));
}
if (item.TargetNodeId.IsNull)
{
return new ValueTask<ServiceResult>(new ServiceResult(StatusCodes.BadTargetNodeIdInvalid));
}
if (!PredefinedNodes.TryGetValue(item.SourceNodeId, out NodeState? source))
{
return new ValueTask<ServiceResult>(new ServiceResult(StatusCodes.BadSourceNodeIdInvalid));
}
bool isInverse = !item.IsForward;
if (source.ReferenceExists(item.ReferenceTypeId, isInverse, item.TargetNodeId))
{
return new ValueTask<ServiceResult>(new ServiceResult(StatusCodes.BadDuplicateReferenceNotAllowed));
}
source.AddReference(item.ReferenceTypeId, isInverse, item.TargetNodeId);
return new ValueTask<ServiceResult>(ServiceResult.Good);
}
/// <inheritdoc/>
public virtual ValueTask<ServiceResult> DeleteReferenceAsync(
OperationContext context,
DeleteReferencesItem item,
CancellationToken cancellationToken = default)
{
if (item == null)
{
return new ValueTask<ServiceResult>(new ServiceResult(StatusCodes.BadNothingToDo));
}
if (item.SourceNodeId.IsNull)
{
return new ValueTask<ServiceResult>(new ServiceResult(StatusCodes.BadSourceNodeIdInvalid));
}
if (item.ReferenceTypeId.IsNull)
{
return new ValueTask<ServiceResult>(new ServiceResult(StatusCodes.BadReferenceTypeIdInvalid));
}
if (!PredefinedNodes.TryGetValue(item.SourceNodeId, out NodeState? source))
{
return new ValueTask<ServiceResult>(new ServiceResult(StatusCodes.BadSourceNodeIdInvalid));
}
bool isInverse = !item.IsForward;
bool removed = source.RemoveReference(item.ReferenceTypeId, isInverse, item.TargetNodeId);
if (!removed)
{
return new ValueTask<ServiceResult>(new ServiceResult(StatusCodes.BadNoMatch));
}
return new ValueTask<ServiceResult>(ServiceResult.Good);
}
/// <summary>
/// Creates a NodeState for an <see cref="AddNodesItem"/> based on the
/// requested node class and any node attributes the caller provided.
/// </summary>
/// <exception cref="ServiceResultException">
/// Thrown when the supplied attributes are not valid for the node class
/// or when the node class is not supported.
/// </exception>
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;
}
}
/// <summary>
/// Returns the namespaces used by the node manager.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
/// <exception cref="ArgumentNullException"></exception>
public virtual IEnumerable<string> NamespaceUris
{
get => m_namespaceUris!;
protected set
{
if (value == null)
{
throw new ArgumentNullException(nameof(value));
}
var namespaceUris = new List<string>(value);
SetNamespaces([.. namespaceUris]);
}
}
/// <summary>
/// Does any initialization required before the address space can be used.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
public virtual ValueTask CreateAddressSpaceAsync(
IDictionary<NodeId, IList<IReference>> externalReferences,
CancellationToken cancellationToken = default)
{
return LoadPredefinedNodesAsync(SystemContext, externalReferences, cancellationToken);
}
/// <summary>
/// Loads a node set from a file or resource and adds them to the set of predefined nodes.
/// </summary>
public virtual async ValueTask LoadPredefinedNodesAsync(
ISystemContext context,
Assembly assembly,
string resourcePath,
IDictionary<NodeId, IList<IReference>> 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);
}
/// <summary>
/// Loads a node set from a file or resource and adds them to the set of predefined nodes.
/// </summary>
protected virtual ValueTask<NodeStateCollection> LoadPredefinedNodesAsync(ISystemContext context,
CancellationToken cancellationToken = default)
{
return new ValueTask<NodeStateCollection>([]);
}
/// <summary>
/// Loads a node set from a file or resource and adds them to the set of predefined nodes.
/// </summary>
protected virtual async ValueTask LoadPredefinedNodesAsync(
ISystemContext context,
IDictionary<NodeId, IList<IReference>> 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);
}
/// <summary>
/// Replaces the generic node with a node specific to the model.
/// </summary>
protected virtual ValueTask<NodeState> AddBehaviourToPredefinedNodeAsync(
ISystemContext context,
NodeState predefinedNode,
CancellationToken cancellationToken = default)
{
return new ValueTask<NodeState>(predefinedNode);
}
/// <summary>
/// Recursively indexes the node and its children.
/// </summary>
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<BaseInstanceState>();
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);
}
}
/// <summary>
/// Recursively indexes the node and its children.
/// </summary>
protected virtual async ValueTask RemovePredefinedNodeAsync(
ISystemContext context,
NodeState node,
List<LocalReference> 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<BaseInstanceState>();
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<IReference>();
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);
}
}
/// <summary>
/// Called after a node has been deleted.
/// </summary>
protected virtual ValueTask OnNodeRemovedAsync(NodeState node, CancellationToken cancellationToken = default)
{
// overridden by the sub-class.
return new ValueTask();
}
/// <summary>
/// Ensures that all reverse references exist.
/// </summary>
/// <param name="externalReferences">A list of references to add to external targets.</param>
/// <param name="cancellationToken">The cancellation token</param>
protected virtual async ValueTask AddReverseReferencesAsync(
IDictionary<NodeId, IList<IReference>> externalReferences,
CancellationToken cancellationToken = default)
{
foreach (KeyValuePair<NodeId, NodeState> kvp in PredefinedNodes)
{
NodeState source = kvp.Value;
var references = new List<IReference>();
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);
}
}
}
/// <summary>
/// Adds an external reference to the dictionary.
/// </summary>
protected void AddExternalReference(
NodeId sourceId,
NodeId referenceTypeId,
bool isInverse,
NodeId targetId,
IDictionary<NodeId, IList<IReference>> externalReferences)
{
// get list of references to external nodes.
if (!externalReferences.TryGetValue(sourceId, out IList<IReference>? referencesToAdd))
{
externalReferences[sourceId] = referencesToAdd = [];
}
// add reserve reference from external node.
var referenceToAdd = new ReferenceNode
{
ReferenceTypeId = referenceTypeId,
IsInverse = isInverse,
TargetId = targetId
};
referencesToAdd.Add(referenceToAdd);
}
/// <summary>
/// Recursively adds the types to the type tree.
/// </summary>
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);
}
}
/// <summary>
/// Recursively adds the types to the type tree.
/// </summary>
protected void AddTypesToTypeTree(NodeId typeId)
{
if (!PredefinedNodes.TryGetValue(typeId, out NodeState? node))
{
return;
}
if (node is not BaseTypeState type)
{
return;
}
AddTypesToTypeTree(type);
}
/// <summary>
/// Finds the specified and checks if it is of the expected type.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <returns>Returns null if not found or not of the correct type.</returns>
public T FindPredefinedNode<T>(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!;
}
/// <summary>
/// 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.
/// </summary>
/// <param name="context">The system context.</param>
/// <param name="typeDefinitionId">The TypeDefinitionId of the object instance.</param>
/// <param name="methodId">The NodeId of the method to find.</param>
/// <returns>The found method state, or null if not found.</returns>
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<NodeState>(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<MethodState>(methodId);
if (method != null)
{
return method;
}
}
}
typeId = Server.TypeTree.FindSuperType(typeId);
}
return null;
}
/// <summary>
/// Frees any resources allocated for the address space.
/// </summary>
/// <remarks>
/// Iterates the predefined nodes and invokes <see cref="NodeState.Delete"/> on each
/// root node so the <see cref="NodeState.OnBeforeDelete"/> and
/// <see cref="NodeState.OnAfterDelete"/> callbacks fire and
/// <see cref="NodeStateChangeMasks.Deleted"/> 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).
/// </remarks>
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;
}
/// <summary>
/// Returns a unique handle for the node.
/// </summary>
/// <remarks>
/// 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'.
/// </remarks>
public virtual async ValueTask<object> GetManagerHandleAsync(NodeId nodeId, CancellationToken cancellationToken = default)
{
return await GetManagerHandleAsync(SystemContext, nodeId, null!, cancellationToken).ConfigureAwait(false);
}
/// <summary>
/// Returns a unique handle for the node.
/// </summary>
/// <remarks>
/// It is thread safe to call this method outside the node manager lock.
/// </remarks>
protected virtual ValueTask<NodeHandle> GetManagerHandleAsync(
ServerSystemContext context,
NodeId nodeId,
IDictionary<NodeId, NodeState> cache,
CancellationToken cancellationToken = default)
{
if (!IsNodeIdInNamespace(nodeId))
{
return new ValueTask<NodeHandle>();
}
if (PredefinedNodes.TryGetValue(nodeId, out NodeState? node))
{
return new ValueTask<NodeHandle>(new NodeHandle
{
NodeId = nodeId,
Node = node,
Validated = true
});
}
return new ValueTask<NodeHandle>();
}
/// <summary>
/// This method is used to add bi-directional references to nodes from other node managers.
/// </summary>
/// <remarks>
/// The additional references are optional, however, the NodeManager should support them.
/// </remarks>
public virtual async ValueTask AddReferencesAsync(IDictionary<NodeId, IList<IReference>> references, CancellationToken cancellationToken = default)
{
foreach (KeyValuePair<NodeId, IList<IReference>> current in references)
{
// get the handle.
NodeHandle source = await GetManagerHandleAsync(SystemContext, current.Key, null!, cancellationToken).ConfigureAwait(false);
// only support external references to nodes that are stored in memory.
if (source?.Node == null || !source.Validated)
{
continue;
}
// add reference to external target.
foreach (IReference reference in current.Value)
{
lock (source.Node)
{
if (!source.Node.ReferenceExists(
reference.ReferenceTypeId,
reference.IsInverse,
reference.TargetId))
{
source.Node.AddReference(
reference.ReferenceTypeId,
reference.IsInverse,
reference.TargetId);
}
}
}
}
}
/// <summary>
/// This method is used to delete bi-directional references to nodes from other node managers.
/// </summary>
public virtual async ValueTask<ServiceResult> DeleteReferenceAsync(
object sourceHandle,
NodeId referenceTypeId,
bool isInverse,
ExpandedNodeId targetId,
bool deleteBidirectional,
CancellationToken cancellationToken = default)
{
// get the handle.
NodeHandle? source = IsHandleInNamespace(sourceHandle);
if (source == null)
{
return StatusCodes.BadNodeIdUnknown;
}
// only support external references to nodes that are stored in memory.
if (!source.Validated || source.Node == null)
{
return StatusCodes.BadNotSupported;
}
// only support references to Source Areas.
lock (source.Node)
{
source.Node.RemoveReference(referenceTypeId, isInverse, targetId);
}
if (deleteBidirectional)
{
// check if the target is also managed by this node manager.
if (!targetId.IsAbsolute)
{
NodeHandle? target = await GetManagerHandleAsync(SystemContext, (NodeId)targetId, null!, cancellationToken).ConfigureAwait(false);
if (target != null && target.Validated && target.Node != null)
{
lock (target.Node)
{
target.Node.RemoveReference(referenceTypeId, !isInverse, source.NodeId);
}
}
}
}
return ServiceResult.Good;
}
private static readonly uint[] s_nodeMetaDataAttributes =
[
Attributes.WriteMask,
Attributes.UserWriteMask,
Attributes.DataType,
Attributes.ValueRank,
Attributes.ArrayDimensions,
Attributes.AccessLevel,
Attributes.UserAccessLevel,
Attributes.EventNotifier,
Attributes.Executable,
Attributes.UserExecutable,
Attributes.AccessRestrictions,
Attributes.RolePermissions,
Attributes.UserRolePermissions
];
/// <summary>
/// Returns the basic metadata for the node. Returns null if the node does not exist.
/// </summary>
/// <remarks>
/// This method validates any placeholder handle.
/// </remarks>
public async ValueTask<NodeMetadata> GetNodeMetadataAsync(
OperationContext context,
object targetHandle,
BrowseResultMask resultMask,
CancellationToken cancellationToken = default)
{
ServerSystemContext systemContext = SystemContext.Copy(context);
// check for valid handle.
NodeHandle? handle = IsHandleInNamespace(targetHandle);
if (handle == null)
{
return null!;
}
// validate node.
NodeState? target = await ValidateNodeAsync(systemContext, handle, null!, cancellationToken).ConfigureAwait(false);
if (target == null)
{
return null!;
}
var nodeMetadataValues = new Variant[s_nodeMetaDataAttributes.Length];
// read the attributes.
lock (target)
{
target.ReadAttributes(
systemContext,
ref nodeMetadataValues,
s_nodeMetaDataAttributes);
}
// construct the meta-data object.
var metadata = new NodeMetadata(target, target.NodeId)
{
NodeClass = target.NodeClass,
BrowseName = target.BrowseName,
DisplayName = target.DisplayName,
IsPartOfTypeHierarchy = target.IsPartOfTypeHierarchy
};
if (nodeMetadataValues[0].TryGetValue(out uint writeMask) &&
nodeMetadataValues[1].TryGetValue(out uint userWriteMask))
{
metadata.WriteMask = (AttributeWriteMask)(writeMask & userWriteMask);
}
metadata.DataType = nodeMetadataValues[2].GetNodeId();
if (nodeMetadataValues[3].TryGetValue(out int valueRank))
{
metadata.ValueRank = valueRank;
}
if (nodeMetadataValues[4].TryGetValue(out ArrayOf<uint> arrayDimensions))
{
metadata.ArrayDimensions = arrayDimensions;
}
if (nodeMetadataValues[5].TryGetValue(out byte accessLevel) &&
nodeMetadataValues[6].TryGetValue(out byte userAccessLevel))
{
metadata.AccessLevel = (byte)(accessLevel & userAccessLevel);
}
if (nodeMetadataValues[7].TryGetValue(out byte eventNotifier))
{
metadata.EventNotifier = eventNotifier;
}
if (nodeMetadataValues[8].TryGetValue(out bool executeAble) &&
nodeMetadataValues[9].TryGetValue(out bool userExecuteable))
{
metadata.Executable = executeAble && userExecuteable;
}
if (nodeMetadataValues[10].TryGetValue(out ushort accessRestrictionType))
{
metadata.AccessRestrictions = (AccessRestrictionType)accessRestrictionType;
}
if (nodeMetadataValues[11].TryGetStructure(out ArrayOf<RolePermissionType> rolePermissions))
{
metadata.RolePermissions = [.. rolePermissions];
}
if (nodeMetadataValues[12].TryGetStructure(out ArrayOf<RolePermissionType> userRolePermissions))
{
metadata.UserRolePermissions = [.. userRolePermissions];
}
await SetDefaultPermissionsAsync(systemContext, target, metadata, cancellationToken).ConfigureAwait(false);
// get instance references.
if (target is BaseInstanceState instance)
{
metadata.TypeDefinition = instance.TypeDefinitionId;
metadata.ModellingRule = instance.ModellingRuleId;
}
// fill in the common attributes.
return metadata;
}
/// <summary>
/// Sets the AccessRestrictions, RolePermissions and UserRolePermissions values in the metadata
/// </summary>
/// <exception cref="ArgumentException"></exception>
private static void SetAccessAndRolePermissions(Variant[] values, NodeMetadata metadata)
{
if (values.Length != 3)
{
throw new ArgumentException("Values need to have a length of 3 to contain Access and Rolepermissions",
nameof(values));
}
if (values[0].TryGetValue(out ushort accessRestrictions))
{
metadata.AccessRestrictions = (AccessRestrictionType)accessRestrictions;
}
if (values[1].TryGetStructure(out ArrayOf<RolePermissionType> rolePermissions))
{
metadata.RolePermissions = [.. rolePermissions];
}
if (values[2].TryGetStructure(out ArrayOf<RolePermissionType> userRolePermissions))
{
metadata.UserRolePermissions = [.. userRolePermissions];
}
}
/// <summary>
/// Reads and caches the Attributes used by the AccessRestrictions and RolePermission validation process
/// </summary>
/// <param name="uniqueNodesServiceAttributes">The cache used to save the attributes</param>
/// <param name="systemContext">The context</param>
/// <param name="target">The target for which the attributes are read and cached</param>
/// <param name="key">The key representing the NodeId for which the cache is kept</param>
/// <param name="values">The array to store the values of the attributes</param>
private static void ReadAndCacheValidationAttributes(
Dictionary<NodeId, Variant[]> uniqueNodesServiceAttributes,
ServerSystemContext systemContext,
NodeState target,
NodeId key,
ref Variant[] values)
{
ReadValidationAttributes(systemContext, target, ref values);
uniqueNodesServiceAttributes[key] = values;
}
/// <summary>
/// Reads the Attributes used by the AccessRestrictions and RolePermission validation process
/// </summary>
/// <param name="systemContext">The context</param>
/// <param name="target">The target for which the attributes are read and cached</param>
/// <param name="values">The array to store the values of the attributes</param>
private static void ReadValidationAttributes(
ServerSystemContext systemContext,
NodeState target,
ref Variant[] values)
{
// This is the list of attributes to be populated by GetNodeMetadata from CustomNodeManagers.
// The are originating from services in the context of AccessRestrictions and RolePermission validation.
// For such calls the other attributes are ignored since reading them might trigger unnecessary callbacks
lock (target)
{
target.ReadAttributes(
systemContext,
ref values,
Attributes.AccessRestrictions,
Attributes.RolePermissions,
Attributes.UserRolePermissions);
}
}
/// <summary>
/// Browses the references from a node managed by the node manager.
/// </summary>
/// <remarks>
/// The continuation point is created for every browse operation and contains the browse parameters.
/// The node manager can store its state information in the Data and Index properties.
/// </remarks>
/// <exception cref="ArgumentNullException"><paramref name="continuationPoint"/> is <c>null</c>.</exception>
/// <exception cref="ServiceResultException"></exception>
public async ValueTask<ContinuationPoint?> BrowseAsync(
OperationContext context,
ContinuationPoint continuationPoint,
IList<ReferenceDescription> references,
CancellationToken cancellationToken = default)
{
if (continuationPoint == null)
{
throw new ArgumentNullException(nameof(continuationPoint));
}
ServerSystemContext systemContext = SystemContext.Copy(context);
// check for valid view.
ValidateViewDescription(systemContext, continuationPoint.View!);
// check for valid handle.
NodeHandle handle =
IsHandleInNamespace(continuationPoint.NodeToBrowse)
?? throw new ServiceResultException(StatusCodes.BadNodeIdUnknown);
// validate node.
NodeState source =
await ValidateNodeAsync(systemContext, handle, null!, cancellationToken).ConfigureAwait(false)
?? throw new ServiceResultException(StatusCodes.BadNodeIdUnknown);
// check if node is in the view.
if (!IsNodeInView(systemContext, continuationPoint, source))
{
throw new ServiceResultException(StatusCodes.BadNodeNotInView);
}
// fetch list of references.
if (continuationPoint.Data is not BrowserContext browserContext)
{
INodeBrowser browser;
lock (source)
{
// create a new browser.
browser = source.CreateBrowser(
systemContext,
continuationPoint.View,
continuationPoint.ReferenceTypeId,
continuationPoint.IncludeSubtypes,
continuationPoint.BrowseDirection,
default,
null,
false);
}
continuationPoint.Data = browserContext = new BrowserContext(browser);
}
// prevent multiple access the browser object.
await browserContext.Semaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
try
{
INodeBrowser browser = browserContext.Browser;
// apply filters to references.
var cache = new Dictionary<NodeId, NodeState>();
for (IReference? reference = browser.Next();
reference != null;
reference = browser.Next())
{
// validate Browse permission
ServiceResult serviceResult = await ValidateRolePermissionsAsync(
context,
ExpandedNodeId.ToNodeId(reference.TargetId, Server.NamespaceUris),
PermissionType.Browse,
cancellationToken).ConfigureAwait(false);
if (ServiceResult.IsBad(serviceResult))
{
// ignore reference
continue;
}
// create the type definition reference.
ReferenceDescription description = await GetReferenceDescriptionAsync(
systemContext,
cache,
reference,
continuationPoint,
cancellationToken).ConfigureAwait(false);
if (description == null)
{
continue;
}
// check if limit reached.
if (continuationPoint.MaxResultsToReturn != 0 &&
references.Count >= continuationPoint.MaxResultsToReturn)
{
browser.Push(reference);
return continuationPoint;
}
references.Add(description);
}
}
finally
{
browserContext.Semaphore.Release();
}
// release the continuation point if all done.
continuationPoint.Dispose();
return null!;
}
/// <summary>
/// Validates the view description passed to a browse request (throws on error).
/// </summary>
/// <exception cref="ServiceResultException"></exception>
protected virtual void ValidateViewDescription(
ServerSystemContext context,
ViewDescription view)
{
if (ViewDescription.IsDefault(view))
{
return;
}
_ =
FindPredefinedNode<ViewState>(view.ViewId)
?? throw new ServiceResultException(StatusCodes.BadViewIdUnknown);
if (view.Timestamp != DateTimeUtc.MinValue)
{
throw new ServiceResultException(StatusCodes.BadViewTimestampInvalid);
}
if (view.ViewVersion != 0)
{
throw new ServiceResultException(StatusCodes.BadViewVersionInvalid);
}
}
/// <summary>
/// Checks if the node is in the view.
/// </summary>
protected virtual bool IsNodeInView(
ServerSystemContext context,
ContinuationPoint continuationPoint,
NodeState node)
{
if (continuationPoint == null || ViewDescription.IsDefault(continuationPoint.View))
{
return true;
}
return IsNodeInView(context, continuationPoint!.View!.ViewId, node);
}
/// <summary>
/// Checks if the node is in the view.
/// </summary>
protected virtual bool IsNodeInView(
ServerSystemContext context,
NodeId viewId,
NodeState node)
{
ViewState view = FindPredefinedNode<ViewState>(viewId);
return view != null;
}
/// <summary>
/// Checks if the reference is in the view.
/// </summary>
protected virtual bool IsReferenceInView(
ServerSystemContext context,
ContinuationPoint continuationPoint,
IReference reference)
{
return true;
}
/// <summary>
/// Returns the references for the node that meets the criteria specified.
/// </summary>
protected async ValueTask<ReferenceDescription> GetReferenceDescriptionAsync(
ServerSystemContext context,
Dictionary<NodeId, NodeState> cache,
IReference reference,
ContinuationPoint continuationPoint,
CancellationToken cancellationToken = default)
{
_ = SystemContext.Copy(context);
// create the type definition reference.
var description = new ReferenceDescription { NodeId = reference.TargetId };
description.SetReferenceType(
continuationPoint.ResultMask,
reference.ReferenceTypeId,
!reference.IsInverse);
// check if reference is in the view.
if (!IsReferenceInView(context, continuationPoint, reference))
{
return null!;
}
// do not cache target parameters for remote nodes.
if (reference.TargetId.IsAbsolute)
{
// only return remote references if no node class filter is specified.
if (continuationPoint.NodeClassMask != 0)
{
return null!;
}
return description;
}
NodeState? target = null;
// check for local reference.
if (reference is NodeStateReference referenceInfo)
{
target = referenceInfo.Target;
}
// check for internal reference.
if (target == null)
{
NodeHandle handle = await GetManagerHandleAsync(context, (NodeId)reference.TargetId, null!, cancellationToken).ConfigureAwait(false);
if (handle != null)
{
target = await ValidateNodeAsync(context, handle, null!, cancellationToken).ConfigureAwait(false);
}
}
// the target may be a reference to a node in another node manager. In these cases
// the target attributes must be fetched by the caller. The Unfiltered flag tells the
// caller to do that.
if (target == null)
{
description.Unfiltered = true;
return description;
}
// apply node class filter.
if (continuationPoint.NodeClassMask != 0 &&
((continuationPoint.NodeClassMask & (uint)target.NodeClass) == 0))
{
return null!;
}
// check if target is in the view.
if (!IsNodeInView(context, continuationPoint, target))
{
return null!;
}
// look up the type definition.
NodeId typeDefinition = default;
if (target is BaseInstanceState instance)
{
typeDefinition = instance.TypeDefinitionId;
}
// set target attributes.
description.SetTargetAttributes(
continuationPoint.ResultMask,
target.NodeClass,
target.BrowseName,
target.DisplayName,
typeDefinition);
return description;
}
/// <summary>
/// Returns the target of the specified browse path fragment(s).
/// </summary>
/// <remarks>
/// If reference exists but the node manager does not know the browse name it must
/// return the NodeId as an unresolvedTargetIds. The caller will try to check the
/// browse name.
/// </remarks>
public virtual async ValueTask TranslateBrowsePathAsync(
OperationContext context,
object sourceHandle,
RelativePathElement relativePath,
IList<ExpandedNodeId> targetIds,
IList<NodeId> unresolvedTargetIds,
CancellationToken cancellationToken = default)
{
ServerSystemContext systemContext = SystemContext.Copy(context);
IDictionary<NodeId, NodeState> operationCache = new NodeIdDictionary<NodeState>();
// check for valid handle.
NodeHandle? handle = IsHandleInNamespace(sourceHandle);
if (handle == null)
{
return;
}
// validate node.
NodeState source = await ValidateNodeAsync(systemContext, handle, operationCache, cancellationToken).ConfigureAwait(false);
if (source == null)
{
return;
}
INodeBrowser browser;
// get list of references that relative path.
lock (source)
{
browser = source.CreateBrowser(
systemContext,
null,
relativePath.ReferenceTypeId,
relativePath.IncludeSubtypes,
relativePath.IsInverse ? BrowseDirection.Inverse : BrowseDirection.Forward,
relativePath.TargetName,
null,
false);
}
// check the browse names.
try
{
for (IReference? reference = browser.Next();
reference != null;
reference = browser.Next())
{
// ignore unknown external references.
if (reference.TargetId.IsAbsolute)
{
continue;
}
NodeState? target = null;
// check for local reference.
if (reference is NodeStateReference referenceInfo)
{
target = referenceInfo.Target;
}
if (target == null)
{
var targetId = (NodeId)reference.TargetId;
// the target may be a reference to a node in another node manager.
if (!IsNodeIdInNamespace(targetId))
{
unresolvedTargetIds.Add((NodeId)reference.TargetId);
continue;
}
// look up the target manually.
NodeHandle targetHandle = await GetManagerHandleAsync(
systemContext,
targetId,
operationCache,
cancellationToken).ConfigureAwait(false);
if (targetHandle == null)
{
continue;
}
// validate target.
target = await ValidateNodeAsync(systemContext, targetHandle, operationCache, cancellationToken).ConfigureAwait(false);
if (target == null)
{
continue;
}
}
// check browse name.
if (target.BrowseName == relativePath.TargetName &&
!targetIds.Contains(reference.TargetId))
{
targetIds.Add(reference.TargetId);
}
}
}
finally
{
browser.Dispose();
}
}
/// <summary>
/// Reads the value for the specified attribute.
/// </summary>
public virtual async ValueTask ReadAsync(
OperationContext context,
double maxAge,
ArrayOf<ReadValueId> nodesToRead,
IList<DataValue> values,
IList<ServiceResult> errors,
CancellationToken cancellationToken = default)
{
ServerSystemContext systemContext = SystemContext.Copy(context);
IDictionary<NodeId, NodeState> operationCache = new NodeIdDictionary<NodeState>();
var nodesToValidate = new List<NodeHandle>();
for (int ii = 0; ii < nodesToRead.Count; ii++)
{
ReadValueId nodeToRead = nodesToRead[ii];
// skip items that have already been processed.
if (nodeToRead.Processed)
{
continue;
}
// check for valid handle.
NodeHandle handle = await GetManagerHandleAsync(
systemContext,
nodeToRead.NodeId,
operationCache,
cancellationToken).ConfigureAwait(false);
if (handle == null)
{
continue;
}
// owned by this node manager.
nodeToRead.Processed = true;
// create an initial value (default ctor already sets
// WrappedValue = Variant.Null, ServerTimestamp /
// SourceTimestamp = DateTimeUtc.MinValue, StatusCode = Good).
DataValue value = values[ii] = new DataValue();
// check if the node is a area in memory.
if (handle.Node == null)
{
errors[ii] = StatusCodes.BadNodeIdUnknown;
// must validate node in a separate operation
handle.Index = ii;
nodesToValidate.Add(handle);
continue;
}
// read the attribute value.
(errors[ii], value) = await handle.Node.ReadAttributeAsync(
systemContext,
nodeToRead.AttributeId,
nodeToRead.ParsedIndexRange,
nodeToRead.DataEncoding,
value,
cancellationToken).ConfigureAwait(false);
// Set timestamps after ReadAttribute to ensure consistency
// For Value attributes, match ServerTimestamp to SourceTimestamp
// For other attributes, just ensure ServerTimestamp is set
if (nodeToRead.AttributeId == Attributes.Value)
{
if (value.SourceTimestamp == DateTimeUtc.MinValue)
{
value = value.WithSourceTimestamp(DateTimeUtc.Now);
}
value = value.WithServerTimestamp(value.SourceTimestamp);
}
else
{
// For non-value attributes, only ServerTimestamp is relevant
if (value.ServerTimestamp == DateTimeUtc.MinValue)
{
value = value.WithServerTimestamp(DateTimeUtc.Now);
}
}
// Commit any With-chain rebindings back into the results array.
values[ii] = value;
#if DEBUG
if (nodeToRead.AttributeId == Attributes.Value)
{
m_logger.LogTrace(
Utils.TraceMasks.ServiceDetail,
"READ: NodeId={NodeId} Value={Value} Range={Range}",
nodeToRead.NodeId,
value.WrappedValue,
nodeToRead.IndexRange);
}
#endif
}
// check for nothing to do.
if (nodesToValidate.Count == 0)
{
return;
}
// validates the nodes (reads values from the underlying data source if required).
await ReadAsync(systemContext, nodesToRead, values, errors, nodesToValidate, operationCache, cancellationToken).ConfigureAwait(false);
}
/// <summary>
/// Finds a node in the dynamic cache.
/// </summary>
/// <param name="context">The current context.</param>
/// <param name="handle">The node handle.</param>
/// <param name="cache">The cache to search.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The node if found. Null otherwise.</returns>
protected virtual ValueTask<NodeState> FindNodeInCacheAsync(
ServerSystemContext context,
NodeHandle handle,
IDictionary<NodeId, NodeState> cache,
CancellationToken cancellationToken = default)
{
// not valid if no root.
if (handle == null)
{
return default;
}
// check if previously validated.
if (handle.Validated)
{
return new ValueTask<NodeState>(handle.Node);
}
// construct id for root node.
NodeId rootId = handle.RootId;
if (cache != null)
{
// lookup component in local cache for request.
if (cache.TryGetValue(handle.NodeId, out NodeState? target))
{
return new ValueTask<NodeState>(target);
}
// lookup root in local cache for request.
if (!string.IsNullOrEmpty(handle.ComponentPath) &&
cache.TryGetValue(rootId, out target))
{
NodeState? child;
lock (target)
{
child = target.FindChildBySymbolicName(context, handle.ComponentPath);
}
// component exists.
if (child != null)
{
return new ValueTask<NodeState>(child);
}
}
}
// lookup component in shared cache.
return new ValueTask<NodeState>(LookupNodeInComponentCache(context, handle)!);
}
/// <summary>
/// Marks the handle as validated and saves the node in the dynamic cache.
/// </summary>
protected virtual NodeState ValidationComplete(
ServerSystemContext context,
NodeHandle handle,
NodeState node,
IDictionary<NodeId, NodeState> cache)
{
handle.Node = node;
handle.Validated = true;
if (cache != null && handle != null)
{
cache[handle.NodeId] = node;
}
return node;
}
/// <summary>
/// Verifies that the specified node exists.
/// </summary>
protected virtual async ValueTask<NodeState> ValidateNodeAsync(
ServerSystemContext context,
NodeHandle handle,
IDictionary<NodeId, NodeState> cache,
CancellationToken cancellationToken = default)
{
// lookup in cache.
NodeState target = await FindNodeInCacheAsync(context, handle, cache, cancellationToken).ConfigureAwait(false);
if (target != null)
{
handle.Node = target;
handle.Validated = true;
return handle.Node;
}
// return default.
return handle.Node;
}
/// <summary>
/// Validates the nodes and reads the values from the underlying source.
/// </summary>
/// <param name="context">The context.</param>
/// <param name="nodesToRead">The nodes to read.</param>
/// <param name="values">The values.</param>
/// <param name="errors">The errors.</param>
/// <param name="nodesToValidate">The nodes to validate.</param>
/// <param name="cache">The cache.</param>
/// <param name="cancellationToken">The cancellation token.</param>
protected virtual async ValueTask ReadAsync(
ServerSystemContext context,
ArrayOf<ReadValueId> nodesToRead,
IList<DataValue> values,
IList<ServiceResult> errors,
List<NodeHandle> nodesToValidate,
IDictionary<NodeId, NodeState> cache,
CancellationToken cancellationToken = default)
{
for (int ii = 0; ii < nodesToValidate.Count; ii++)
{
NodeHandle handle = nodesToValidate[ii];
// validate node.
NodeState source = await ValidateNodeAsync(context, handle, cache, cancellationToken).ConfigureAwait(false);
if (source == null)
{
continue;
}
ReadValueId nodeToRead = nodesToRead[handle.Index];
DataValue value = values[handle.Index];
// update the attribute value.
// Async path — the default NodeState.ReadAttributeAsync re-takes
// `lock(this)` around the synchronous fallback when no async
// hook is set, so behaviour is preserved bit-for-bit. When a
// BaseVariableState async hook is set the lock is dropped on
// the await, allowing true async I/O.
(errors[handle.Index], value) = await source.ReadAttributeAsync(
context,
nodeToRead.AttributeId,
nodeToRead.ParsedIndexRange,
nodeToRead.DataEncoding,
value,
cancellationToken).ConfigureAwait(false);
values[handle.Index] = value;
}
}
/// <summary>
/// Writes the value for the specified attributes.
/// </summary>
public virtual async ValueTask WriteAsync(
OperationContext context,
ArrayOf<WriteValue> nodesToWrite,
IList<ServiceResult> errors,
CancellationToken cancellationToken = default)
{
ServerSystemContext systemContext = SystemContext.Copy(context);
IDictionary<NodeId, NodeState> operationCache = new NodeIdDictionary<NodeState>();
var nodesToValidate = new List<NodeHandle>();
await m_writeSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
try
{
for (int ii = 0; ii < nodesToWrite.Count; ii++)
{
WriteValue nodeToWrite = nodesToWrite[ii];
// skip items that have already been processed.
if (nodeToWrite.Processed)
{
continue;
}
// check for valid handle.
NodeHandle handle = await GetManagerHandleAsync(
systemContext,
nodeToWrite.NodeId,
operationCache,
cancellationToken).ConfigureAwait(false);
if (handle == null)
{
continue;
}
// owned by this node manager.
nodeToWrite.Processed = true;
// index range is not supported.
if (nodeToWrite.AttributeId != Attributes.Value &&
!string.IsNullOrEmpty(nodeToWrite.IndexRange))
{
errors[ii] = StatusCodes.BadWriteNotSupported;
continue;
}
// check if the node is a area in memory.
if (handle.Node == null)
{
errors[ii] = StatusCodes.BadNodeIdUnknown;
// must validate node in a separate operation.
handle.Index = ii;
nodesToValidate.Add(handle);
continue;
}
// check if the node is AnalogItem and the values are outside the InstrumentRange.
if (handle.Node is AnalogItemState analogItemState &&
analogItemState.InstrumentRange != null)
{
try
{
Variant doubleVariant = nodeToWrite.Value.WrappedValue
.ConvertTo(BuiltInType.Double);
if (doubleVariant.TypeInfo.IsArray)
{
bool isOutOfRange = false;
foreach (double newValue in doubleVariant.GetDoubleArray())
{
if (newValue > analogItemState.InstrumentRange.Value.High ||
newValue < analogItemState.InstrumentRange.Value.Low)
{
isOutOfRange = true;
break;
}
}
if (isOutOfRange)
{
errors[ii] = StatusCodes.BadOutOfRange;
continue;
}
}
else
{
double newValue = doubleVariant.GetDouble();
if (newValue > analogItemState.InstrumentRange.Value.High ||
newValue < analogItemState.InstrumentRange.Value.Low)
{
errors[ii] = StatusCodes.BadOutOfRange;
continue;
}
}
}
catch
{
//skip the InstrumentRange check if the transformation isn't possible.
}
}
#if DEBUG
m_logger.LogTrace(
Utils.TraceMasks.ServiceDetail,
"WRITE: NodeId={NodeId} Value={Value} Range={Range}",
nodeToWrite.NodeId,
nodeToWrite.Value.WrappedValue,
nodeToWrite.IndexRange);
#endif
var propertyState = handle.Node as PropertyState;
Variant previousPropertyValue = propertyState?.Value ?? default;
Variant oldValue = default;
if (Server?.Auditing == true)
{
//current server supports auditing
// read the old value for the purpose of auditing
var oldDataValue = new DataValue();
(_, oldDataValue) = await handle.Node.ReadAttributeAsync(
systemContext,
nodeToWrite.AttributeId,
nodeToWrite.ParsedIndexRange,
QualifiedName.Null,
oldDataValue,
cancellationToken).ConfigureAwait(false);
oldValue = oldDataValue.WrappedValue;
}
// write the attribute value.
errors[ii] = await handle.Node.WriteAttributeAsync(
systemContext,
nodeToWrite.AttributeId,
nodeToWrite.ParsedIndexRange,
nodeToWrite.Value,
cancellationToken).ConfigureAwait(false);
// report the write value audit event
Server.ReportAuditWriteUpdateEvent(
systemContext,
nodeToWrite,
oldValue,
errors[ii]?.StatusCode ?? StatusCodes.Good,
m_logger);
if (!ServiceResult.IsGood(errors[ii]))
{
continue;
}
if (propertyState != null)
{
CheckIfSemanticsHaveChanged(
systemContext,
propertyState,
nodeToWrite.Value,
previousPropertyValue);
}
//not needed for sampling groups
if (m_monitoredItemManager is MonitoredNodeMonitoredItemManager)
{
// updates to source finished - report changes to monitored items.
handle.Node.ClearChangeMasks(systemContext, true);
}
}
// check for nothing to do.
if (nodesToValidate.Count == 0)
{
return;
}
}
finally
{
m_writeSemaphore.Release();
}
// validates the nodes and writes the value to the underlying system.
await WriteAsync(
systemContext,
nodesToWrite,
errors,
nodesToValidate,
operationCache,
cancellationToken).ConfigureAwait(false);
}
private void CheckIfSemanticsHaveChanged(
ServerSystemContext systemContext,
PropertyState property,
Variant newPropertyValue,
Variant previousPropertyValue)
{
// check if the changed property is one that can trigger semantic changes
string? propertyName = property.BrowseName.Name;
if (propertyName
is not BrowseNames.EURange
and not BrowseNames.InstrumentRange
and not BrowseNames.EngineeringUnits
and not BrowseNames.Title
and not BrowseNames.AxisDefinition
and not BrowseNames.FalseState
and not BrowseNames.TrueState
and not BrowseNames.EnumStrings
and not BrowseNames.XAxisDefinition
and not BrowseNames.YAxisDefinition
and not BrowseNames.ZAxisDefinition)
{
return;
}
// ceck if property value changed
if (Utils.IsEqual(newPropertyValue, previousPropertyValue))
{
return;
}
foreach (KeyValuePair<uint, IMonitoredItem> kvp in MonitoredItems)
{
if (kvp.Value is not ISampledDataChangeMonitoredItem monitoredItem ||
monitoredItem.AttributeId != Attributes.Value)
{
continue;
}
// Try to get the node from the handle
if (monitoredItem.ManagerHandle is not NodeHandle handle || handle.Node == null)
{
continue;
}
NodeState node = handle.Node;
BaseInstanceState? propertyState = node.FindChild(
systemContext,
property!.BrowseName);
if (propertyState != null &&
property != null &&
propertyState.NodeId == property.NodeId)
{
if ((
node is AnalogItemState &&
(propertyName == BrowseNames.EURange ||
propertyName == BrowseNames.EngineeringUnits)
) ||
(
node is TwoStateDiscreteState &&
(propertyName == BrowseNames.FalseState ||
propertyName == BrowseNames.TrueState)
) ||
(node is MultiStateDiscreteState &&
(propertyName == BrowseNames.EnumStrings)) ||
(
node is ArrayItemState &&
(
propertyName == BrowseNames.InstrumentRange ||
propertyName == BrowseNames.EURange ||
propertyName == BrowseNames.EngineeringUnits ||
propertyName == BrowseNames.Title)
) ||
(
(node is YArrayItemState || node is XYArrayItemState) &&
(
propertyName == BrowseNames.InstrumentRange ||
propertyName == BrowseNames.EURange ||
propertyName == BrowseNames.EngineeringUnits ||
propertyName == BrowseNames.Title ||
propertyName == BrowseNames.XAxisDefinition)
) ||
(
node is ImageItemState &&
(
propertyName == BrowseNames.InstrumentRange ||
propertyName == BrowseNames.EURange ||
propertyName == BrowseNames.EngineeringUnits ||
propertyName == BrowseNames.Title ||
propertyName == BrowseNames.XAxisDefinition ||
propertyName == BrowseNames.YAxisDefinition)
) ||
(
node is CubeItemState &&
(
propertyName == BrowseNames.InstrumentRange ||
propertyName == BrowseNames.EURange ||
propertyName == BrowseNames.EngineeringUnits ||
propertyName == BrowseNames.Title ||
propertyName == BrowseNames.XAxisDefinition ||
propertyName == BrowseNames.YAxisDefinition ||
propertyName == BrowseNames.ZAxisDefinition)
) ||
(
node is NDimensionArrayItemState &&
(
propertyName == BrowseNames.InstrumentRange ||
propertyName == BrowseNames.EURange ||
propertyName == BrowseNames.EngineeringUnits ||
propertyName == BrowseNames.Title ||
propertyName == BrowseNames.AxisDefinition)))
{
monitoredItem.SetSemanticsChanged();
var value = new DataValue(Variant.Null, StatusCodes.Good, DateTimeUtc.MinValue, DateTime.UtcNow);
lock (node)
{
node.ReadAttribute(
systemContext,
Attributes.Value,
monitoredItem.IndexRange,
default,
ref value);
}
monitoredItem.QueueValue(value, ServiceResult.Good, true);
RaiseSemanticChangeEvent(systemContext, node, property);
}
}
}
}
/// <summary>
/// Raises a semantic change event to notify clients that the structure or meaning of a node has changed.
/// </summary>
protected void RaiseSemanticChangeEvent(ISystemContext systemContext, NodeState node, PropertyState property)
{
var e = new SemanticChangeEventState(null);
var message = new TranslationInfo(
"SemanticChangeEvent",
"en-US",
"SemanticChangeEvent.");
e.Initialize(systemContext, null, EventSeverity.Min, new LocalizedText(message));
e.SetChildValue(
systemContext,
BrowseNames.SourceNode,
ObjectIds.Server,
false);
e.SetChildValue(systemContext, BrowseNames.SourceName, "Server", false);
e.CreateOrReplaceChanges(systemContext, null!);
e!.Changes!.Value = new[]
{
new SemanticChangeStructureDataType
{
Affected = node.NodeId,
AffectedType = property.TypeDefinitionId
}
}.ToArrayOf();
Server.ReportEvent(e);
}
/// <summary>
/// Raises a GeneralModelChangeEvent to notify clients that nodes or references
/// have been added or removed in the address space.
/// </summary>
/// <remarks>
/// Per Part 5 §9.32.2 only nodes that carry a <c>NodeVersion</c>
/// property may trigger a <c>ModelChangeEvent</c>. When
/// <see cref="RequireNodeVersionForModelChange"/> is <c>true</c>
/// (the default), entries whose Affected node is owned by this
/// node manager and lacks a <c>NodeVersion</c> are dropped. The
/// affected nodes that survive the filter have their NodeVersion
/// bumped. Set <see cref="RequireNodeVersionForModelChange"/> to
/// <c>false</c> to opt back into the legacy unconditional
/// behaviour.
/// </remarks>
protected void RaiseGeneralModelChangeEvent(
ISystemContext systemContext,
ArrayOf<ModelChangeStructureDataType> changes)
{
if (changes.Count == 0)
{
return;
}
if (RequireNodeVersionForModelChange)
{
changes = FilterChangesByNodeVersion(systemContext, changes);
if (changes.Count == 0)
{
return;
}
}
var e = new GeneralModelChangeEventState(null);
var message = new TranslationInfo(
"GeneralModelChangeEvent",
"en-US",
"Address space model changed.");
e.Initialize(systemContext, null, EventSeverity.Low, new LocalizedText(message));
e.SetChildValue(
systemContext,
BrowseNames.SourceNode,
ObjectIds.Server,
false);
e.SetChildValue(systemContext, BrowseNames.SourceName, "Server", false);
e.CreateOrReplaceChanges(systemContext, null!);
e!.Changes!.Value = changes;
Server.ReportEvent(e);
}
/// <summary>
/// Raises a <c>BaseModelChangeEvent</c> for the supplied source
/// node — used when something about the address space changed
/// but the framework cannot describe the change as a list of
/// <c>ModelChangeStructureDataType</c> entries (Part 5 §9.32.5).
/// </summary>
/// <remarks>
/// The default source is <see cref="ObjectIds.Server"/> per
/// Part 5 §9.32.3, which models the whole address space as the
/// default View.
/// </remarks>
protected void RaiseBaseModelChangeEvent(
ISystemContext systemContext,
NodeId sourceNode = default)
{
NodeId effectiveSource = sourceNode.IsNull ? ObjectIds.Server : sourceNode;
var e = new BaseModelChangeEventState(null);
var message = new TranslationInfo(
"BaseModelChangeEvent",
"en-US",
"Address space model changed.");
e.Initialize(systemContext, null, EventSeverity.Low, new LocalizedText(message));
e.SetChildValue(systemContext, BrowseNames.SourceNode, effectiveSource, false);
e.SetChildValue(systemContext, BrowseNames.SourceName, "Server", false);
Server.ReportEvent(e);
}
/// <summary>
/// Returns a new <see cref="ArrayOf{T}"/> containing only those
/// <see cref="ModelChangeStructureDataType"/> entries whose
/// Affected node carries a <c>NodeVersion</c> property — when
/// the node is owned by this node manager. Affected nodes that
/// belong to a different node manager (or that cannot be
/// resolved) pass through unchanged. Bumps the NodeVersion of
/// every entry that is kept.
/// </summary>
private ArrayOf<ModelChangeStructureDataType> FilterChangesByNodeVersion(
ISystemContext systemContext,
ArrayOf<ModelChangeStructureDataType> changes)
{
var kept = new List<ModelChangeStructureDataType>(changes.Count);
for (int ii = 0; ii < changes.Count; ii++)
{
ModelChangeStructureDataType entry = changes[ii];
NodeId affected = entry.Affected;
if (affected.IsNull)
{
continue;
}
if (!PredefinedNodes.TryGetValue(affected, out NodeState? affectedNode))
{
// Not owned by this manager — pass through unchanged.
kept.Add(entry);
continue;
}
if (!affectedNode.HasNodeVersion())
{
continue;
}
affectedNode.BumpNodeVersion(systemContext);
kept.Add(entry);
}
return new ArrayOf<ModelChangeStructureDataType>(kept.ToArray());
}
/// <summary>
/// The model change aggregator used by <see cref="CreateNodeAsync"/> /
/// <see cref="DeleteNodeAsync(ServerSystemContext, NodeId, CancellationToken)"/>
/// to batch address-space changes per publish cycle.
/// </summary>
protected ModelChangeAggregator ModelChangeAggregator { get; }
= new ModelChangeAggregator();
/// <summary>
/// Gets or sets whether the framework automatically emits
/// <c>GeneralModelChangeEventType</c> from <see cref="CreateNodeAsync"/>
/// / <see cref="DeleteNodeAsync(ServerSystemContext, NodeId, CancellationToken)"/>.
/// Default: <c>true</c>.
/// </summary>
public bool ModelChangeEmissionEnabled { get; set; } = true;
/// <summary>
/// Gets or sets whether <see cref="RaiseGeneralModelChangeEvent"/>
/// and the auto-emit paths require the affected node to carry a
/// <c>NodeVersion</c> property (per Part 5 §9.32.2). When
/// <c>true</c> (the default) nodes without a NodeVersion neither
/// appear in the emitted <c>Changes[]</c> array nor cause a
/// <c>GeneralModelChangeEvent</c> to be raised on their own.
/// Set to <c>false</c> to opt back into the legacy
/// unconditional behaviour.
/// </summary>
public bool RequireNodeVersionForModelChange { get; set; } = true;
/// <summary>
/// Marks <paramref name="node"/> as eligible to trigger
/// <c>ModelChangeEvents</c> by attaching a <c>NodeVersion</c>
/// property in this manager's primary namespace if none is
/// present, and wiring an <c>OnWriteValue</c> handler that
/// posts a <c>BaseModelChangeEvent</c> whenever the property is
/// written by an external client (Part 5 §9.32.2). Idempotent.
/// </summary>
/// <param name="node">The node to enable tracking on.</param>
/// <param name="namespaceIndex">
/// Optional namespace index for the new NodeVersion property.
/// Defaults to this manager's <see cref="NamespaceIndex"/>.
/// </param>
/// <returns>The existing or newly attached NodeVersion property.</returns>
public PropertyState<string> EnableModelChangeTrackingFor(NodeState node, ushort? namespaceIndex = null)
{
return node.EnableModelChangeTracking(
namespaceIndex ?? NamespaceIndex,
(ctx, owner) => RaiseBaseModelChangeEvent(ctx, owner.NodeId));
}
/// <summary>
/// Drains the <see cref="ModelChangeAggregator"/> and emits a
/// single <c>GeneralModelChangeEvent</c>. No-op when no changes
/// are pending.
/// </summary>
protected void EmitModelChange(ISystemContext systemContext)
{
if (!ModelChangeAggregator.HasPending)
{
return;
}
ArrayOf<ModelChangeStructureDataType> changes = ModelChangeAggregator.Drain();
RaiseGeneralModelChangeEvent(systemContext, changes);
}
/// <summary>
/// Validates the nodes and writes the value to the underlying system.
/// </summary>
/// <param name="context">The context.</param>
/// <param name="nodesToWrite">The nodes to write.</param>
/// <param name="errors">The errors.</param>
/// <param name="nodesToValidate">The nodes to validate.</param>
/// <param name="cache">The cache.</param>
/// <param name="cancellationToken">The cancellation token.</param>
protected virtual async ValueTask WriteAsync(
ServerSystemContext context,
ArrayOf<WriteValue> nodesToWrite,
IList<ServiceResult> errors,
List<NodeHandle> nodesToValidate,
IDictionary<NodeId, NodeState> cache,
CancellationToken cancellationToken = default)
{
// validates the nodes (reads values from the underlying data source if required).
for (int ii = 0; ii < nodesToValidate.Count; ii++)
{
NodeHandle handle = nodesToValidate[ii];
await m_writeSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
try
{
// validate node.
NodeState source = await ValidateNodeAsync(context, handle, cache, cancellationToken).ConfigureAwait(false);
if (source == null)
{
continue;
}
WriteValue nodeToWrite = nodesToWrite[handle.Index];
// Async path — see ReadAsync for the locking rationale.
errors[handle.Index] = await source.WriteAttributeAsync(
context,
nodeToWrite.AttributeId,
nodeToWrite.ParsedIndexRange,
nodeToWrite.Value,
cancellationToken).ConfigureAwait(false);
// updates to source finished - report changes to monitored items.
source.ClearChangeMasks(context, false);
}
finally
{
m_writeSemaphore.Release();
}
}
}
/// <summary>
/// Reads the history for the specified nodes.
/// </summary>
public virtual async ValueTask HistoryReadAsync(
OperationContext context,
HistoryReadDetails details,
TimestampsToReturn timestampsToReturn,
bool releaseContinuationPoints,
ArrayOf<HistoryReadValueId> nodesToRead,
IList<HistoryReadResult> results,
IList<ServiceResult> errors,
CancellationToken cancellationToken = default)
{
ServerSystemContext systemContext = SystemContext.Copy(context);
IDictionary<NodeId, NodeState> operationCache = new NodeIdDictionary<NodeState>();
var nodesToProcess = new List<NodeHandle>();
for (int ii = 0; ii < nodesToRead.Count; ii++)
{
HistoryReadValueId nodeToRead = nodesToRead[ii];
// skip items that have already been processed.
if (nodeToRead.Processed)
{
continue;
}
// check for valid handle.
NodeHandle handle = await GetManagerHandleAsync(
systemContext,
nodeToRead.NodeId,
operationCache,
cancellationToken).ConfigureAwait(false);
if (handle == null)
{
continue;
}
// owned by this node manager.
nodeToRead.Processed = true;
// create an initial result.
HistoryReadResult result = results[ii] = new HistoryReadResult();
result.HistoryData = default;
result.ContinuationPoint = default;
result.StatusCode = StatusCodes.Good;
// check if the node is a area in memory.
if (handle.Node == null)
{
errors[ii] = StatusCodes.BadNodeIdUnknown;
// must validate node in a separate operation
handle.Index = ii;
nodesToProcess.Add(handle);
continue;
}
errors[ii] = StatusCodes.BadHistoryOperationUnsupported;
// check for data history variable.
if (handle.Node is BaseVariableState variable &&
(variable.AccessLevel & AccessLevels.HistoryRead) != 0)
{
handle.Index = ii;
nodesToProcess.Add(handle);
continue;
}
// check for event history object.
if (handle.Node is BaseObjectState notifier &&
(notifier.EventNotifier & EventNotifiers.HistoryRead) != 0)
{
handle.Index = ii;
nodesToProcess.Add(handle);
}
}
// check for nothing to do.
if (nodesToProcess.Count == 0)
{
return;
}
// validates the nodes (reads values from the underlying data source if required).
await HistoryReadAsync(
systemContext,
details,
timestampsToReturn,
releaseContinuationPoints,
nodesToRead,
results,
errors,
nodesToProcess,
operationCache,
cancellationToken).ConfigureAwait(false);
}
/// <summary>
/// Returns the historian provider for a node. The default
/// implementation returns <c>null</c>, which causes the dispatcher
/// to fall back to the server-wide
/// <see cref="IHistorianProviderRegistry"/>.
/// </summary>
/// <remarks>
/// <para>
/// Subclasses can override this to bind a specific
/// <see cref="IHistorianProvider"/> to nodes owned by this node
/// manager — for example a database-backed historian for variables
/// in this manager's namespace, while the server-wide registry
/// covers everything else.
/// </para>
/// </remarks>
protected virtual IHistorianProvider? GetHistorianProvider(NodeState node)
{
return null;
}
private IHistorianProvider? ResolveHistorianProvider(NodeState node)
{
return HistorianDispatcher.ResolveProvider(Server, node, GetHistorianProvider(node));
}
/// <summary>
/// Releases the continuation points.
/// </summary>
protected virtual async ValueTask HistoryReleaseContinuationPointsAsync(
ServerSystemContext context,
ArrayOf<HistoryReadValueId> nodesToRead,
IList<ServiceResult> errors,
List<NodeHandle> nodesToProcess,
IDictionary<NodeId, NodeState> cache,
CancellationToken cancellationToken = default)
{
for (int ii = 0; ii < nodesToProcess.Count; ii++)
{
NodeHandle handle = nodesToProcess[ii];
// validate node.
NodeState source = await ValidateNodeAsync(context, handle, cache, cancellationToken).ConfigureAwait(false);
if (source == null)
{
continue;
}
errors[handle.Index] = HistorianDispatcher.ReleaseContinuationPoint(
context, nodesToRead[handle.Index]);
}
}
/// <summary>
/// Reads raw history data.
/// </summary>
protected virtual async ValueTask HistoryReadRawModifiedAsync(
ServerSystemContext context,
ReadRawModifiedDetails details,
TimestampsToReturn timestampsToReturn,
ArrayOf<HistoryReadValueId> nodesToRead,
IList<HistoryReadResult> results,
IList<ServiceResult> errors,
List<NodeHandle> nodesToProcess,
IDictionary<NodeId, NodeState> cache,
CancellationToken cancellationToken = default)
{
for (int ii = 0; ii < nodesToProcess.Count; ii++)
{
NodeHandle handle = nodesToProcess[ii];
NodeState source = await ValidateNodeAsync(context, handle, cache, cancellationToken).ConfigureAwait(false);
if (source == null)
{
continue;
}
// Route HistoryRead on an Annotations property to the
// parent variable's annotation provider (Part 11 §5.2.7).
if (HistorianDispatcher.IsAnnotationsProperty(source))
{
BaseVariableState? parent = HistorianDispatcher.GetAnnotationsParent(source);
if (parent == null)
{
errors[handle.Index] = StatusCodes.BadHistoryOperationUnsupported;
continue;
}
IHistorianProvider? annotationProvider = ResolveHistorianProvider(parent);
if (annotationProvider == null)
{
errors[handle.Index] = StatusCodes.BadHistoryOperationUnsupported;
continue;
}
errors[handle.Index] = await HistorianDispatcher.DispatchAnnotationReadAsync(
context,
annotationProvider,
parent,
nodesToRead[handle.Index],
details,
timestampsToReturn,
results[handle.Index],
cancellationToken).ConfigureAwait(false);
continue;
}
if (source is not BaseVariableState)
{
errors[handle.Index] = StatusCodes.BadHistoryOperationUnsupported;
continue;
}
IHistorianProvider? provider = ResolveHistorianProvider(source);
if (provider == null)
{
errors[handle.Index] = StatusCodes.BadHistoryOperationUnsupported;
continue;
}
errors[handle.Index] = await HistorianDispatcher.DispatchRawReadAsync(
context,
provider,
source,
nodesToRead[handle.Index],
details,
timestampsToReturn,
results[handle.Index],
cancellationToken).ConfigureAwait(false);
}
}
/// <summary>
/// Reads processed history data.
/// </summary>
protected virtual async ValueTask HistoryReadProcessedAsync(
ServerSystemContext context,
ReadProcessedDetails details,
TimestampsToReturn timestampsToReturn,
ArrayOf<HistoryReadValueId> nodesToRead,
IList<HistoryReadResult> results,
IList<ServiceResult> errors,
List<NodeHandle> nodesToProcess,
IDictionary<NodeId, NodeState> cache,
CancellationToken cancellationToken = default)
{
for (int ii = 0; ii < nodesToProcess.Count; ii++)
{
NodeHandle handle = nodesToProcess[ii];
NodeState source = await ValidateNodeAsync(context, handle, cache, cancellationToken).ConfigureAwait(false);
if (source == null)
{
continue;
}
if (source is not BaseVariableState)
{
errors[handle.Index] = StatusCodes.BadHistoryOperationUnsupported;
continue;
}
IHistorianProvider? provider = ResolveHistorianProvider(source);
if (provider == null)
{
errors[handle.Index] = StatusCodes.BadHistoryOperationUnsupported;
continue;
}
NodeId aggregateId = !details.AggregateType.IsNull && details.AggregateType.Count > 0
? (handle.Index < details.AggregateType.Count
? details.AggregateType[handle.Index]
: details.AggregateType[0])
: NodeId.Null;
errors[handle.Index] = await HistorianDispatcher.DispatchProcessedReadAsync(
context,
provider,
source,
nodesToRead[handle.Index],
details,
aggregateId,
timestampsToReturn,
results[handle.Index],
cancellationToken).ConfigureAwait(false);
}
}
/// <summary>
/// Reads history data at specified times.
/// </summary>
protected virtual async ValueTask HistoryReadAtTimeAsync(
ServerSystemContext context,
ReadAtTimeDetails details,
TimestampsToReturn timestampsToReturn,
ArrayOf<HistoryReadValueId> nodesToRead,
IList<HistoryReadResult> results,
IList<ServiceResult> errors,
List<NodeHandle> nodesToProcess,
IDictionary<NodeId, NodeState> cache,
CancellationToken cancellationToken = default)
{
for (int ii = 0; ii < nodesToProcess.Count; ii++)
{
NodeHandle handle = nodesToProcess[ii];
NodeState source = await ValidateNodeAsync(context, handle, cache, cancellationToken).ConfigureAwait(false);
if (source == null)
{
continue;
}
if (source is not BaseVariableState)
{
errors[handle.Index] = StatusCodes.BadHistoryOperationUnsupported;
continue;
}
IHistorianProvider? provider = ResolveHistorianProvider(source);
if (provider == null)
{
errors[handle.Index] = StatusCodes.BadHistoryOperationUnsupported;
continue;
}
errors[handle.Index] = await HistorianDispatcher.DispatchAtTimeReadAsync(
context,
provider,
source,
nodesToRead[handle.Index],
details,
timestampsToReturn,
results[handle.Index],
cancellationToken).ConfigureAwait(false);
}
}
/// <summary>
/// Reads history events.
/// </summary>
protected virtual async ValueTask HistoryReadEventsAsync(
ServerSystemContext context,
ReadEventDetails details,
TimestampsToReturn timestampsToReturn,
ArrayOf<HistoryReadValueId> nodesToRead,
IList<HistoryReadResult> results,
IList<ServiceResult> errors,
List<NodeHandle> nodesToProcess,
IDictionary<NodeId, NodeState> cache,
CancellationToken cancellationToken = default)
{
for (int ii = 0; ii < nodesToProcess.Count; ii++)
{
NodeHandle handle = nodesToProcess[ii];
NodeState source = await ValidateNodeAsync(context, handle, cache, cancellationToken).ConfigureAwait(false);
if (source == null)
{
continue;
}
IHistorianProvider? provider = ResolveHistorianProvider(source);
if (provider == null)
{
errors[handle.Index] = StatusCodes.BadHistoryOperationUnsupported;
continue;
}
errors[handle.Index] = await HistorianDispatcher.DispatchEventReadAsync(
context, provider, source, nodesToRead[handle.Index],
details, timestampsToReturn, results[handle.Index],
cancellationToken).ConfigureAwait(false);
}
}
/// <summary>
/// Validates the nodes and reads the values from the underlying source.
/// </summary>
/// <exception cref="ServiceResultException"></exception>
protected virtual async ValueTask HistoryReadAsync(
ServerSystemContext context,
HistoryReadDetails details,
TimestampsToReturn timestampsToReturn,
bool releaseContinuationPoints,
ArrayOf<HistoryReadValueId> nodesToRead,
IList<HistoryReadResult> results,
IList<ServiceResult> errors,
List<NodeHandle> nodesToProcess,
IDictionary<NodeId, NodeState> cache,
CancellationToken cancellationToken = default)
{
// check if continuation points are being released.
if (releaseContinuationPoints)
{
await HistoryReleaseContinuationPointsAsync(
context,
nodesToRead,
errors,
nodesToProcess,
cache,
cancellationToken).ConfigureAwait(false);
return;
}
// check timestamps to return.
if (timestampsToReturn is < TimestampsToReturn.Source or > TimestampsToReturn.Neither)
{
throw new ServiceResultException(StatusCodes.BadTimestampsToReturnInvalid);
}
// If any node-to-read carries a continuation point the dispatcher
// will restore the saved request (including its original
// timestamps and NumValuesPerNode). Continuation-point reads
// therefore commonly arrive with MinValue/MinValue timestamps
// and NumValuesPerNode = 0 — skip the timestamp validation in
// that case so subsequent pages aren't rejected.
bool hasContinuationPoint = false;
for (int idx = 0; idx < nodesToRead.Count; idx++)
{
if (!nodesToRead[idx].ContinuationPoint.IsEmpty)
{
hasContinuationPoint = true;
break;
}
}
// handle raw data request.
if (details is ReadRawModifiedDetails readRawModifiedDetails)
{
if (!hasContinuationPoint)
{
// at least one must be provided.
if (readRawModifiedDetails.StartTime == DateTimeUtc.MinValue &&
readRawModifiedDetails.EndTime == DateTimeUtc.MinValue)
{
throw new ServiceResultException(StatusCodes.BadInvalidTimestampArgument);
}
// if one is null the num values must be provided.
if (readRawModifiedDetails.StartTime == DateTimeUtc.MinValue ||
readRawModifiedDetails.EndTime == DateTimeUtc.MinValue)
{
if (readRawModifiedDetails.NumValuesPerNode == 0)
{
throw new ServiceResultException(StatusCodes.BadInvalidTimestampArgument);
}
}
}
await HistoryReadRawModifiedAsync(
context,
readRawModifiedDetails,
timestampsToReturn,
nodesToRead,
results,
errors,
nodesToProcess,
cache,
cancellationToken).ConfigureAwait(false);
return;
}
// handle processed data request.
if (details is ReadProcessedDetails readProcessedDetails)
{
// check the list of aggregates.
if (readProcessedDetails.AggregateType.Count != nodesToRead.Count)
{
throw new ServiceResultException(StatusCodes.BadAggregateListMismatch);
}
if (!hasContinuationPoint)
{
// check start/end time.
if (readProcessedDetails.StartTime == DateTimeUtc.MinValue ||
readProcessedDetails.EndTime == DateTimeUtc.MinValue)
{
throw new ServiceResultException(StatusCodes.BadInvalidTimestampArgument);
}
}
await HistoryReadProcessedAsync(
context,
readProcessedDetails,
timestampsToReturn,
nodesToRead,
results,
errors,
nodesToProcess,
cache,
cancellationToken).ConfigureAwait(false);
return;
}
// handle raw data at time request.
if (details is ReadAtTimeDetails readAtTimeDetails)
{
await HistoryReadAtTimeAsync(
context,
readAtTimeDetails,
timestampsToReturn,
nodesToRead,
results,
errors,
nodesToProcess,
cache,
cancellationToken).ConfigureAwait(false);
return;
}
// handle read events request.
if (details is ReadEventDetails readEventDetails)
{
if (!hasContinuationPoint)
{
// check start/end time and max values.
if (readEventDetails.NumValuesPerNode == 0)
{
if (readEventDetails.StartTime == DateTimeUtc.MinValue ||
readEventDetails.EndTime == DateTimeUtc.MinValue)
{
throw new ServiceResultException(StatusCodes.BadInvalidTimestampArgument);
}
}
else if (readEventDetails.StartTime == DateTimeUtc.MinValue &&
readEventDetails.EndTime == DateTimeUtc.MinValue)
{
throw new ServiceResultException(StatusCodes.BadInvalidTimestampArgument);
}
// validate the event filter.
EventFilter.Result result = readEventDetails.Filter.Validate(
new FilterContext(Server.NamespaceUris, Server.TypeTree, context, Server.Telemetry));
if (ServiceResult.IsBad(result.Status))
{
throw new ServiceResultException(result.Status);
}
}
// read the event history.
await HistoryReadEventsAsync(
context,
readEventDetails,
timestampsToReturn,
nodesToRead,
results,
errors,
nodesToProcess,
cache,
cancellationToken).ConfigureAwait(false);
}
}
/// <summary>
/// Updates the history for the specified nodes.
/// </summary>
public virtual async ValueTask HistoryUpdateAsync(
OperationContext context,
Type detailsType,
ArrayOf<HistoryUpdateDetails> nodesToUpdate,
IList<HistoryUpdateResult> results,
IList<ServiceResult> errors,
CancellationToken cancellationToken = default)
{
ServerSystemContext systemContext = SystemContext.Copy(context);
IDictionary<NodeId, NodeState> operationCache = new NodeIdDictionary<NodeState>();
var nodesToProcess = new List<NodeHandle>();
await m_writeSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
try
{
for (int ii = 0; ii < nodesToUpdate.Count; ii++)
{
HistoryUpdateDetails nodeToUpdate = nodesToUpdate[ii];
// skip items that have already been processed.
if (nodeToUpdate.Processed)
{
continue;
}
// check for valid handle.
NodeHandle handle = await GetManagerHandleAsync(
systemContext,
nodeToUpdate.NodeId,
operationCache,
cancellationToken).ConfigureAwait(false);
if (handle == null)
{
continue;
}
// owned by this node manager.
nodeToUpdate.Processed = true;
// create an initial result.
HistoryUpdateResult result = results[ii] = new HistoryUpdateResult();
result.StatusCode = StatusCodes.Good;
// check if the node is a area in memory.
if (handle.Node == null)
{
errors[ii] = StatusCodes.BadNodeIdUnknown;
// must validate node in a separate operation
handle.Index = ii;
nodesToProcess.Add(handle);
continue;
}
errors[ii] = StatusCodes.BadHistoryOperationUnsupported;
// check for data history variable.
if (handle.Node is BaseVariableState variable &&
(variable.AccessLevel & AccessLevels.HistoryWrite) != 0)
{
handle.Index = ii;
nodesToProcess.Add(handle);
continue;
}
// check for event history object.
if (handle.Node is BaseObjectState notifier &&
(notifier.EventNotifier & EventNotifiers.HistoryWrite) != 0)
{
handle.Index = ii;
nodesToProcess.Add(handle);
}
}
// check for nothing to do.
if (nodesToProcess.Count == 0)
{
return;
}
}
finally
{
m_writeSemaphore.Release();
}
// validates the nodes and updates.
await HistoryUpdateAsync(
systemContext,
detailsType,
nodesToUpdate,
results,
errors,
nodesToProcess,
operationCache,
cancellationToken).ConfigureAwait(false);
}
/// <summary>
/// Validates the nodes and updates the history.
/// </summary>
protected virtual async ValueTask HistoryUpdateAsync(
ServerSystemContext context,
Type detailsType,
ArrayOf<HistoryUpdateDetails> nodesToUpdate,
IList<HistoryUpdateResult> results,
IList<ServiceResult> errors,
List<NodeHandle> nodesToProcess,
IDictionary<NodeId, NodeState> cache,
CancellationToken cancellationToken)
{
// handle update data request.
if (detailsType == typeof(UpdateDataDetails))
{
var details = new UpdateDataDetails[nodesToUpdate.Count];
for (int ii = 0; ii < details.Length; ii++)
{
details[ii] = (UpdateDataDetails)nodesToUpdate[ii];
}
await HistoryUpdateDataAsync(context,
details,
results,
errors,
nodesToProcess,
cache,
cancellationToken).ConfigureAwait(false);
return;
}
// handle update structure data request.
if (detailsType == typeof(UpdateStructureDataDetails))
{
var details = new UpdateStructureDataDetails[nodesToUpdate.Count];
for (int ii = 0; ii < details.Length; ii++)
{
details[ii] = (UpdateStructureDataDetails)nodesToUpdate[ii];
}
await HistoryUpdateStructureDataAsync(
context,
details,
results,
errors,
nodesToProcess,
cache,
cancellationToken).ConfigureAwait(false);
return;
}
// handle update events request.
if (detailsType == typeof(UpdateEventDetails))
{
var details = new UpdateEventDetails[nodesToUpdate.Count];
for (int ii = 0; ii < details.Length; ii++)
{
details[ii] = (UpdateEventDetails)nodesToUpdate[ii];
}
await HistoryUpdateEventsAsync(context,
details,
results,
errors,
nodesToProcess,
cache,
cancellationToken).ConfigureAwait(false);
return;
}
// handle delete raw data request.
if (detailsType == typeof(DeleteRawModifiedDetails))
{
var details = new DeleteRawModifiedDetails[nodesToUpdate.Count];
for (int ii = 0; ii < details.Length; ii++)
{
details[ii] = (DeleteRawModifiedDetails)nodesToUpdate[ii];
}
await HistoryDeleteRawModifiedAsync(context,
details,
results,
errors,
nodesToProcess,
cache,
cancellationToken).ConfigureAwait(false);
return;
}
// handle delete at time request.
if (detailsType == typeof(DeleteAtTimeDetails))
{
var details = new DeleteAtTimeDetails[nodesToUpdate.Count];
for (int ii = 0; ii < details.Length; ii++)
{
details[ii] = (DeleteAtTimeDetails)nodesToUpdate[ii];
}
await HistoryDeleteAtTimeAsync(context,
details,
results,
errors,
nodesToProcess,
cache,
cancellationToken).ConfigureAwait(false);
return;
}
// handle delete at time request.
if (detailsType == typeof(DeleteEventDetails))
{
var details = new DeleteEventDetails[nodesToUpdate.Count];
for (int ii = 0; ii < details.Length; ii++)
{
details[ii] = (DeleteEventDetails)nodesToUpdate[ii];
}
await HistoryDeleteEventsAsync(context,
details,
results,
errors,
nodesToProcess,
cache,
cancellationToken).ConfigureAwait(false);
}
}
/// <summary>
/// Updates the data history for one or more nodes.
/// </summary>
protected virtual async ValueTask HistoryUpdateDataAsync(
ServerSystemContext context,
ArrayOf<UpdateDataDetails> nodesToUpdate,
IList<HistoryUpdateResult> results,
IList<ServiceResult> errors,
List<NodeHandle> nodesToProcess,
IDictionary<NodeId, NodeState> cache,
CancellationToken cancellationToken)
{
for (int ii = 0; ii < nodesToProcess.Count; ii++)
{
NodeHandle handle = nodesToProcess[ii];
NodeState source = await ValidateNodeAsync(context, handle, cache, cancellationToken).ConfigureAwait(false);
if (source == null)
{
continue;
}
if (source is not BaseVariableState)
{
errors[handle.Index] = StatusCodes.BadHistoryOperationUnsupported;
continue;
}
IHistorianProvider? provider = ResolveHistorianProvider(source);
if (provider == null)
{
errors[handle.Index] = StatusCodes.BadHistoryOperationUnsupported;
continue;
}
errors[handle.Index] = await HistorianDispatcher.DispatchUpdateDataAsync(
context,
provider,
source,
nodesToUpdate[handle.Index],
results[handle.Index],
cancellationToken).ConfigureAwait(false);
}
}
/// <summary>
/// Updates the structured data history (Part 11 §5.2.7
/// Annotations) for one or more nodes.
/// </summary>
protected virtual async ValueTask HistoryUpdateStructureDataAsync(
ServerSystemContext context,
ArrayOf<UpdateStructureDataDetails> nodesToUpdate,
IList<HistoryUpdateResult> results,
IList<ServiceResult> errors,
List<NodeHandle> nodesToProcess,
IDictionary<NodeId, NodeState> cache,
CancellationToken cancellationToken)
{
for (int ii = 0; ii < nodesToProcess.Count; ii++)
{
NodeHandle handle = nodesToProcess[ii];
NodeState source = await ValidateNodeAsync(context, handle, cache, cancellationToken).ConfigureAwait(false);
if (source == null)
{
continue;
}
if (!HistorianDispatcher.IsAnnotationsProperty(source))
{
errors[handle.Index] = StatusCodes.BadHistoryOperationUnsupported;
continue;
}
BaseVariableState? parent = HistorianDispatcher.GetAnnotationsParent(source);
if (parent == null)
{
errors[handle.Index] = StatusCodes.BadHistoryOperationUnsupported;
continue;
}
IHistorianProvider? provider = ResolveHistorianProvider(parent);
if (provider == null)
{
errors[handle.Index] = StatusCodes.BadHistoryOperationUnsupported;
continue;
}
errors[handle.Index] = await HistorianDispatcher.DispatchAnnotationUpdateAsync(
context,
provider,
parent,
nodesToUpdate[handle.Index],
results[handle.Index],
cancellationToken).ConfigureAwait(false);
}
}
/// <summary>
/// Updates the event history for one or more nodes.
/// </summary>
protected virtual async ValueTask HistoryUpdateEventsAsync(
ServerSystemContext context,
ArrayOf<UpdateEventDetails> nodesToUpdate,
IList<HistoryUpdateResult> results,
IList<ServiceResult> errors,
List<NodeHandle> nodesToProcess,
IDictionary<NodeId, NodeState> cache,
CancellationToken cancellationToken = default)
{
for (int ii = 0; ii < nodesToProcess.Count; ii++)
{
NodeHandle handle = nodesToProcess[ii];
NodeState source = await ValidateNodeAsync(context, handle, cache, cancellationToken).ConfigureAwait(false);
if (source == null)
{
continue;
}
IHistorianProvider? provider = ResolveHistorianProvider(source);
if (provider == null)
{
errors[handle.Index] = StatusCodes.BadHistoryOperationUnsupported;
continue;
}
errors[handle.Index] = await HistorianDispatcher.DispatchUpdateEventAsync(
context, provider, source, nodesToUpdate[handle.Index],
results[handle.Index], cancellationToken).ConfigureAwait(false);
}
}
/// <summary>
/// Deletes the data history for one or more nodes.
/// </summary>
protected virtual async ValueTask HistoryDeleteRawModifiedAsync(
ServerSystemContext context,
ArrayOf<DeleteRawModifiedDetails> nodesToUpdate,
IList<HistoryUpdateResult> results,
IList<ServiceResult> errors,
List<NodeHandle> nodesToProcess,
IDictionary<NodeId, NodeState> cache,
CancellationToken cancellationToken = default)
{
for (int ii = 0; ii < nodesToProcess.Count; ii++)
{
NodeHandle handle = nodesToProcess[ii];
NodeState source = await ValidateNodeAsync(context, handle, cache, cancellationToken).ConfigureAwait(false);
if (source == null)
{
continue;
}
if (source is not BaseVariableState)
{
errors[handle.Index] = StatusCodes.BadHistoryOperationUnsupported;
continue;
}
IHistorianProvider? provider = ResolveHistorianProvider(source);
if (provider == null)
{
errors[handle.Index] = StatusCodes.BadHistoryOperationUnsupported;
continue;
}
errors[handle.Index] = await HistorianDispatcher.DispatchDeleteRawAsync(
context,
provider,
source,
nodesToUpdate[handle.Index],
results[handle.Index],
cancellationToken).ConfigureAwait(false);
}
}
/// <summary>
/// Deletes the data history at specified timestamps for one or more nodes.
/// </summary>
protected virtual async ValueTask HistoryDeleteAtTimeAsync(
ServerSystemContext context,
ArrayOf<DeleteAtTimeDetails> nodesToUpdate,
IList<HistoryUpdateResult> results,
IList<ServiceResult> errors,
List<NodeHandle> nodesToProcess,
IDictionary<NodeId, NodeState> cache,
CancellationToken cancellationToken = default)
{
for (int ii = 0; ii < nodesToProcess.Count; ii++)
{
NodeHandle handle = nodesToProcess[ii];
NodeState source = await ValidateNodeAsync(context, handle, cache, cancellationToken).ConfigureAwait(false);
if (source == null)
{
continue;
}
if (source is not BaseVariableState)
{
errors[handle.Index] = StatusCodes.BadHistoryOperationUnsupported;
continue;
}
IHistorianProvider? provider = ResolveHistorianProvider(source);
if (provider == null)
{
errors[handle.Index] = StatusCodes.BadHistoryOperationUnsupported;
continue;
}
errors[handle.Index] = await HistorianDispatcher.DispatchDeleteAtTimeAsync(
context,
provider,
source,
nodesToUpdate[handle.Index],
results[handle.Index],
cancellationToken).ConfigureAwait(false);
}
}
/// <summary>
/// Deletes the event history for one or more nodes.
/// </summary>
protected virtual async ValueTask HistoryDeleteEventsAsync(
ServerSystemContext context,
ArrayOf<DeleteEventDetails> nodesToUpdate,
IList<HistoryUpdateResult> results,
IList<ServiceResult> errors,
List<NodeHandle> nodesToProcess,
IDictionary<NodeId, NodeState> cache,
CancellationToken cancellationToken = default)
{
for (int ii = 0; ii < nodesToProcess.Count; ii++)
{
NodeHandle handle = nodesToProcess[ii];
NodeState source = await ValidateNodeAsync(context, handle, cache, cancellationToken).ConfigureAwait(false);
if (source == null)
{
continue;
}
IHistorianProvider? provider = ResolveHistorianProvider(source);
if (provider == null)
{
errors[handle.Index] = StatusCodes.BadHistoryOperationUnsupported;
continue;
}
errors[handle.Index] = await HistorianDispatcher.DispatchDeleteEventsAsync(
context, provider, source, nodesToUpdate[handle.Index],
results[handle.Index], cancellationToken).ConfigureAwait(false);
}
}
/// <summary>
/// Resolves the effective method for a call request.
/// </summary>
public virtual async ValueTask<MethodState> FindMethodStateAsync(
OperationContext context,
CallMethodRequest methodToCall,
CancellationToken cancellationToken = default)
{
if (methodToCall == null || methodToCall.ObjectId.IsNull || methodToCall.MethodId.IsNull)
{
return null!;
}
ServerSystemContext systemContext = SystemContext.Copy(context);
IDictionary<NodeId, NodeState> operationCache = new NodeIdDictionary<NodeState>();
NodeHandle? handle = await GetManagerHandleAsync(
systemContext,
methodToCall.ObjectId,
operationCache,
cancellationToken).ConfigureAwait(false);
if (handle == null)
{
return null!;
}
NodeState? source = await ValidateNodeAsync(systemContext, handle, operationCache, cancellationToken).ConfigureAwait(false);
if (source == null)
{
return null!;
}
MethodState? method;
lock (source)
{
method = source.FindMethod(systemContext, methodToCall.MethodId);
}
if (method != null)
{
return method;
}
bool referenceExists;
lock (source)
{
referenceExists = source.ReferenceExists(
ReferenceTypeIds.HasComponent,
false,
methodToCall.MethodId);
}
if (referenceExists)
{
method = FindPredefinedNode<MethodState>(methodToCall.MethodId);
}
if (method == null && source is BaseInstanceState instanceState)
{
method = FindMethodInTypeHierarchy(systemContext, instanceState.TypeDefinitionId, methodToCall.MethodId);
}
return method!;
}
/// <summary>
/// Asynchronously calls a method defined on an object.
/// </summary>
public virtual async ValueTask CallAsync(
OperationContext context,
ArrayOf<CallMethodRequest> methodsToCall,
IList<CallMethodResult> results,
IList<ServiceResult> errors,
CancellationToken cancellationToken = default)
{
ServerSystemContext systemContext = SystemContext.Copy(context);
IDictionary<NodeId, NodeState> operationCache = new NodeIdDictionary<NodeState>();
for (int ii = 0; ii < methodsToCall.Count; ii++)
{
CallMethodRequest methodToCall = methodsToCall[ii];
// skip items that have already been processed.
if (methodToCall.Processed)
{
continue;
}
// check for valid handle.
NodeHandle handle = await GetManagerHandleAsync(
systemContext,
methodToCall.ObjectId,
operationCache,
cancellationToken).ConfigureAwait(false);
if (handle == null)
{
continue;
}
// owned by this node manager.
methodToCall.Processed = true;
// validate the source node.
if (await ValidateNodeAsync(systemContext, handle, operationCache, cancellationToken).ConfigureAwait(false) == null)
{
errors[ii] = StatusCodes.BadNodeIdUnknown;
continue;
}
MethodState? method = await FindMethodStateAsync(
context,
methodToCall,
cancellationToken).ConfigureAwait(false);
if (method == null)
{
errors[ii] = StatusCodes.BadMethodInvalid;
continue;
}
// validate the role permissions for method to be executed,
// it may be a different MethodState that does not have the MethodId specified in the method call
errors[ii] = await ValidateRolePermissionsAsync(
context,
method.NodeId,
PermissionType.Call,
cancellationToken).ConfigureAwait(false);
if (ServiceResult.IsBad(errors[ii]))
{
continue;
}
// call the method.
CallMethodResult result = results[ii] = new CallMethodResult();
errors[ii] = await CallAsync(
systemContext,
methodToCall,
method,
result,
cancellationToken).ConfigureAwait(false);
}
}
/// <summary>
/// Asynchronously calls a method on an object.
/// </summary>
protected virtual async ValueTask<ServiceResult> CallAsync(
ISystemContext context,
CallMethodRequest methodToCall,
MethodState method,
CallMethodResult result,
CancellationToken cancellationToken = default)
{
var systemContext = context as ServerSystemContext;
var argumentErrors = new List<ServiceResult>();
var outputArguments = new List<Variant>();
ServiceResult callResult = await method.CallAsync(
context,
methodToCall.ObjectId,
methodToCall.InputArguments,
argumentErrors,
outputArguments,
cancellationToken
).ConfigureAwait(false);
if (ServiceResult.IsBad(callResult))
{
return callResult;
}
// check for argument errors.
bool argumentsValid = true;
var inputArgumentResults = new List<StatusCode>();
var inputArgumentDiagnosticInfos = new List<DiagnosticInfo>();
for (int jj = 0; jj < argumentErrors.Count; jj++)
{
ServiceResult argumentError = argumentErrors[jj];
if (argumentError != null)
{
inputArgumentResults.Add(argumentError.StatusCode);
if (ServiceResult.IsBad(argumentError))
{
argumentsValid = false;
}
// only fill in diagnostic info if it is requested.
if (systemContext!.OperationContext != null &&
(systemContext.OperationContext.DiagnosticsMask &
DiagnosticsMasks.OperationAll) != 0)
{
if (ServiceResult.IsBad(argumentError))
{
inputArgumentDiagnosticInfos.Add(
new DiagnosticInfo(
argumentError,
systemContext.OperationContext.DiagnosticsMask,
false,
systemContext.OperationContext.StringTable,
m_logger));
}
else
{
inputArgumentDiagnosticInfos.Add(null!);
}
}
}
}
// check for validation errors.
if (!argumentsValid)
{
// Per OPC UA Part 4, Section 5.12: InputArgumentResults must be empty
// when StatusCode is Good. Therefore set here to the argument results
// and return a Bad status code.
result.InputArgumentResults = inputArgumentResults;
result.InputArgumentDiagnosticInfos = inputArgumentDiagnosticInfos;
result.StatusCode = StatusCodes.BadInvalidArgument;
return result.StatusCode;
}
// return output arguments.
result.OutputArguments = outputArguments;
// return the actual result of the original call
return callResult;
}
/// <summary>
/// Subscribes or unsubscribes to events produced by the specified source.
/// </summary>
/// <remarks>
/// This method is called when a event subscription is created or deletes. The node manager
/// must start/stop reporting events for the specified object and all objects below it in
/// the notifier hierarchy.
/// </remarks>
public virtual async ValueTask<ServiceResult> SubscribeToEventsAsync(
OperationContext context,
object sourceId,
uint subscriptionId,
IEventMonitoredItem monitoredItem,
bool unsubscribe,
CancellationToken cancellationToken = default)
{
ServerSystemContext systemContext = SystemContext.Copy(context);
// check for valid handle.
NodeHandle? handle = IsHandleInNamespace(sourceId);
if (handle == null)
{
return StatusCodes.BadNodeIdInvalid;
}
// check for valid node.
NodeState source = await ValidateNodeAsync(systemContext, handle, null!, cancellationToken).ConfigureAwait(false);
if (source == null)
{
return StatusCodes.BadNodeIdUnknown;
}
// subscribe to events.
return await SubscribeToEventsAsync(systemContext, source, monitoredItem, unsubscribe, cancellationToken).ConfigureAwait(false);
}
/// <summary>
/// Subscribes or unsubscribes to events produced by all event sources.
/// </summary>
/// <remarks>
/// This method is called when a event subscription is created or deleted. The node
/// manager must start/stop reporting events for all objects that it manages.
/// </remarks>
public virtual async ValueTask<ServiceResult> SubscribeToAllEventsAsync(
OperationContext context,
uint subscriptionId,
IEventMonitoredItem monitoredItem,
bool unsubscribe,
CancellationToken cancellationToken = default)
{
ServerSystemContext systemContext = SystemContext.Copy(context);
// A client has subscribed to the Server object which means all events produced
// by this manager must be reported. This is done by incrementing the monitoring
// reference count for all root notifiers.
foreach (KeyValuePair<NodeId, NodeState> kvp in RootNotifiers)
{
await SubscribeToEventsAsync(
systemContext,
kvp.Value,
monitoredItem,
unsubscribe,
cancellationToken).ConfigureAwait(false);
}
return ServiceResult.Good;
}
/// <summary>
/// Adds a root notifier.
/// </summary>
/// <param name="notifier">The notifier.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <remarks>
/// A root notifier is a notifier owned by the NodeManager that is not the target of a
/// HasNotifier reference. These nodes need to be linked directly to the Server object.
/// </remarks>
protected virtual async ValueTask AddRootNotifierAsync(NodeState notifier, CancellationToken cancellationToken = default)
{
RootNotifiers.AddOrUpdate(notifier.NodeId, notifier, (key, _) => notifier);
// need to prevent recursion with the server object.
if (notifier.NodeId != ObjectIds.Server)
{
lock (notifier)
{
notifier.OnReportEvent = OnReportEvent;
if (!notifier.ReferenceExists(
ReferenceTypeIds.HasNotifier,
true,
ObjectIds.Server))
{
notifier.AddReference(ReferenceTypeIds.HasNotifier, true, ObjectIds.Server);
}
}
}
// subscribe to existing events.
if (Server.EventManager != null)
{
IList<IEventMonitoredItem> monitoredItems = Server.EventManager
.GetMonitoredItems();
for (int ii = 0; ii < monitoredItems.Count; ii++)
{
if (monitoredItems[ii].MonitoringAllEvents)
{
await SubscribeToEventsAsync(SystemContext, notifier, monitoredItems[ii], true, cancellationToken).ConfigureAwait(false);
}
}
}
}
/// <summary>
/// Removes a root notifier previously added with AddRootNotifier.
/// </summary>
/// <param name="notifier">The notifier.</param>
/// <param name="cancellationToken">The cancellation token.</param>
protected virtual ValueTask RemoveRootNotifierAsync(
NodeState notifier,
CancellationToken cancellationToken = default)
{
if (RootNotifiers.TryRemove(notifier.NodeId, out notifier!))
{
lock (notifier)
{
notifier.OnReportEvent = null;
if (notifier.ReferenceExists(
ReferenceTypeIds.HasNotifier,
true,
ObjectIds.Server))
{
notifier.RemoveReference(
ReferenceTypeIds.HasNotifier,
true,
ObjectIds.Server);
}
}
}
return default;
}
/// <summary>
/// Reports an event for a root notifier.
/// </summary>
protected virtual void OnReportEvent(
ISystemContext context,
NodeState node,
IFilterTarget e)
{
Server.ReportEvent(context, e);
}
/// <summary>
/// Subscribes to events.
/// </summary>
/// <param name="context">The context.</param>
/// <param name="source">The source.</param>
/// <param name="monitoredItem">The monitored item.</param>
/// <param name="unsubscribe">if set to <c>true</c> [unsubscribe].</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Any error code.</returns>
protected virtual async ValueTask<ServiceResult> SubscribeToEventsAsync(
ServerSystemContext context,
NodeState source,
IEventMonitoredItem monitoredItem,
bool unsubscribe,
CancellationToken cancellationToken = default)
{
await m_monitoredItemSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
try
{
(MonitoredNode2? monitoredNode, ServiceResult serviceResult) = m_monitoredItemManager!
.SubscribeToEvents(
context,
source,
monitoredItem,
unsubscribe);
// This call recursively updates a reference count all nodes in the notifier
// hierarchy below the area. Sources with a reference count of 0 do not have
// any active subscriptions so they do not need to report events.
lock (source)
{
source.SetAreEventsMonitored(context, !unsubscribe, true);
}
// signal update.
await OnSubscribeToEventsAsync(context, monitoredNode!, unsubscribe, cancellationToken).ConfigureAwait(false);
// all done.
return serviceResult;
}
finally
{
m_monitoredItemSemaphore.Release();
}
}
/// <summary>
/// Called after subscribing/unsubscribing to events.
/// </summary>
/// <param name="context">The context.</param>
/// <param name="monitoredNode">The monitored node.</param>
/// <param name="unsubscribe">if set to <c>true</c> unsubscribing.</param>
/// <param name="cancellationToken">The cancellation token.</param>
protected virtual ValueTask OnSubscribeToEventsAsync(
ServerSystemContext context,
MonitoredNode2 monitoredNode,
bool unsubscribe,
CancellationToken cancellationToken = default)
{
// defined by the sub-class
return new ValueTask();
}
/// <summary>
/// Tells the node manager to refresh any conditions associated with the specified monitored items.
/// </summary>
/// <remarks>
/// This method is called when the condition refresh method is called for a subscription.
/// The node manager must create a refresh event for each condition monitored by the subscription.
/// </remarks>
public virtual async ValueTask<ServiceResult> ConditionRefreshAsync(
OperationContext context,
IList<IEventMonitoredItem> monitoredItems,
CancellationToken cancellationToken = default)
{
ServerSystemContext systemContext = SystemContext.Copy(context);
for (int ii = 0; ii < monitoredItems.Count; ii++)
{
// the IEventMonitoredItem should always be MonitoredItems since they are created by the MasterNodeManager.
IEventMonitoredItem monitoredItem = monitoredItems[ii];
if (monitoredItem == null)
{
continue;
}
var events = new List<IFilterTarget>();
var nodesToRefresh = new List<NodeState>();
await m_monitoredItemSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
try
{
// check for server subscription.
if (monitoredItem.NodeId == ObjectIds.Server)
{
if (!RootNotifiers.IsEmpty)
{
nodesToRefresh.AddRange(RootNotifiers.Values);
}
}
else
{
// check if monitored Item is managed by this node manager
if (!MonitoredItems.ContainsKey(monitoredItem.Id))
{
continue;
}
// get the refresh events.
nodesToRefresh.Add(((NodeHandle)monitoredItem.ManagerHandle).Node);
}
}
finally
{
m_monitoredItemSemaphore.Release();
}
// block and wait for the refresh.
for (int jj = 0; jj < nodesToRefresh.Count; jj++)
{
nodesToRefresh[jj].ConditionRefresh(systemContext, events, true);
}
// queue the events.
for (int jj = 0; jj < events.Count; jj++)
{
// verify if the event can be received by the current monitored item
ServiceResult result = await ValidateEventRolePermissionsAsync(monitoredItem, events[jj], cancellationToken).ConfigureAwait(false);
if (ServiceResult.IsBad(result))
{
continue;
}
monitoredItem.QueueEvent(events[jj]);
}
}
// all done.
return ServiceResult.Good;
}
/// <summary>
/// Restore a set of monitored items after a restart.
/// </summary>
/// <exception cref="ArgumentNullException"><paramref name="itemsToRestore"/> is <c>null</c>.</exception>
/// <exception cref="InvalidOperationException"></exception>
public virtual async ValueTask RestoreMonitoredItemsAsync(
IList<IStoredMonitoredItem> itemsToRestore,
IList<IMonitoredItem> monitoredItems,
IUserIdentity savedOwnerIdentity,
CancellationToken cancellationToken = default)
{
if (itemsToRestore == null)
{
throw new ArgumentNullException(nameof(itemsToRestore));
}
if (monitoredItems == null)
{
throw new ArgumentNullException(nameof(monitoredItems));
}
if (Server.IsRunning)
{
throw new InvalidOperationException(
"Subscription restore can only occur on startup");
}
ServerSystemContext systemContext = SystemContext.Copy();
IDictionary<NodeId, NodeState> operationCache = new NodeIdDictionary<NodeState>();
var nodesToValidate = new List<NodeHandle>();
for (int ii = 0; ii < itemsToRestore.Count; ii++)
{
IStoredMonitoredItem itemToCreate = itemsToRestore[ii];
// skip items that have already been processed.
if (itemToCreate.IsRestored)
{
continue;
}
// check for valid handle.
NodeHandle handle = await GetManagerHandleAsync(
systemContext,
itemToCreate.NodeId,
operationCache,
cancellationToken).ConfigureAwait(false);
if (handle == null)
{
continue;
}
// owned by this node manager.
itemToCreate.IsRestored = true;
handle.Index = ii;
nodesToValidate.Add(handle);
}
// check for nothing to do.
if (nodesToValidate.Count == 0)
{
return;
}
await m_monitoredItemSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
try
{
// validates the nodes (reads values from the underlying data source if required).
for (int ii = 0; ii < nodesToValidate.Count; ii++)
{
NodeHandle handle = nodesToValidate[ii];
bool success;
IMonitoredItem? monitoredItem = null;
try
{
// validate node.
NodeState source = await ValidateNodeAsync(systemContext, handle, operationCache, cancellationToken).ConfigureAwait(false);
if (source == null)
{
continue;
}
IStoredMonitoredItem itemToCreate = itemsToRestore[handle.Index];
// create monitored item.
success = RestoreMonitoredItem(
systemContext,
handle,
itemToCreate,
savedOwnerIdentity,
out monitoredItem);
if (!success)
{
continue;
}
// save the monitored item.
monitoredItems[handle.Index] = monitoredItem!;
monitoredItem = null; // ownership transferred
}
finally
{
(monitoredItem as IDisposable)?.Dispose();
}
}
m_monitoredItemManager.ApplyChanges();
}
finally
{
m_monitoredItemSemaphore.Release();
}
// do any post processing.
OnCreateMonitoredItemsComplete(systemContext, monitoredItems);
}
/// <summary>
/// Restore a single monitored Item after a restart
/// </summary>
/// <returns>true if sucesfully restored</returns>
protected virtual bool RestoreMonitoredItem(
ServerSystemContext context,
NodeHandle handle,
IStoredMonitoredItem storedMonitoredItem,
IUserIdentity savedOwnerIdentity,
out IMonitoredItem? monitoredItem)
{
monitoredItem = null;
// validate attribute.
if (!Attributes.IsValid(handle.Node.NodeClass, storedMonitoredItem.AttributeId))
{
return false;
}
// put an upper limit on queue size.
storedMonitoredItem.QueueSize = SubscriptionManager.CalculateRevisedQueueSize(
storedMonitoredItem.IsDurable,
storedMonitoredItem.QueueSize,
MaxQueueSize,
MaxDurableQueueSize);
bool success = m_monitoredItemManager.RestoreMonitoredItem(
Server,
m_syncNodeManager.ToAsyncNodeManager(),
context,
handle,
storedMonitoredItem,
savedOwnerIdentity,
AddNodeToComponentCache,
out ISampledDataChangeMonitoredItem restoredItem);
monitoredItem = restoredItem;
// report change.
OnMonitoredItemCreated(context, handle, restoredItem);
return true;
}
/// <summary>
/// Creates a new set of monitored items for a set of variables.
/// </summary>
/// <remarks>
/// This method only handles data change subscriptions. Event subscriptions are created by the SDK.
/// </remarks>
public virtual async ValueTask CreateMonitoredItemsAsync(
OperationContext context,
uint subscriptionId,
double publishingInterval,
TimestampsToReturn timestampsToReturn,
ArrayOf<MonitoredItemCreateRequest> itemsToCreate,
IList<ServiceResult> errors,
IList<MonitoringFilterResult> filterErrors,
IList<IMonitoredItem> monitoredItems,
bool createDurable,
MonitoredItemIdFactory monitoredItemIdFactory,
CancellationToken cancellationToken = default)
{
ServerSystemContext systemContext = SystemContext.Copy(context);
IDictionary<NodeId, NodeState> operationCache = new NodeIdDictionary<NodeState>();
var nodesToValidate = new List<NodeHandle>();
var createdItems = new List<IMonitoredItem>();
for (int ii = 0; ii < itemsToCreate.Count; ii++)
{
MonitoredItemCreateRequest itemToCreate = itemsToCreate[ii];
// skip items that have already been processed.
if (itemToCreate.Processed)
{
continue;
}
ReadValueId itemToMonitor = itemToCreate.ItemToMonitor;
// check for valid handle.
NodeHandle handle = await GetManagerHandleAsync(
systemContext,
itemToMonitor.NodeId,
operationCache,
cancellationToken).ConfigureAwait(false);
if (handle == null)
{
continue;
}
// owned by this node manager.
itemToCreate.Processed = true;
// must validate node in a separate operation.
errors[ii] = StatusCodes.BadNodeIdUnknown;
handle.Index = ii;
nodesToValidate.Add(handle);
}
// check for nothing to do.
if (nodesToValidate.Count == 0)
{
return;
}
await m_monitoredItemSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
try
{
// validates the nodes (reads values from the underlying data source if required).
for (int ii = 0; ii < nodesToValidate.Count; ii++)
{
NodeHandle handle = nodesToValidate[ii];
MonitoringFilterResult? filterResult;
IMonitoredItem? monitoredItem;
// validate node.
NodeState source = await ValidateNodeAsync(systemContext, handle, operationCache, cancellationToken).ConfigureAwait(false);
if (source == null)
{
continue;
}
MonitoredItemCreateRequest itemToCreate = itemsToCreate[handle.Index];
// create monitored item.
(errors[handle.Index], filterResult, monitoredItem) = await CreateMonitoredItemAsync(
systemContext,
handle,
subscriptionId,
publishingInterval,
context.DiagnosticsMask,
timestampsToReturn,
itemToCreate,
createDurable,
monitoredItemIdFactory,
cancellationToken).ConfigureAwait(false);
// save any filter error details.
filterErrors[handle.Index] = filterResult!;
if (ServiceResult.IsBad(errors[handle.Index]))
{
continue;
}
// save the monitored item.
monitoredItems[handle.Index] = monitoredItem!;
createdItems.Add(monitoredItem!);
}
m_monitoredItemManager.ApplyChanges();
}
finally
{
m_monitoredItemSemaphore.Release();
}
// do any post processing.
OnCreateMonitoredItemsComplete(systemContext, createdItems);
}
/// <summary>
/// Called when a batch of monitored items has been created.
/// </summary>
protected virtual void OnCreateMonitoredItemsComplete(
ServerSystemContext context,
IList<IMonitoredItem> monitoredItems)
{
// defined by the sub-class
}
/// <summary>
/// Creates a new set of monitored items for a set of variables.
/// </summary>
/// <remarks>
/// This method only handles data change subscriptions. Event subscriptions are created by the SDK.
/// </remarks>
protected virtual async ValueTask<(ServiceResult error, MonitoringFilterResult? filterResult, IMonitoredItem? monitoredItem)> CreateMonitoredItemAsync(
ServerSystemContext context,
NodeHandle handle,
uint subscriptionId,
double publishingInterval,
DiagnosticsMasks diagnosticsMasks,
TimestampsToReturn timestampsToReturn,
MonitoredItemCreateRequest itemToCreate,
bool createDurable,
MonitoredItemIdFactory monitoredItemId,
CancellationToken cancellationToken = default)
{
MonitoringFilterResult? filterResult = null;
IMonitoredItem? monitoredItem = null;
// validate parameters.
MonitoringParameters parameters = itemToCreate.RequestedParameters;
// validate attribute.
if (!Attributes.IsValid(handle.Node.NodeClass, itemToCreate.ItemToMonitor.AttributeId))
{
return (StatusCodes.BadAttributeIdInvalid, filterResult, monitoredItem);
}
// determine the sampling interval.
double samplingInterval = itemToCreate.RequestedParameters.SamplingInterval;
if (samplingInterval < 0)
{
samplingInterval = publishingInterval;
}
// ensure minimum sampling interval is not exceeded.
if (itemToCreate.ItemToMonitor.AttributeId == Attributes.Value &&
handle.Node is BaseVariableState variable &&
samplingInterval < variable.MinimumSamplingInterval)
{
samplingInterval = variable.MinimumSamplingInterval;
}
// put a large upper limit on sampling.
if (samplingInterval == double.MaxValue)
{
samplingInterval = 365 * 24 * 3600 * 1000.0;
}
// put an upper limit on queue size.
uint revisedQueueSize = SubscriptionManager.CalculateRevisedQueueSize(
createDurable,
itemToCreate.RequestedParameters.QueueSize,
MaxQueueSize,
MaxDurableQueueSize);
// validate the monitoring filter.
ValidateMonitoringFilterResult validateMonitoringFilterResult = await ValidateMonitoringFilterAsync(
context,
handle,
itemToCreate.ItemToMonitor.AttributeId,
samplingInterval,
revisedQueueSize,
parameters.Filter,
cancellationToken).ConfigureAwait(false);
if (ServiceResult.IsBad(validateMonitoringFilterResult.StatusCode))
{
return (validateMonitoringFilterResult.StatusCode, filterResult, monitoredItem);
}
ISampledDataChangeMonitoredItem dataChangeMonitoredItem =
m_monitoredItemManager.CreateMonitoredItem(
Server,
m_syncNodeManager.ToAsyncNodeManager(),
context,
handle,
subscriptionId,
publishingInterval,
diagnosticsMasks,
timestampsToReturn,
itemToCreate,
validateMonitoringFilterResult.Range,
validateMonitoringFilterResult.FilterToUse,
samplingInterval,
revisedQueueSize,
createDurable,
monitoredItemId,
AddNodeToComponentCache);
monitoredItem = dataChangeMonitoredItem;
// report the initial value.
ServiceResult error = ReadInitialValue(context, handle, dataChangeMonitoredItem);
if (ServiceResult.IsBad(error))
{
if (error.StatusCode == StatusCodes.BadAttributeIdInvalid ||
error.StatusCode == StatusCodes.BadDataEncodingInvalid ||
error.StatusCode == StatusCodes.BadDataEncodingUnsupported)
{
return (error, filterResult, monitoredItem);
}
error = StatusCodes.Good;
}
// report change.
OnMonitoredItemCreated(context, handle, dataChangeMonitoredItem);
return (error, filterResult, monitoredItem);
}
/// <summary>
/// Reads the initial value for a monitored item.
/// </summary>
/// <param name="context">The context.</param>
/// <param name="handle">The item handle.</param>
/// <param name="monitoredItem">The monitored item.</param>
protected virtual ServiceResult ReadInitialValue(
ISystemContext context,
NodeHandle handle,
IDataChangeMonitoredItem2 monitoredItem)
{
var initialValue = new DataValue(
Variant.Null,
StatusCodes.BadWaitingForInitialData,
DateTimeUtc.MinValue,
DateTime.UtcNow);
ServiceResult error = handle.Node.ReadAttribute(
context,
monitoredItem.AttributeId,
monitoredItem.IndexRange,
monitoredItem.DataEncoding,
ref initialValue);
monitoredItem.QueueValue(initialValue, error, true);
return error;
}
/// <summary>
/// Called after creating a MonitoredItem.
/// </summary>
/// <param name="context">The context.</param>
/// <param name="handle">The handle for the node.</param>
/// <param name="monitoredItem">The monitored item.</param>
protected virtual void OnMonitoredItemCreated(
ServerSystemContext context,
NodeHandle handle,
ISampledDataChangeMonitoredItem monitoredItem)
{
// overridden by the sub-class.
}
/// <summary>
/// Validates Role permissions for the specified NodeId
/// </summary>
public virtual async ValueTask<ServiceResult> ValidateRolePermissionsAsync(
OperationContext operationContext,
NodeId nodeId,
PermissionType requestedPermission,
CancellationToken cancellationToken = default)
{
if (requestedPermission == PermissionType.None)
{
// no permission is required hence the validation passes.
return StatusCodes.Good;
}
(object? nodeHandle, IAsyncNodeManager? nodeManager) = await Server.NodeManager
.GetManagerHandleAsync(nodeId, cancellationToken).ConfigureAwait(false);
if (nodeHandle == null || nodeManager == null)
{
// ignore unknown nodes.
return StatusCodes.Good;
}
NodeMetadata nodeMetadata = await nodeManager.GetNodeMetadataAsync(
operationContext,
nodeHandle,
BrowseResultMask.All,
cancellationToken).ConfigureAwait(false);
return MasterNodeManager.ValidateRolePermissions(
operationContext,
nodeMetadata,
requestedPermission);
}
/// <summary>
/// Validates if the specified event monitored item has enough permissions to receive the specified event
/// </summary>
public ValueTask<ServiceResult> ValidateEventRolePermissionsAsync(
IEventMonitoredItem monitoredItem,
IFilterTarget filterTarget,
CancellationToken cancellationToken = default)
{
NodeId eventTypeId = default;
NodeId sourceNodeId = default;
var baseEventState = filterTarget as BaseEventState;
if (baseEventState == null && filterTarget is InstanceStateSnapshot snapshot)
{
// try to get the event instance from snapshot object
baseEventState = snapshot.Handle as BaseEventState;
}
if (baseEventState != null)
{
eventTypeId = baseEventState.EventType?.Value ?? default;
sourceNodeId = baseEventState.SourceNode?.Value ?? default;
}
var operationContext = new OperationContext(monitoredItem);
return ValidateEventReceivePermissionsAsync(
operationContext,
eventTypeId,
sourceNodeId,
cancellationToken);
}
/// <summary>
/// Validates <see cref="PermissionType.ReceiveEvents"/> on both the
/// event type node and the source node of an event. Spec reference
/// Part 3 §8.55 (PermissionType bit 11): "A Client only receives an
/// Event if this bit is set on the Node identified by the
/// EventTypeId field and on the Node identified by the SourceNode
/// field." Exposed so callers that have already extracted the two
/// NodeIds (for example a per-monitored-item cache) can avoid the
/// extra event-state inspection that
/// <see cref="ValidateEventRolePermissionsAsync"/> performs.
/// </summary>
public async ValueTask<ServiceResult> ValidateEventReceivePermissionsAsync(
OperationContext operationContext,
NodeId eventTypeId,
NodeId sourceNodeId,
CancellationToken cancellationToken = default)
{
// validate the event type id permissions as specified
ServiceResult result = await ValidateRolePermissionsAsync(
operationContext,
eventTypeId,
PermissionType.ReceiveEvents,
cancellationToken).ConfigureAwait(false);
if (ServiceResult.IsBad(result))
{
return result;
}
// validate the source node id permissions as specified
return await ValidateRolePermissionsAsync(
operationContext,
sourceNodeId,
PermissionType.ReceiveEvents,
cancellationToken).ConfigureAwait(false);
}
/// <summary>
/// Represents the result of validating a monitoring filter, including the filter to use, the applicable range,
/// and the result of the filter evaluation.
/// </summary>
/// <remarks>This class encapsulates the necessary information to determine which monitoring
/// filter is applicable and the outcome of its evaluation within a specified range.</remarks>
public class ValidateMonitoringFilterResult
{
/// <summary>
/// The Status of the Validation.
/// </summary>
public StatusCode StatusCode { get; set; }
/// <summary>
/// The filter to use
/// </summary>
public MonitoringFilter FilterToUse { get; set; } = null!;
/// <summary>
/// The range to use
/// </summary>
public Range Range { get; set; } = null!;
/// <summary>
/// The filter result
/// </summary>
public MonitoringFilterResult FilterResult { get; set; } = null!;
}
/// <summary>
/// Validates the monitoring filter specified by the client.
/// </summary>
protected virtual async ValueTask<ValidateMonitoringFilterResult> ValidateMonitoringFilterAsync(
ServerSystemContext context,
NodeHandle handle,
uint attributeId,
double samplingInterval,
uint queueSize,
ExtensionObject filter,
CancellationToken cancellationToken = default)
{
var result = new ValidateMonitoringFilterResult();
// nothing to do if the filter is not specified.
if (filter.IsNull)
{
result.StatusCode = StatusCodes.Good;
return result;
}
// extension objects wrap any data structure. must check that the client provided the correct structure.
if (ExtensionObject.ToEncodeable(filter) is not DataChangeFilter deadbandFilter)
{
if (ExtensionObject.ToEncodeable(filter) is not AggregateFilter aggregateFilter ||
attributeId != Attributes.Value)
{
result.StatusCode = StatusCodes.BadFilterNotAllowed;
return result;
}
if (!Server.AggregateManager.IsSupported(aggregateFilter.AggregateType))
{
result.StatusCode = StatusCodes.BadAggregateNotSupported;
return result;
}
var revisedFilter = new ServerAggregateFilter
{
AggregateType = aggregateFilter.AggregateType,
StartTime = aggregateFilter.StartTime,
ProcessingInterval = aggregateFilter.ProcessingInterval,
AggregateConfiguration = aggregateFilter.AggregateConfiguration,
Stepped = false
};
StatusCode error = await ReviseAggregateFilterAsync(
context,
handle,
samplingInterval,
queueSize,
revisedFilter,
cancellationToken).ConfigureAwait(false);
if (StatusCode.IsBad(error))
{
result.StatusCode = error;
return result;
}
var aggregateFilterResult = new AggregateFilterResult
{
RevisedProcessingInterval = aggregateFilter.ProcessingInterval,
RevisedStartTime = aggregateFilter.StartTime,
RevisedAggregateConfiguration = aggregateFilter.AggregateConfiguration
};
result.FilterToUse = revisedFilter;
result.FilterResult = aggregateFilterResult;
result.StatusCode = StatusCodes.Good;
return result;
}
// deadband filters only allowed for variable values.
if (attributeId != Attributes.Value)
{
result.StatusCode = StatusCodes.BadFilterNotAllowed;
return result;
}
if (handle.Node is not BaseVariableState variable)
{
result.StatusCode = StatusCodes.BadFilterNotAllowed;
return result;
}
// check for status filter.
if (deadbandFilter.DeadbandType == (uint)DeadbandType.None)
{
result.FilterToUse = deadbandFilter;
result.StatusCode = StatusCodes.Good;
return result;
}
// deadband filters can only be used for numeric values.
if (!Server.TypeTree.IsTypeOf(variable.DataType, DataTypeIds.Number))
{
result.StatusCode = StatusCodes.BadFilterNotAllowed;
return result;
}
// nothing more to do for absolute filters.
if (deadbandFilter.DeadbandType == (uint)DeadbandType.Absolute)
{
result.FilterToUse = deadbandFilter;
result.StatusCode = StatusCodes.Good;
return result;
}
// need to look up the EU range if a percent filter is requested.
if (deadbandFilter.DeadbandType == (uint)DeadbandType.Percent)
{
lock (handle.Node)
{
if (handle.Node.FindChild(
context,
QualifiedName.From(BrowseNames.EURange)) is not PropertyState property)
{
result.StatusCode = StatusCodes.BadMonitoredItemFilterUnsupported;
return result;
}
Range tmpRange;
bool gotRange = property.Value.TryGetStructure(out tmpRange!);
if (!gotRange)
{
result.StatusCode = StatusCodes.BadMonitoredItemFilterUnsupported;
return result;
}
Range range = tmpRange;
result.FilterToUse = deadbandFilter;
result.Range = range!;
result.StatusCode = StatusCodes.Good;
return result;
}
}
// no other type of filter supported.
result.StatusCode = StatusCodes.BadFilterNotAllowed;
return result;
}
/// <summary>
/// Revises an aggregate filter (may require knowledge of the variable being used).
/// </summary>
/// <param name="context">The context.</param>
/// <param name="handle">The handle.</param>
/// <param name="samplingInterval">The sampling interval for the monitored item.</param>
/// <param name="queueSize">The queue size for the monitored item.</param>
/// <param name="filterToUse">The filter to revise.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Good if the </returns>
protected virtual ValueTask<StatusCode> ReviseAggregateFilterAsync(
ServerSystemContext context,
NodeHandle handle,
double samplingInterval,
uint queueSize,
ServerAggregateFilter filterToUse,
CancellationToken cancellationToken = default)
{
if (filterToUse.ProcessingInterval < samplingInterval)
{
filterToUse.ProcessingInterval = samplingInterval;
}
if (filterToUse.ProcessingInterval < Server.AggregateManager.MinimumProcessingInterval)
{
filterToUse.ProcessingInterval = Server.AggregateManager.MinimumProcessingInterval;
}
DateTime earliestStartTime = DateTime.UtcNow.AddMilliseconds(
-(queueSize - 1) * filterToUse.ProcessingInterval);
if (earliestStartTime > filterToUse.StartTime)
{
filterToUse.StartTime = earliestStartTime;
}
if (filterToUse.AggregateConfiguration.UseServerCapabilitiesDefaults)
{
filterToUse.AggregateConfiguration = Server.AggregateManager
.GetDefaultConfiguration(default);
}
return new ValueTask<StatusCode>(StatusCodes.Good);
}
/// <summary>
/// Modifies the parameters for a set of monitored items.
/// </summary>
public virtual async ValueTask ModifyMonitoredItemsAsync(
OperationContext context,
TimestampsToReturn timestampsToReturn,
IList<IMonitoredItem> monitoredItems,
ArrayOf<MonitoredItemModifyRequest> itemsToModify,
IList<ServiceResult> errors,
IList<MonitoringFilterResult> filterErrors,
CancellationToken cancellationToken = default)
{
ServerSystemContext systemContext = SystemContext.Copy(context);
var nodesInNamespace = new List<(int, NodeHandle)>(monitoredItems.Count);
for (int ii = 0; ii < monitoredItems.Count; ii++)
{
MonitoredItemModifyRequest itemToModify = itemsToModify[ii];
// skip items that have already been processed.
if (itemToModify.Processed || monitoredItems[ii] == null)
{
continue;
}
// check handle.
NodeHandle? handle = IsHandleInNamespace(monitoredItems[ii].ManagerHandle);
if (handle == null)
{
continue;
}
nodesInNamespace.Add((ii, handle));
}
if (nodesInNamespace.Count == 0)
{
return;
}
var modifiedItems = new List<IMonitoredItem>();
await m_monitoredItemSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
try
{
foreach ((int, NodeHandle) nodeInNamespace in nodesInNamespace)
{
int ii = nodeInNamespace.Item1;
NodeHandle handle = nodeInNamespace.Item2;
MonitoredItemModifyRequest itemToModify = itemsToModify[ii];
// owned by this node manager.
itemToModify.Processed = true;
// modify the monitored item.
(ServiceResult error, MonitoringFilterResult? filterResult) = await ModifyMonitoredItemAsync(
systemContext,
context.DiagnosticsMask,
timestampsToReturn,
monitoredItems[ii],
itemToModify,
handle,
cancellationToken).ConfigureAwait(false);
errors[ii] = error;
filterErrors[ii] = filterResult!;
// save the modified item.
if (ServiceResult.IsGood(errors[ii]))
{
modifiedItems.Add(monitoredItems[ii]);
}
}
m_monitoredItemManager.ApplyChanges();
}
finally
{
m_monitoredItemSemaphore.Release();
}
// do any post processing.
await OnModifyMonitoredItemsCompleteAsync(systemContext, modifiedItems, cancellationToken).ConfigureAwait(false);
}
/// <summary>
/// Called when a batch of monitored items has been modified.
/// </summary>
protected virtual ValueTask OnModifyMonitoredItemsCompleteAsync(
ServerSystemContext context,
IList<IMonitoredItem> monitoredItems,
CancellationToken cancellationToken = default)
{
// defined by the sub-class
return new ValueTask();
}
/// <summary>
/// Modifies the parameters for a monitored item.
/// </summary>
protected virtual async ValueTask<(ServiceResult error, MonitoringFilterResult? filterResult)> ModifyMonitoredItemAsync(
ServerSystemContext context,
DiagnosticsMasks diagnosticsMasks,
TimestampsToReturn timestampsToReturn,
IMonitoredItem monitoredItem,
MonitoredItemModifyRequest itemToModify,
NodeHandle handle,
CancellationToken cancellationToken = default)
{
// check for valid monitored item.
var datachangeItem = monitoredItem as ISampledDataChangeMonitoredItem;
// validate parameters.
MonitoringParameters parameters = itemToModify.RequestedParameters;
double previousSamplingInterval = datachangeItem!.SamplingInterval;
// check if the variable needs to be sampled.
double samplingInterval = itemToModify.RequestedParameters.SamplingInterval;
if (samplingInterval < 0)
{
samplingInterval = previousSamplingInterval;
}
// ensure minimum sampling interval is not exceeded.
if (datachangeItem.AttributeId == Attributes.Value &&
handle.Node is BaseVariableState variable &&
samplingInterval < variable.MinimumSamplingInterval)
{
samplingInterval = variable.MinimumSamplingInterval;
}
// put a large upper limit on sampling.
if (samplingInterval == double.MaxValue)
{
samplingInterval = 365 * 24 * 3600 * 1000.0;
}
// put an upper limit on queue size.
uint revisedQueueSize = SubscriptionManager.CalculateRevisedQueueSize(
monitoredItem.IsDurable,
itemToModify.RequestedParameters.QueueSize,
MaxQueueSize,
MaxDurableQueueSize);
// validate the monitoring filter.
ValidateMonitoringFilterResult validateMonitoringFilterResult = await ValidateMonitoringFilterAsync(
context,
handle,
datachangeItem.AttributeId,
samplingInterval,
revisedQueueSize,
parameters.Filter,
cancellationToken).ConfigureAwait(false);
if (ServiceResult.IsBad(validateMonitoringFilterResult.StatusCode))
{
return (validateMonitoringFilterResult.StatusCode, validateMonitoringFilterResult.FilterResult);
}
// modify the monitored item parameters.
ServiceResult? error = m_monitoredItemManager!.ModifyMonitoredItem(
context,
diagnosticsMasks,
timestampsToReturn,
validateMonitoringFilterResult.FilterToUse,
validateMonitoringFilterResult.Range,
samplingInterval,
revisedQueueSize,
datachangeItem,
itemToModify);
// report change.
if (ServiceResult.IsGood(error))
{
await OnMonitoredItemModifiedAsync(context, handle, datachangeItem, cancellationToken).ConfigureAwait(false);
}
return (error!, validateMonitoringFilterResult.FilterResult);
}
/// <summary>
/// Called after modifying a MonitoredItem.
/// </summary>
/// <param name="context">The context.</param>
/// <param name="handle">The handle for the node.</param>
/// <param name="monitoredItem">The monitored item.</param>
/// <param name="cancellationToken">The cancellation token.</param>
protected virtual ValueTask OnMonitoredItemModifiedAsync(
ServerSystemContext context,
NodeHandle handle,
ISampledDataChangeMonitoredItem monitoredItem,
CancellationToken cancellationToken = default)
{
// overridden by the sub-class.
return new ValueTask();
}
/// <summary>
/// Deletes a set of monitored items.
/// </summary>
public virtual async ValueTask DeleteMonitoredItemsAsync(
OperationContext context,
IList<IMonitoredItem> monitoredItems,
IList<bool> processedItems,
IList<ServiceResult> errors,
CancellationToken cancellationToken = default)
{
ServerSystemContext systemContext = SystemContext.Copy(context);
var nodesInNamespace = new List<(int, NodeHandle)>(monitoredItems.Count);
for (int ii = 0; ii < monitoredItems.Count; ii++)
{
// skip items that have already been processed.
if (processedItems[ii] || monitoredItems[ii] == null)
{
continue;
}
// check handle.
NodeHandle? handle = IsHandleInNamespace(monitoredItems[ii].ManagerHandle);
if (handle == null)
{
continue;
}
nodesInNamespace.Add((ii, handle));
}
if (nodesInNamespace.Count == 0)
{
return;
}
var deletedItems = new List<IMonitoredItem>();
await m_monitoredItemSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
try
{
foreach ((int, NodeHandle) nodeInNamespace in nodesInNamespace)
{
int ii = nodeInNamespace.Item1;
NodeHandle handle = nodeInNamespace.Item2;
// owned by this node manager.
processedItems[ii] = true;
errors[ii] = await DeleteMonitoredItemAsync(systemContext,
monitoredItems[ii],
handle,
cancellationToken).ConfigureAwait(false);
// save the modified item.
if (ServiceResult.IsGood(errors[ii]))
{
deletedItems.Add(monitoredItems[ii]);
RemoveNodeFromComponentCache(systemContext, handle);
}
}
m_monitoredItemManager.ApplyChanges();
}
finally
{
m_monitoredItemSemaphore.Release();
}
// do any post processing.
await OnDeleteMonitoredItemsCompleteAsync(systemContext, deletedItems, cancellationToken).ConfigureAwait(false);
}
/// <summary>
/// Called when a batch of monitored items has been modified.
/// </summary>
protected virtual ValueTask OnDeleteMonitoredItemsCompleteAsync(
ServerSystemContext context,
IList<IMonitoredItem> monitoredItems,
CancellationToken cancellationToken = default)
{
// defined by the sub-class
return new ValueTask();
}
/// <summary>
/// Deletes a monitored item.
/// </summary>
protected virtual async ValueTask<ServiceResult> DeleteMonitoredItemAsync(
ServerSystemContext context,
IMonitoredItem monitoredItem,
NodeHandle handle,
CancellationToken cancellationToken = default)
{
var sampledDataChangeMonitoredItem = monitoredItem as ISampledDataChangeMonitoredItem;
StatusCode statusCode = m_monitoredItemManager.DeleteMonitoredItem(
context,
sampledDataChangeMonitoredItem!,
handle);
// report change.
await OnMonitoredItemDeletedAsync(context, handle, sampledDataChangeMonitoredItem!, cancellationToken).ConfigureAwait(false);
return statusCode;
}
/// <summary>
/// Called after deleting a MonitoredItem.
/// </summary>
/// <param name="context">The context.</param>
/// <param name="handle">The handle for the node.</param>
/// <param name="monitoredItem">The monitored item.</param>
/// <param name="cancellationToken">The cancellation token.</param>
protected virtual ValueTask OnMonitoredItemDeletedAsync(
ServerSystemContext context,
NodeHandle handle,
ISampledDataChangeMonitoredItem monitoredItem,
CancellationToken cancellationToken = default)
{
// overridden by the sub-class.
return new ValueTask();
}
/// <summary>
/// Transfers a set of monitored items.
/// </summary>
/// <param name="context">The context.</param>
/// <param name="sendInitialValues">Whether the subscription should send initial values after transfer.</param>
/// <param name="monitoredItems">The set of monitoring items to update.</param>
/// <param name="processedItems">The list of bool with items that were already processed.</param>
/// <param name="errors">Any errors.</param>
/// <param name="cancellationToken">The cancellation token.</param>
public virtual async ValueTask TransferMonitoredItemsAsync(
OperationContext context,
bool sendInitialValues,
IList<IMonitoredItem> monitoredItems,
IList<bool> processedItems,
IList<ServiceResult> errors,
CancellationToken cancellationToken = default)
{
ServerSystemContext systemContext = SystemContext.Copy(context);
var transferredItems = new List<IMonitoredItem>();
await m_monitoredItemSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
try
{
for (int ii = 0; ii < monitoredItems.Count; ii++)
{
// skip items that have already been processed.
if (processedItems[ii] || monitoredItems[ii] == null)
{
continue;
}
// check handle.
NodeHandle? handle = IsHandleInNamespace(monitoredItems[ii].ManagerHandle);
if (handle == null)
{
continue;
}
// owned by this node manager.
processedItems[ii] = true;
transferredItems.Add(monitoredItems[ii]);
if (sendInitialValues)
{
monitoredItems[ii].SetupResendDataTrigger();
}
errors[ii] = StatusCodes.Good;
}
}
finally
{
m_monitoredItemSemaphore.Release();
}
// do any post processing.
await OnMonitoredItemsTransferredAsync(systemContext, transferredItems, cancellationToken).ConfigureAwait(false);
}
/// <summary>
/// Called after transfer of MonitoredItems.
/// </summary>
/// <param name="context">The context.</param>
/// <param name="monitoredItems">The transferred monitored items.</param>
/// <param name="cancellationToken">The cancellation token.</param>
protected virtual ValueTask OnMonitoredItemsTransferredAsync(
ServerSystemContext context,
IList<IMonitoredItem> monitoredItems,
CancellationToken cancellationToken = default)
{
// defined by the sub-class
return new ValueTask();
}
/// <summary>
/// Changes the monitoring mode for a set of monitored items.
/// </summary>
/// <param name="context">The context.</param>
/// <param name="monitoringMode">The monitoring mode.</param>
/// <param name="monitoredItems">The set of monitoring items to update.</param>
/// <param name="processedItems">Flags indicating which items have been processed.</param>
/// <param name="errors">Any errors.</param>
/// <param name="cancellationToken">The cancellation token.</param>
public virtual async ValueTask SetMonitoringModeAsync(
OperationContext context,
MonitoringMode monitoringMode,
IList<IMonitoredItem> monitoredItems,
IList<bool> processedItems,
IList<ServiceResult> errors,
CancellationToken cancellationToken = default)
{
ServerSystemContext systemContext = SystemContext.Copy(context);
var nodesInNamespace = new List<(int, NodeHandle)>(monitoredItems.Count);
for (int ii = 0; ii < monitoredItems.Count; ii++)
{
// skip items that have already been processed.
if (processedItems[ii] || monitoredItems[ii] == null)
{
continue;
}
// check handle.
NodeHandle? handle = IsHandleInNamespace(monitoredItems[ii].ManagerHandle);
if (handle == null)
{
continue;
}
nodesInNamespace.Add((ii, handle));
}
if (nodesInNamespace.Count == 0)
{
return;
}
var changedItems = new List<IMonitoredItem>();
await m_monitoredItemSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
try
{
foreach ((int, NodeHandle) nodeInNamespace in nodesInNamespace)
{
int ii = nodeInNamespace.Item1;
NodeHandle handle = nodeInNamespace.Item2;
// indicate whether it was processed or not.
processedItems[ii] = true;
// update monitoring mode.
errors[ii] = await SetMonitoringModeAsync(
systemContext,
monitoredItems[ii],
monitoringMode,
handle,
cancellationToken).ConfigureAwait(false);
// save the modified item.
if (ServiceResult.IsGood(errors[ii]))
{
changedItems.Add(monitoredItems[ii]);
}
}
m_monitoredItemManager.ApplyChanges();
}
finally
{
m_monitoredItemSemaphore.Release();
}
// do any post processing.
await OnSetMonitoringModeCompleteAsync(systemContext, changedItems, cancellationToken).ConfigureAwait(false);
}
/// <summary>
/// Called when a batch of monitored items has their monitoring mode changed.
/// </summary>
protected virtual ValueTask OnSetMonitoringModeCompleteAsync(
ServerSystemContext context,
IList<IMonitoredItem> monitoredItems,
CancellationToken cancellationToken = default)
{
// defined by the sub-class
return new ValueTask();
}
/// <summary>
/// Changes the monitoring mode for an item.
/// </summary>
protected virtual async ValueTask<ServiceResult> SetMonitoringModeAsync(
ServerSystemContext context,
IMonitoredItem monitoredItem,
MonitoringMode monitoringMode,
NodeHandle handle,
CancellationToken cancellationToken = default)
{
var sampledDataChangeMonitoredItem = monitoredItem as ISampledDataChangeMonitoredItem;
(ServiceResult result, MonitoringMode? previousMode) = await m_monitoredItemManager
.SetMonitoringModeAsync(
context,
sampledDataChangeMonitoredItem!,
monitoringMode,
handle,
cancellationToken).ConfigureAwait(false);
// report change.
if (ServiceResult.IsGood(result) && previousMode != monitoringMode)
{
await OnMonitoringModeChangedAsync(
context,
handle,
sampledDataChangeMonitoredItem!,
(MonitoringMode)previousMode!,
monitoringMode,
cancellationToken).ConfigureAwait(false);
}
return result;
}
/// <summary>
/// Called after changing the MonitoringMode for a MonitoredItem.
/// </summary>
/// <param name="context">The context.</param>
/// <param name="handle">The handle for the node.</param>
/// <param name="monitoredItem">The monitored item.</param>
/// <param name="previousMode">The previous monitoring mode.</param>
/// <param name="monitoringMode">The current monitoring mode.</param>
/// <param name="cancellationToken">The cancellation token.</param>
protected virtual ValueTask OnMonitoringModeChangedAsync(
ServerSystemContext context,
NodeHandle handle,
ISampledDataChangeMonitoredItem monitoredItem,
MonitoringMode previousMode,
MonitoringMode monitoringMode,
CancellationToken cancellationToken = default)
{
// overridden by the sub-class.
return new ValueTask();
}
/// <summary>
/// Called when a session is closed.
/// </summary>
public virtual ValueTask SessionClosingAsync(
OperationContext context,
NodeId sessionId,
bool deleteSubscriptions,
CancellationToken cancellationToken = default)
{
// overridden by the sub-class.
return new ValueTask();
}
/// <summary>
/// Called when a session is activated and the user identity has changed.
/// Invalidates cached role permissions for all monitored items belonging to the session
/// so that permissions are re-evaluated on the next data change notification.
/// </summary>
public virtual ValueTask SessionActivatedAsync(
OperationContext context,
NodeId sessionId,
CancellationToken cancellationToken = default)
{
foreach (MonitoredNode2 monitoredNode in MonitoredNodes.Values)
{
monitoredNode.InvalidatePermissionCacheForSession(sessionId);
}
return default;
}
/// <summary>
/// Returns true if a node is in a view.
/// </summary>
public virtual async ValueTask<bool> IsNodeInViewAsync(
OperationContext context,
NodeId viewId,
object nodeHandle,
CancellationToken cancellationToken = default)
{
if (nodeHandle is not NodeHandle handle)
{
return false;
}
if (handle.Node != null)
{
return await IsNodeInViewAsync(context, viewId, handle.Node, cancellationToken).ConfigureAwait(false);
}
return false;
}
/// <summary>
/// Returns the metadata containing the AccessRestrictions, RolePermissions and UserRolePermissions for the node.
/// Returns null if the node does not exist.
/// </summary>
/// <remarks>
/// This method validates any placeholder handle.
/// </remarks>
public async ValueTask<NodeMetadata?> GetPermissionMetadataAsync(
OperationContext context,
object targetHandle,
BrowseResultMask resultMask,
Dictionary<NodeId, Variant[]> uniqueNodesServiceAttributesCache,
bool permissionsOnly,
CancellationToken cancellationToken = default)
{
ServerSystemContext systemContext = SystemContext.Copy(context);
// check for valid handle.
NodeHandle? handle = IsHandleInNamespace(targetHandle);
if (handle == null)
{
return null!;
}
// validate node.
NodeState? target = await ValidateNodeAsync(systemContext, handle, null!, cancellationToken).ConfigureAwait(false);
if (target == null)
{
return null!;
}
var values = new Variant[3];
// construct the meta-data object.
var metadata = new NodeMetadata(target, target.NodeId)
{
IsPartOfTypeHierarchy = target.IsPartOfTypeHierarchy
};
// Treat the case of calls originating from the optimized services that use the cache (Read, Browse and Call services)
if (uniqueNodesServiceAttributesCache != null)
{
NodeId key = handle.NodeId;
if (uniqueNodesServiceAttributesCache.ContainsKey(key))
{
if (uniqueNodesServiceAttributesCache[key].Length != 3)
{
ReadAndCacheValidationAttributes(
uniqueNodesServiceAttributesCache,
systemContext,
target,
key,
ref values);
}
else
{
// Retrieve value from cache
values = uniqueNodesServiceAttributesCache[key];
}
}
else
{
ReadAndCacheValidationAttributes(
uniqueNodesServiceAttributesCache,
systemContext,
target,
key,
ref values);
}
SetAccessAndRolePermissions(values, metadata);
} // All other calls that do not use the cache
else if (permissionsOnly)
{
ReadValidationAttributes(systemContext, target, ref values);
SetAccessAndRolePermissions(values, metadata);
}
await SetDefaultPermissionsAsync(systemContext, target, metadata, cancellationToken).ConfigureAwait(false);
return metadata;
}
/// <summary>
/// Set the metadata default permission values for DefaultAccessRestrictions, DefaultRolePermissions and DefaultUserRolePermissions
/// </summary>
private async ValueTask SetDefaultPermissionsAsync(
ServerSystemContext systemContext,
NodeState target,
NodeMetadata metadata,
CancellationToken cancellationToken = default)
{
// check if NamespaceMetadata is defined for NamespaceIndex of the node.
NamespaceMetadataState? namespaceMetadataState = await
Server.NodeManager.ConfigurationNodeManager!.GetNamespaceMetadataStateAsync(target.NodeId.NamespaceIndex, cancellationToken)
.ConfigureAwait(false);
if (namespaceMetadataState != null)
{
Variant value = default;
DateTimeUtc sourceTimestamp = DateTimeUtc.MinValue;
if (namespaceMetadataState.DefaultAccessRestrictions != null)
{
// get DefaultAccessRestrictions for Namespace
namespaceMetadataState.DefaultAccessRestrictions
.ReadAttribute(
systemContext,
Attributes.Value,
ref value,
ref sourceTimestamp);
if (!value.IsNull)
{
metadata.DefaultAccessRestrictions =
value.GetEnumeration<AccessRestrictionType>();
}
}
if (namespaceMetadataState.DefaultRolePermissions != null)
{
// get DefaultRolePermissions for Namespace
namespaceMetadataState.DefaultRolePermissions
.ReadAttribute(
systemContext,
Attributes.Value,
ref value,
ref sourceTimestamp);
if (!value.IsNull && value.TryGetStructure(out ArrayOf<RolePermissionType> rolePermissions))
{
metadata.DefaultRolePermissions = rolePermissions;
}
}
if (namespaceMetadataState.DefaultUserRolePermissions != null)
{
// get DefaultUserRolePermissions for Namespace
namespaceMetadataState.DefaultUserRolePermissions
.ReadAttribute(
systemContext,
Attributes.Value,
ref value,
ref sourceTimestamp);
if (!value.IsNull && value.TryGetStructure(out ArrayOf<RolePermissionType> userRolePermissions))
{
metadata.DefaultUserRolePermissions = userRolePermissions;
}
}
}
}
/// <summary>
/// Stores a reference count for entries in the component cache.
/// </summary>
private class CacheEntry
{
public int RefCount;
public NodeState? Entry;
}
/// <summary>
/// Looks up a component in cache.
/// </summary>
protected NodeState? LookupNodeInComponentCache(ISystemContext context, NodeHandle handle)
{
if (m_componentCache == null)
{
return null;
}
m_componentCacheSemaphore.Wait();
try
{
CacheEntry? entry = null;
if (!string.IsNullOrEmpty(handle.ComponentPath))
{
if (m_componentCache.TryGetValue(handle.RootId, out entry))
{
return entry.Entry!.FindChildBySymbolicName(context, handle.ComponentPath);
}
}
else if (m_componentCache.TryGetValue(handle.NodeId, out entry))
{
return entry.Entry;
}
return null;
}
finally
{
m_componentCacheSemaphore.Release();
}
}
/// <summary>
/// Removes a reference to a component in thecache.
/// </summary>
protected void RemoveNodeFromComponentCache(ISystemContext context, NodeHandle handle)
{
if (handle == null)
{
return;
}
m_componentCacheSemaphore.Wait();
try
{
if (m_componentCache != null)
{
NodeId nodeId = handle.NodeId;
if (!string.IsNullOrEmpty(handle.ComponentPath))
{
nodeId = handle.RootId;
}
if (m_componentCache.TryGetValue(nodeId, out CacheEntry? entry))
{
entry.RefCount--;
if (entry.RefCount == 0)
{
m_componentCache.Remove(nodeId);
}
}
}
}
finally
{
m_componentCacheSemaphore.Release();
}
}
/// <summary>
/// Adds a node to the component cache.
/// </summary>
protected NodeState AddNodeToComponentCache(
ISystemContext context,
NodeHandle handle,
NodeState node)
{
if (handle == null)
{
return node;
}
m_componentCacheSemaphore.Wait();
try
{
m_componentCache ??= [];
// check if a component is actually specified.
if (!string.IsNullOrEmpty(handle.ComponentPath))
{
if (m_componentCache.TryGetValue(handle.RootId, out CacheEntry? entry))
{
entry.RefCount++;
if (!string.IsNullOrEmpty(handle.ComponentPath))
{
return entry.Entry!
.FindChildBySymbolicName(context, handle.ComponentPath)!;
}
return entry.Entry!;
}
NodeState root = node.GetHierarchyRoot();
if (root != null)
{
entry = new CacheEntry { RefCount = 1, Entry = root };
m_componentCache.Add(handle.RootId, entry);
}
}
// simply add the node to the cache.
else
{
if (m_componentCache.TryGetValue(handle.NodeId, out CacheEntry? entry))
{
entry.RefCount++;
return entry.Entry!;
}
entry = new CacheEntry { RefCount = 1, Entry = node };
m_componentCache.Add(handle.NodeId, entry);
}
return node;
}
finally
{
m_componentCacheSemaphore.Release();
}
}
private IReadOnlyList<string>? m_namespaceUris;
private ushort[] m_namespaceIndexes;
private NodeIdDictionary<CacheEntry>? m_componentCache;
private readonly SemaphoreSlim m_componentCacheSemaphore = new(1, 1);
/// <summary>
/// A logger to use
/// </summary>
#pragma warning disable IDE1006 // Naming Styles
protected ILogger m_logger { get; }
#pragma warning restore IDE1006 // Naming Styles
/// <summary>
/// A wrapper for the browser that provides a lock.
/// </summary>
private class BrowserContext : IDisposable
{
public INodeBrowser Browser { get; }
public SemaphoreSlim Semaphore { get; } = new(1, 1);
public BrowserContext(INodeBrowser browser)
{
Browser = browser;
}
public void Dispose()
{
Browser.Dispose();
Semaphore.Dispose();
}
}
/// <summary>
/// the monitored item manager of the NodeManager
/// </summary>
protected IMonitoredItemManager m_monitoredItemManager;
/// <summary>
/// the sync NodeManager adapter
/// </summary>
protected INodeManager3 m_syncNodeManager;
/// <summary>
/// The synchronaization primitive used to synchronize write operations;
/// </summary>
protected SemaphoreSlim m_writeSemaphore = new(1, 1);
/// <summary>
/// The synchronaization primitive used to protect access to operations affecting the MonitoredItems owned by the NodeManager.
/// </summary>
protected SemaphoreSlim m_monitoredItemSemaphore = new(1, 1);
/// <summary>
/// Set of <see cref="NodeId"/>s that opt into multiple event consumer
/// task handling. Nodes in this set will use dynamic scaling of
/// consumer tasks based on the number of event monitored items.
/// </summary>
internal NodeIdDictionary<bool> MultiConsumerNodeIds { get; } = [];
/// <summary>
/// Counter for the NodeIdFactory.New Method
/// </summary>
private uint m_lastUsedNodeId;
}
}