Files
Marc Schier 81b3c9b371 [Server] Implement NodeManagement service set and fix DeleteAddressSpace (#3839)
# Description

Adds the OPC UA NodeManagement service set (AddNodes / DeleteNodes /
AddReferences / DeleteReferences) to the server SDK and fixes a
long-standing shutdown bug where `NodeState.Delete` callbacks never
fired on address-space teardown.

### NodeManagement service set (#3736)

- New optional `INodeManagementAsyncNodeManager` facet interface (added
to `INodeManager.cs` alongside other facets, discovered via `as`,
**not** part of the `IAsyncNodeManager` composite). Opt-in per
NodeManager via `AllowNodeManagement` — default is `false` so existing
NodeManagers preserve binary compatibility.
- Dispatcher in `MasterNodeManager` routes:
- **AddNodes** by `RequestedNewNodeId.NamespaceIndex` when supplied,
otherwise to the parent's owning NodeManager. When the parent's owner is
read-only (e.g. `ObjectsFolder` owned by `CoreNodeManager`), the
dispatcher falls back to the first opted-in NodeManager so conformance
flows work out of the box.
- **DeleteNodes** / **AddReferences** / **DeleteReferences** to the
owning NodeManager of the target or source node, returning
`BadUserAccessDenied` when that NodeManager has not opted in.
Cross-NodeManager `AddReference` / `DeleteReference` best-effort mirrors
the inverse edge on the target's NodeManager.
- `AsyncCustomNodeManager` provides a default implementation: per-item
validation, `RequestedNewNodeId` honored, browse-name dedup for both
local and cross-NodeManager parents, `DeleteTargetReferences` semantics,
and a NodeId allocator fallback when an `INodeIdFactory.New` override
returns a null `NodeId` (e.g. parent-based hierarchical naming with no
parent attached).
- `StandardServer` adds the four service overrides plumbed through
`OperationLimits.MaxNodesPerNodeManagement`, audit events, and
`OnRequestComplete`; new `ReportAuditAddReferencesEvent` /
`ReportAuditDeleteReferencesEvent` helpers in `AuditEvents`.
- Quickstarts `ReferenceNodeManager` opts in via `AllowNodeManagement =>
true`; the legacy partial-class override in
`ReferenceServer.NodeManagement.cs` is removed.

### `NodeState.Delete` on shutdown (#3762)

- `CustomNodeManager.DeleteAddressSpace` and
`AsyncCustomNodeManager.DeleteAddressSpaceAsync` now snapshot root
`PredefinedNodes` (skipping children whose parent is in the snapshot to
prevent double-fire), invoke `NodeState.Delete(SystemContext)` on each
root (which recurses into children and raises `OnBeforeDelete` /
`OnAfterDelete` / `NodeStateChangeMasks.Deleted`), and then clear the
map.
- Derived overrides in Quickstarts `ReferenceNodeManager`,
`SampleNodeManager`, and `FileSystemNodeManager` chain to `base` so the
callback fires for their nodes too.

### Tests

- **21 new dispatcher unit tests** in
`Tests/Opc.Ua.Server.Tests/NodeManager/AsyncCustomNodeManagerNodeManagementTests.cs`
covering `AllowNodeManagement` default/opt-in, AddNode validation
(`BadBrowseNameInvalid`, `BadParentNodeIdInvalid`, `BadNodeIdRejected`,
`BadNodeIdExists`, `BadBrowseNameDuplicated`, `BadNodeClassInvalid`),
RequestedNewNodeId paths, DeleteNode + `DeleteTargetReferences`,
AddReference duplicate detection (`BadDuplicateReferenceNotAllowed`),
and DeleteReference (`BadNoMatch`).
- **2 new DeleteAddressSpaceAsync tests** in
`AsyncCustomNodeManagerTests` verifying `NodeState.Delete` invocation
and no double-fire on children.
- Updated InformationModel
`NodeManagementAddRefTests.AddReferenceBetweenExistingNodesSucceedsAsync`
to accept `BadUserAccessDenied` / `BadNotSupported` when the source NM
has opted out.
- **Full `Opc.Ua.Server.Tests` on net10.0: 1709 passed, 0 failed.**
- **All 26 InformationModel `Category=NodeManagement` conformance tests
pass** against the reference server (1 skipped where source NM opts
out).

### Docs

- New [`Docs/NodeManagement.md`](Docs/NodeManagement.md) with
architecture, opt-in pattern, dispatch and routing rules,
request-handling table, error-code table, audit events, and
custom-behavior examples.
- `Docs/MigrationGuide.md` gets two new sections — *NodeManagement
service set* (downstream migration steps) and *DeleteAddressSpace
invokes NodeState.Delete (#3762)* (shutdown behavior change) — with TOC
entries.
- `Docs/README.md` links the new `NodeManagement.md`.

## Related Issues

- Fixes #3736
- Fixes #3762

## Checklist

_Put an `x` in the boxes that apply. You can complete these step by step
after opening the PR._

- [ ] 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.
- [x] I have added all necessary documentation.
- [x] I have verified that my changes do not introduce (new) build or
analyzer warnings.
- [ ] I ran **all** tests locally using the **UA.slnx** solution against
at least .net **framework** and .net **10**, and all passed.
- [ ] I fixed **all** failing and flaky tests in the CI pipelines and
**all** CodeQL warnings.
- [ ] I have addressed **all** PR feedback received.
2026-06-04 13:02:32 +02:00

855 lines
32 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.Generic;
using System.Threading;
using System.Threading.Tasks;
namespace Opc.Ua.Server
{
/// <summary>
/// The factory for an <see cref="AsyncNodeManagerAdapter"/>
/// </summary>
public static class AsyncNodeManagerAdapterFactory
{
/// <summary>
/// returns an instance of <see cref="IAsyncNodeManager"/>
/// if the NodeManager does not implement the interface uses the <see cref="AsyncNodeManagerAdapter"/>
/// to create an IAsyncNodeManager compatible object
/// </summary>
public static IAsyncNodeManager ToAsyncNodeManager(this INodeManager nodeManager)
{
if (nodeManager is IAsyncNodeManager asyncNodeManager)
{
return asyncNodeManager;
}
return new AsyncNodeManagerAdapter(nodeManager);
}
}
/// <summary>
/// An adapter that makes a synchronous INodeManager conform to the IAsyncNodeManager interface.
/// </summary>
/// <remarks>
/// This allows synchronous, or only partially asynchronous node managers to be treated as asynchronous, which can help
/// unify the calling logic within the MasterNodeManager.
/// </remarks>
public class AsyncNodeManagerAdapter : IAsyncNodeManager, IDisposable
{
/// <summary>
/// Initializes a new instance of the <see cref="AsyncNodeManagerAdapter"/> class.
/// </summary>
/// <param name="nodeManager">The synchronous node manager to wrap.</param>
/// <exception cref="ArgumentNullException"><paramref name="nodeManager"/> is <c>null</c>.</exception>
public AsyncNodeManagerAdapter(INodeManager nodeManager)
{
SyncNodeManager = nodeManager ?? throw new ArgumentNullException(nameof(nodeManager));
}
/// <inheritdoc/>
public IEnumerable<string> NamespaceUris => SyncNodeManager.NamespaceUris;
/// <inheritdoc/>
public INodeManager SyncNodeManager { get; }
/// <inheritdoc/>
public ValueTask AddReferencesAsync(
IDictionary<NodeId, IList<IReference>> references,
CancellationToken cancellationToken = default)
{
if (SyncNodeManager is IAsyncNodeManager asyncNodeManager)
{
return asyncNodeManager.AddReferencesAsync(references, cancellationToken);
}
SyncNodeManager.AddReferences(references);
// Return a completed ValueTask since the underlying call is synchronous.
return default;
}
/// <inheritdoc/>
public ValueTask<ContinuationPoint?> BrowseAsync(
OperationContext context,
ContinuationPoint continuationPoint,
IList<ReferenceDescription> references,
CancellationToken cancellationToken = default)
{
if (SyncNodeManager is IBrowseAsyncNodeManager asyncNodeManager)
{
return asyncNodeManager.BrowseAsync(context, continuationPoint, references, cancellationToken);
}
SyncNodeManager.Browse(context, ref continuationPoint, references);
// Return a completed ValueTask since the underlying call is synchronous.
return new ValueTask<ContinuationPoint?>(continuationPoint);
}
/// <inheritdoc/>
public ValueTask CallAsync(
OperationContext context,
ArrayOf<CallMethodRequest> methodsToCall,
IList<CallMethodResult> results,
IList<ServiceResult> errors,
CancellationToken cancellationToken = default)
{
if (SyncNodeManager is ICallAsyncNodeManager asyncNodeManager)
{
return asyncNodeManager.CallAsync(context, methodsToCall, results, errors, cancellationToken);
}
SyncNodeManager.Call(context, methodsToCall, results, errors);
// Return a completed ValueTask since the underlying call is synchronous.
return default;
}
/// <inheritdoc/>
public ValueTask<MethodState> FindMethodStateAsync(
OperationContext context,
CallMethodRequest methodToCall,
CancellationToken cancellationToken = default)
{
if (SyncNodeManager is INodeManager3 nodeManager)
{
MethodState method = nodeManager.FindMethodState(context, methodToCall);
return new ValueTask<MethodState>(method);
}
return new ValueTask<MethodState>(result: null!);
}
/// <inheritdoc/>
public ValueTask<ServiceResult> ConditionRefreshAsync(
OperationContext context,
IList<IEventMonitoredItem> monitoredItems,
CancellationToken cancellationToken = default)
{
if (SyncNodeManager is IConditionRefreshAsyncNodeManager asyncNodeManager)
{
return asyncNodeManager.ConditionRefreshAsync(context, monitoredItems, cancellationToken);
}
SyncNodeManager.ConditionRefresh(context, monitoredItems);
// Return a completed ValueTask since the underlying call is synchronous.
return default;
}
/// <inheritdoc/>
public ValueTask CreateAddressSpaceAsync(
IDictionary<NodeId, IList<IReference>> externalReferences,
CancellationToken cancellationToken = default)
{
if (SyncNodeManager is IAsyncNodeManager asyncNodeManager)
{
return asyncNodeManager.CreateAddressSpaceAsync(externalReferences, cancellationToken);
}
SyncNodeManager.CreateAddressSpace(externalReferences);
// Return a completed ValueTask since the underlying call is synchronous.
return default;
}
/// <inheritdoc/>
public 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)
{
if (SyncNodeManager is ICreateMonitoredItemsAsyncNodeManager asyncNodeManager)
{
return asyncNodeManager.CreateMonitoredItemsAsync(
context,
subscriptionId,
publishingInterval,
timestampsToReturn,
itemsToCreate,
errors,
filterErrors,
monitoredItems,
createDurable,
monitoredItemIdFactory,
cancellationToken);
}
SyncNodeManager.CreateMonitoredItems(
context,
subscriptionId,
publishingInterval,
timestampsToReturn,
itemsToCreate,
errors,
filterErrors,
monitoredItems,
createDurable,
monitoredItemIdFactory);
// Return a completed ValueTask since the underlying call is synchronous.
return default;
}
/// <inheritdoc/>
public ValueTask DeleteAddressSpaceAsync(CancellationToken cancellationToken = default)
{
if (SyncNodeManager is IAsyncNodeManager asyncNodeManager)
{
return asyncNodeManager.DeleteAddressSpaceAsync(cancellationToken);
}
SyncNodeManager.DeleteAddressSpace();
// Return a completed ValueTask since the underlying call is synchronous.
return default;
}
/// <inheritdoc/>
public ValueTask DeleteMonitoredItemsAsync(
OperationContext context,
IList<IMonitoredItem> monitoredItems,
IList<bool> processedItems,
IList<ServiceResult> errors,
CancellationToken cancellationToken = default)
{
if (SyncNodeManager is IDeleteMonitoredItemsAsyncNodeManager asyncNodeManager)
{
return asyncNodeManager.DeleteMonitoredItemsAsync(
context,
monitoredItems,
processedItems,
errors,
cancellationToken);
}
SyncNodeManager.DeleteMonitoredItems(context, monitoredItems, processedItems, errors);
// Return a completed ValueTask since the underlying call is synchronous.
return default;
}
/// <inheritdoc/>
public ValueTask<ServiceResult> DeleteReferenceAsync(
object sourceHandle,
NodeId referenceTypeId,
bool isInverse,
ExpandedNodeId targetId,
bool deleteBidirectional,
CancellationToken cancellationToken = default)
{
if (SyncNodeManager is IAsyncNodeManager asyncNodeManager)
{
return asyncNodeManager.DeleteReferenceAsync(sourceHandle,
referenceTypeId,
isInverse,
targetId,
deleteBidirectional,
cancellationToken);
}
ServiceResult result = SyncNodeManager.DeleteReference(sourceHandle,
referenceTypeId,
isInverse,
targetId,
deleteBidirectional);
// Return a completed ValueTask since the underlying call is synchronous.
return new ValueTask<ServiceResult>(result);
}
/// <inheritdoc/>
public ValueTask<object> GetManagerHandleAsync(NodeId nodeId, CancellationToken cancellationToken = default)
{
if (SyncNodeManager is IAsyncNodeManager asyncNodeManager)
{
return asyncNodeManager.GetManagerHandleAsync(nodeId, cancellationToken);
}
object? handle = SyncNodeManager.GetManagerHandle(nodeId);
// Return a completed ValueTask since the underlying call is synchronous.
return new ValueTask<object>(handle!);
}
/// <inheritdoc/>
public ValueTask<NodeMetadata> GetNodeMetadataAsync(
OperationContext context,
object targetHandle,
BrowseResultMask resultMask,
CancellationToken cancellationToken = default)
{
if (SyncNodeManager is IAsyncNodeManager asyncNodeManager)
{
return asyncNodeManager.GetNodeMetadataAsync(context, targetHandle, resultMask, cancellationToken);
}
NodeMetadata? nodeMetadata = SyncNodeManager.GetNodeMetadata(context, targetHandle, resultMask);
// Return a completed ValueTask since the underlying call is synchronous.
return new ValueTask<NodeMetadata>(nodeMetadata!);
}
/// <inheritdoc/>
public ValueTask<NodeMetadata?> GetPermissionMetadataAsync(
OperationContext context,
object targetHandle,
BrowseResultMask resultMask,
Dictionary<NodeId, Variant[]> uniqueNodesServiceAttributesCache,
bool permissionsOnly,
CancellationToken cancellationToken = default)
{
if (SyncNodeManager is IAsyncNodeManager asyncNodeManager)
{
return asyncNodeManager.GetPermissionMetadataAsync(
context,
targetHandle,
resultMask,
uniqueNodesServiceAttributesCache,
permissionsOnly,
cancellationToken);
}
if (SyncNodeManager is INodeManager2 nodeManager2)
{
NodeMetadata? nodeMetadata = nodeManager2.GetPermissionMetadata(
context,
targetHandle,
resultMask,
uniqueNodesServiceAttributesCache,
permissionsOnly);
return new ValueTask<NodeMetadata?>(nodeMetadata);
}
return new ValueTask<NodeMetadata?>((NodeMetadata?)null);
}
/// <inheritdoc/>
public ValueTask HistoryReadAsync(
OperationContext context,
HistoryReadDetails details,
TimestampsToReturn timestampsToReturn,
bool releaseContinuationPoints,
ArrayOf<HistoryReadValueId> nodesToRead,
IList<HistoryReadResult> results,
IList<ServiceResult> errors,
CancellationToken cancellationToken = default)
{
if (SyncNodeManager is IHistoryReadAsyncNodeManager asyncNodeManager)
{
return asyncNodeManager.HistoryReadAsync(
context,
details,
timestampsToReturn,
releaseContinuationPoints,
nodesToRead,
results,
errors,
cancellationToken);
}
SyncNodeManager.HistoryRead(
context,
details,
timestampsToReturn,
releaseContinuationPoints,
nodesToRead,
results,
errors);
// Return a completed ValueTask since the underlying call is synchronous.
return default;
}
/// <inheritdoc/>
public ValueTask HistoryUpdateAsync(
OperationContext context,
Type detailsType,
ArrayOf<HistoryUpdateDetails> nodesToUpdate,
IList<HistoryUpdateResult> results,
IList<ServiceResult> errors,
CancellationToken cancellationToken = default)
{
if (SyncNodeManager is IHistoryUpdateAsyncNodeManager asyncNodeManager)
{
return asyncNodeManager.HistoryUpdateAsync(
context,
detailsType,
nodesToUpdate,
results,
errors,
cancellationToken);
}
SyncNodeManager.HistoryUpdate(
context,
detailsType,
nodesToUpdate,
results,
errors);
// Return a completed ValueTask since the underlying call is synchronous.
return default;
}
/// <inheritdoc/>
public ValueTask<bool> IsNodeInViewAsync(
OperationContext context,
NodeId viewId,
object nodeHandle,
CancellationToken cancellationToken = default)
{
if (SyncNodeManager is IAsyncNodeManager asyncNodeManager)
{
return asyncNodeManager.IsNodeInViewAsync(context, viewId, nodeHandle, cancellationToken);
}
if (SyncNodeManager is INodeManager2 nodeManager2)
{
bool result = nodeManager2.IsNodeInView(context, viewId, nodeHandle);
return new ValueTask<bool>(result);
}
throw new NotImplementedException();
}
/// <inheritdoc/>
public ValueTask ModifyMonitoredItemsAsync(OperationContext context,
TimestampsToReturn timestampsToReturn,
IList<IMonitoredItem> monitoredItems,
ArrayOf<MonitoredItemModifyRequest> itemsToModify,
IList<ServiceResult> errors,
IList<MonitoringFilterResult> filterErrors,
CancellationToken cancellationToken = default)
{
if (SyncNodeManager is IModifyMonitoredItemsAsyncNodeManager asyncNodeManager)
{
return asyncNodeManager.ModifyMonitoredItemsAsync(
context,
timestampsToReturn,
monitoredItems,
itemsToModify,
errors,
filterErrors,
cancellationToken);
}
SyncNodeManager.ModifyMonitoredItems(
context,
timestampsToReturn,
monitoredItems,
itemsToModify,
errors,
filterErrors);
// Return a completed ValueTask since the underlying call is synchronous.
return default;
}
/// <inheritdoc/>
public ValueTask ReadAsync(OperationContext context,
double maxAge,
ArrayOf<ReadValueId> nodesToRead,
IList<DataValue> values,
IList<ServiceResult> errors,
CancellationToken cancellationToken = default)
{
if (SyncNodeManager is IReadAsyncNodeManager asyncNodeManager)
{
return asyncNodeManager.ReadAsync(
context,
maxAge,
nodesToRead,
values,
errors,
cancellationToken);
}
SyncNodeManager.Read(context, maxAge, nodesToRead, values, errors);
// Return a completed ValueTask since the underlying call is synchronous.
return default;
}
/// <inheritdoc/>
public ValueTask RestoreMonitoredItemsAsync(
IList<IStoredMonitoredItem> itemsToRestore,
IList<IMonitoredItem> monitoredItems,
IUserIdentity savedOwnerIdentity,
CancellationToken cancellationToken = default)
{
if (SyncNodeManager is IAsyncNodeManager asyncNodeManager)
{
return asyncNodeManager.RestoreMonitoredItemsAsync(itemsToRestore, monitoredItems, savedOwnerIdentity, cancellationToken);
}
SyncNodeManager.RestoreMonitoredItems(itemsToRestore, monitoredItems, savedOwnerIdentity);
// Return a completed ValueTask since the underlying call is synchronous.
return default;
}
/// <inheritdoc/>
public ValueTask SessionClosingAsync(
OperationContext context,
NodeId sessionId,
bool deleteSubscriptions,
CancellationToken cancellationToken = default)
{
if (SyncNodeManager is IAsyncNodeManager asyncNodeManager)
{
return asyncNodeManager.SessionClosingAsync(context, sessionId, deleteSubscriptions, cancellationToken);
}
if (SyncNodeManager is INodeManager2 nodeManager2)
{
nodeManager2.SessionClosing(context, sessionId, deleteSubscriptions);
}
// Return a completed ValueTask since the underlying call is synchronous.
return default;
}
/// <inheritdoc/>
public ValueTask SessionActivatedAsync(
OperationContext context,
NodeId sessionId,
CancellationToken cancellationToken = default)
{
if (SyncNodeManager is IAsyncNodeManager asyncNodeManager)
{
return asyncNodeManager.SessionActivatedAsync(context, sessionId, cancellationToken);
}
if (SyncNodeManager is INodeManager3 nodeManager3)
{
nodeManager3.SessionActivated(context, sessionId);
}
// Return a completed ValueTask since the underlying call is synchronous.
return default;
}
/// <inheritdoc/>
public ValueTask SetMonitoringModeAsync(
OperationContext context,
MonitoringMode monitoringMode,
IList<IMonitoredItem> monitoredItems,
IList<bool> processedItems,
IList<ServiceResult> errors,
CancellationToken cancellationToken = default)
{
if (SyncNodeManager is ISetMonitoringModeAsyncNodeManager asyncNodeManager)
{
return asyncNodeManager.SetMonitoringModeAsync(
context,
monitoringMode,
monitoredItems,
processedItems,
errors,
cancellationToken);
}
SyncNodeManager.SetMonitoringMode(
context,
monitoringMode,
monitoredItems,
processedItems,
errors);
// Return a completed ValueTask since the underlying call is synchronous.
return default;
}
/// <inheritdoc/>
public ValueTask<ServiceResult> SubscribeToAllEventsAsync(
OperationContext context,
uint subscriptionId,
IEventMonitoredItem monitoredItem,
bool unsubscribe,
CancellationToken cancellationToken = default)
{
if (SyncNodeManager is IAsyncNodeManager asyncNodeManager)
{
return asyncNodeManager.SubscribeToAllEventsAsync(
context,
subscriptionId,
monitoredItem,
unsubscribe,
cancellationToken);
}
ServiceResult result = SyncNodeManager.SubscribeToAllEvents(
context,
subscriptionId,
monitoredItem,
unsubscribe);
// Return a completed ValueTask since the underlying call is synchronous.
return new ValueTask<ServiceResult>(result);
}
/// <inheritdoc/>
public ValueTask<ServiceResult> SubscribeToEventsAsync(
OperationContext context,
object sourceId,
uint subscriptionId,
IEventMonitoredItem monitoredItem,
bool unsubscribe,
CancellationToken cancellationToken = default)
{
if (SyncNodeManager is IAsyncNodeManager asyncNodeManager)
{
return asyncNodeManager.SubscribeToEventsAsync(
context,
sourceId,
subscriptionId,
monitoredItem,
unsubscribe,
cancellationToken);
}
ServiceResult result = SyncNodeManager.SubscribeToEvents(
context,
sourceId,
subscriptionId,
monitoredItem,
unsubscribe);
// Return a completed ValueTask since the underlying call is synchronous.
return new ValueTask<ServiceResult>(result);
}
/// <inheritdoc/>
public ValueTask TransferMonitoredItemsAsync(
OperationContext context,
bool sendInitialValues,
IList<IMonitoredItem> monitoredItems,
IList<bool> processedItems,
IList<ServiceResult> errors,
CancellationToken cancellationToken = default)
{
if (SyncNodeManager is ITransferMonitoredItemsAsyncNodeManager asyncNodeManager)
{
return asyncNodeManager.TransferMonitoredItemsAsync(
context,
sendInitialValues,
monitoredItems,
processedItems,
errors,
cancellationToken);
}
SyncNodeManager.TransferMonitoredItems(
context,
sendInitialValues,
monitoredItems,
processedItems,
errors);
// Return a completed ValueTask since the underlying call is synchronous.
return default;
}
/// <inheritdoc/>
public ValueTask TranslateBrowsePathAsync(
OperationContext context,
object sourceHandle,
RelativePathElement relativePath,
IList<ExpandedNodeId> targetIds,
IList<NodeId> unresolvedTargetIds,
CancellationToken cancellationToken = default)
{
if (SyncNodeManager is ITranslateBrowsePathAsyncNodeManager asyncNodeManager)
{
return asyncNodeManager.TranslateBrowsePathAsync(context, sourceHandle, relativePath, targetIds, unresolvedTargetIds, cancellationToken);
}
SyncNodeManager.TranslateBrowsePath(context, sourceHandle, relativePath, targetIds, unresolvedTargetIds);
// Return a completed ValueTask since the underlying call is synchronous.
return default;
}
/// <inheritdoc/>
public ValueTask WriteAsync(
OperationContext context,
ArrayOf<WriteValue> nodesToWrite,
IList<ServiceResult> errors,
CancellationToken cancellationToken = default)
{
if (SyncNodeManager is IWriteAsyncNodeManager asyncNodeManager)
{
return asyncNodeManager.WriteAsync(context, nodesToWrite, errors, cancellationToken);
}
SyncNodeManager.Write(context, nodesToWrite, errors);
// Return a completed ValueTask since the underlying call is synchronous.
return default;
}
/// <inheritdoc/>
public ValueTask<ServiceResult> ValidateEventRolePermissionsAsync(
IEventMonitoredItem monitoredItem,
IFilterTarget filterTarget,
CancellationToken cancellationToken = default)
{
if (SyncNodeManager is IAsyncNodeManager asyncNodeManager)
{
return asyncNodeManager.ValidateEventRolePermissionsAsync(monitoredItem, filterTarget, cancellationToken);
}
if (SyncNodeManager is INodeManager3 nodeManager2)
{
return new ValueTask<ServiceResult>(nodeManager2.ValidateEventRolePermissions(monitoredItem, filterTarget));
}
return new ValueTask<ServiceResult>(ServiceResult.Good);
}
/// <inheritdoc/>
public ValueTask<ServiceResult> ValidateRolePermissionsAsync(
OperationContext operationContext,
NodeId nodeId,
PermissionType requestedPermission,
CancellationToken cancellationToken = default)
{
if (SyncNodeManager is IAsyncNodeManager asyncNodeManager)
{
return asyncNodeManager.ValidateRolePermissionsAsync(operationContext, nodeId, requestedPermission, cancellationToken);
}
if (SyncNodeManager is INodeManager3 nodeManager2)
{
return new ValueTask<ServiceResult>(nodeManager2.ValidateRolePermissions(operationContext, nodeId, requestedPermission));
}
return new ValueTask<ServiceResult>(ServiceResult.Good);
}
/// <inheritdoc/>
public bool IsMultipleEventConsumerNode(NodeId nodeId)
{
return false;
}
/// <inheritdoc/>
/// <remarks>
/// The legacy synchronous <see cref="INodeManager"/> contract does not
/// expose NodeManagement service hooks. Adapted managers therefore
/// opt out by returning <c>false</c>; the master dispatcher rejects
/// per-item requests with <see cref="StatusCodes.BadUserAccessDenied"/>.
/// </remarks>
public bool AllowNodeManagement
=> SyncNodeManager is INodeManagementAsyncNodeManager nm && nm.AllowNodeManagement;
/// <inheritdoc/>
public ValueTask<(ServiceResult result, NodeId addedNodeId)> AddNodeAsync(
OperationContext context,
AddNodesItem item,
CancellationToken cancellationToken = default)
{
if (SyncNodeManager is INodeManagementAsyncNodeManager nm)
{
return nm.AddNodeAsync(context, item, cancellationToken);
}
return new ValueTask<(ServiceResult, NodeId)>(
(new ServiceResult(StatusCodes.BadServiceUnsupported), NodeId.Null));
}
/// <inheritdoc/>
public ValueTask<ServiceResult> DeleteNodeAsync(
OperationContext context,
DeleteNodesItem item,
CancellationToken cancellationToken = default)
{
if (SyncNodeManager is INodeManagementAsyncNodeManager nm)
{
return nm.DeleteNodeAsync(context, item, cancellationToken);
}
return new ValueTask<ServiceResult>(new ServiceResult(StatusCodes.BadServiceUnsupported));
}
/// <inheritdoc/>
public ValueTask<ServiceResult> AddReferenceAsync(
OperationContext context,
AddReferencesItem item,
CancellationToken cancellationToken = default)
{
if (SyncNodeManager is INodeManagementAsyncNodeManager nm)
{
return nm.AddReferenceAsync(context, item, cancellationToken);
}
return new ValueTask<ServiceResult>(new ServiceResult(StatusCodes.BadServiceUnsupported));
}
/// <inheritdoc/>
public ValueTask<ServiceResult> DeleteReferenceAsync(
OperationContext context,
DeleteReferencesItem item,
CancellationToken cancellationToken = default)
{
if (SyncNodeManager is INodeManagementAsyncNodeManager nm)
{
return nm.DeleteReferenceAsync(context, item, cancellationToken);
}
return new ValueTask<ServiceResult>(new ServiceResult(StatusCodes.BadServiceUnsupported));
}
/// <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)
{
(SyncNodeManager as IDisposable)?.Dispose();
}
}
}
}