mirror of
https://github.com/OPCFoundation/UA-.NETStandard.git
synced 2026-10-18 22:55:08 +08:00
15d0ce7bd5
# Description Implement OPC UA Part 10 Programs across `Opc.Ua.Core`/`Core.Types`, `Opc.Ua.Server`, and `Opc.Ua.Client`, reusing the existing state-machine, source-generation, node-management, auditing, and streaming-subscription infrastructure. - Add asynchronous provider-backed execution, standard controls, internal/autonomous transitions, generation checks, and nested-state activation/resumption. - Add checked creation/adoption, vendor Create adapters, deletion, recycling and instance limits, auto-delete, DI, and direct-construction paths. - Maintain ProgramDiagnostic2 and retained final results; publish captured intermediate results and concrete transition/audit events with access controls. - Add generated SDK models/proxies, typed client discovery and reads, event streams, and subscribe-before-call readiness handling. - Add unit, connected client/server, generated Annex A-style model, and NativeAOT coverage. Document usage and migration in `docs/Programs.md` and related guides. ### Compatibility and design decisions The linked Part 10 document is v1.05.00; namespace-zero definitions remain aligned with 1.05.07. Documentation distinguishes normative requirements from conflicting specification text and SDK conventions. - Correct state numbers to Halted=11, Ready=12, Running=13, Suspended=14. Generated subtypes retain inherited namespace-zero state/transition identifiers. - Managed Reset is Halted-to-Ready only. Preserve the existing low-level Reset-from-Suspended behavior for compatibility. - Preserve UInt32 limit types; use `UInt32.MaxValue` as a documented SDK unlimited convention. RecycleCount overflow reports BadOutOfRange without stopping unlimited execution. - In-memory default with injectable providers. Durable recovery, distributed execution/failover, and a production Annex A download engine are excluded. ## 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. - [ ] 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. Tests and focused coverage evidence are above; the combined tests/coverage checkbox remains open because a whole-repository coverage increase has not been established. CLA status, CI/CodeQL results, tracking-issue approval, and PR feedback are not asserted.
227 lines
12 KiB
Markdown
227 lines
12 KiB
Markdown
# NodeManagement service set
|
|
|
|
The OPC UA NodeManagement service set (OPC 10000-4 §5.8) gives clients
|
|
the ability to manipulate the server's address space at runtime:
|
|
|
|
| Service | Description |
|
|
| --- | --- |
|
|
| `AddNodes` | Create a new node beneath an existing parent (Object, Variable, ...). |
|
|
| `DeleteNodes` | Remove a node and, optionally, all references that target it. |
|
|
| `AddReferences` | Add a reference between two existing nodes. |
|
|
| `DeleteReferences` | Remove a reference between two existing nodes. |
|
|
|
|
The SDK ships full request validation, per-item dispatch, audit-event
|
|
emission, and per-NodeManager opt-in. NodeManagers do not need to
|
|
override the four `StandardServer` service methods themselves.
|
|
|
|
## Contents
|
|
|
|
- [Architecture](#architecture)
|
|
- [Opting in: `INodeManagementAsyncNodeManager`](#opting-in-inodemanagementasyncnodemanager)
|
|
- [Dispatch and routing rules](#dispatch-and-routing-rules)
|
|
- [Request handling on `AsyncCustomNodeManager`](#request-handling-on-asynccustomnodemanager)
|
|
- [Error codes returned](#error-codes-returned)
|
|
- [Audit events](#audit-events)
|
|
- [Client-side example](#client-side-example)
|
|
- [Custom behavior](#custom-behavior)
|
|
- [See also](#see-also)
|
|
|
|
## Architecture
|
|
|
|
1. The client request lands on `StandardServer.AddNodesAsync` /
|
|
`DeleteNodesAsync` / `AddReferencesAsync` / `DeleteReferencesAsync`.
|
|
2. Each service override validates the request and forwards the
|
|
per-item collection to the matching
|
|
`IMasterNodeManager.AddNodesAsync` etc. dispatcher.
|
|
3. For each item, `MasterNodeManager` selects the owning NodeManager:
|
|
- `AddNodes`: the requested NodeId namespace, or the BrowseName
|
|
namespace if the server assigns the NodeId.
|
|
- `DeleteNodes`: the node's owning NodeManager.
|
|
- Reference changes: the source node's owning NodeManager.
|
|
|
|
It validates the matching `PermissionType`, then asks the optional
|
|
`INodeManagementAsyncNodeManager` interface to perform the operation.
|
|
`AddNodes` requires `AddNode` in the target namespace's default policy
|
|
and `AddReference` on the parent. The server evaluates each Node
|
|
permission together with the Node's applicable `AccessRestrictions`.
|
|
4. The aggregated per-item results are wrapped in the matching response
|
|
envelope and the audit event (`AuditAddNodesEvent`,
|
|
`AuditDeleteNodesEvent`, `AuditAddReferencesEvent`,
|
|
`AuditDeleteReferencesEvent`) is emitted with an aggregated status.
|
|
|
|
NodeManagers that do not implement `INodeManagementAsyncNodeManager` —
|
|
or have not opted in — return `BadUserAccessDenied`, which is the
|
|
status defined for "server does not allow this operation". This keeps
|
|
the default behavior of every existing NodeManager unchanged.
|
|
|
|
## Opting in: `INodeManagementAsyncNodeManager`
|
|
|
|
```csharp
|
|
public interface INodeManagementAsyncNodeManager
|
|
{
|
|
// Sub-class returns true to opt in. Default is false.
|
|
bool AllowNodeManagement { get; }
|
|
|
|
ValueTask<(ServiceResult result, NodeId addedNodeId)> AddNodeAsync(
|
|
OperationContext context,
|
|
AddNodesItem item,
|
|
CancellationToken cancellationToken = default);
|
|
|
|
ValueTask<ServiceResult> DeleteNodeAsync(
|
|
OperationContext context,
|
|
DeleteNodesItem item,
|
|
CancellationToken cancellationToken = default);
|
|
|
|
ValueTask<ServiceResult> AddReferenceAsync(
|
|
OperationContext context,
|
|
AddReferencesItem item,
|
|
CancellationToken cancellationToken = default);
|
|
|
|
ValueTask<ServiceResult> DeleteReferenceAsync(
|
|
OperationContext context,
|
|
DeleteReferencesItem item,
|
|
CancellationToken cancellationToken = default);
|
|
}
|
|
```
|
|
|
|
`AsyncCustomNodeManager` already implements
|
|
`INodeManagementAsyncNodeManager` with default behavior that handles
|
|
every common case. Sub-classes opt in by overriding a single property:
|
|
|
|
```csharp
|
|
public sealed class MyNodeManager : AsyncCustomNodeManager
|
|
{
|
|
public MyNodeManager(IServerInternal server, ApplicationConfiguration config, ILogger logger)
|
|
: base(server, config, logger, "http://example.org/MyNodes/")
|
|
{
|
|
}
|
|
|
|
public override bool AllowNodeManagement => true;
|
|
}
|
|
```
|
|
|
|
That is the only change needed for a NodeManager to start accepting
|
|
AddNodes / DeleteNodes / AddReferences / DeleteReferences requests.
|
|
|
|
The reference server's `ReferenceNodeManager` opts in this way; you can
|
|
use it as the working example.
|
|
|
|
The [Part 10 Program host](Programs.md) deliberately restricts this opt-in.
|
|
Only explicitly enabled registered Program types can be added; deletion must
|
|
target a permitted Halted invocation, not its children or containing folder.
|
|
NodeManagement cannot bypass the Program provider, capacity reservations or
|
|
lifetime policy. The type's Create method is the primary client creation path.
|
|
|
|
## Dispatch and routing rules
|
|
|
|
- **AddNodes**: routed by the namespace of `RequestedNewNodeId` when
|
|
the caller supplies one. When `RequestedNewNodeId` is null, routed
|
|
to the parent's owning NodeManager. As a pragmatic fallback for the
|
|
common case where the parent is `ObjectsFolder` (owned by the
|
|
read-only `CoreNodeManager`), the dispatcher routes to the first
|
|
NodeManager that has opted in to NodeManagement.
|
|
- **DeleteNodes**: routed to the owning NodeManager of the target node.
|
|
- **AddReferences / DeleteReferences**: routed to the source node's
|
|
owning NodeManager. Target-side validation and inverse mutation occur
|
|
only when the target is explicitly local: `targetServerUri` is empty,
|
|
and the `ExpandedNodeId` has neither a server index nor namespace URI.
|
|
When that target lives in a different opted-in NodeManager, the
|
|
dispatcher resolves the actual source and target `NodeClass`, checks
|
|
both local endpoints before mutation, and mirrors the complementary
|
|
edge. If the inverse mutation fails or is cancelled, the source
|
|
mutation is compensated with an independent bounded cleanup token
|
|
before the original failure is returned or rethrown.
|
|
|
|
## Request handling on `AsyncCustomNodeManager`
|
|
|
|
The default implementation honors the following request fields:
|
|
|
|
| Field | Behavior |
|
|
| --- | --- |
|
|
| `RequestedNewNodeId` | Used as the new node's `NodeId` when valid; otherwise `BadNodeIdRejected`. When null, a fresh `NodeId` is allocated via `INodeIdFactory.New`; if the override returns a null `NodeId` (for example, because it derives identifiers from a parent that is not yet attached) the SDK falls back to the base allocator so AddNodes always yields a usable identifier. `BadNodeIdExists` if it collides with an existing node. |
|
|
| `BrowseName` | Must be non-null. `BadBrowseNameDuplicated` when a sibling under the same parent already uses the same browse name — both local children and previously-added cross-NodeManager children attached to the same parent by this NodeManager are checked. |
|
|
| `ParentNodeId` | Used to attach the new child to the parent. Cross-NodeManager parents are supported — the forward `parent → child` edge is added via the master so the parent's NodeManager records it, and the inverse `child → parent` edge is added on the new node. |
|
|
| `ReferenceTypeId` | Must be a `HierarchicalReferences` subtype. |
|
|
| `NodeAttributes` | Optional `VariableAttributes` or `ObjectAttributes` are applied to the new node. Supported fields include `DisplayName`, `Description`, `DataType`, `ValueRank`, `AccessLevel`, `UserAccessLevel`, `Historizing`, `MinimumSamplingInterval`, `Value`, and `EventNotifier`. |
|
|
| `DeleteTargetReferences` | When true, `DeleteNodes` also removes references on other NodeManagers that target the deleted node. |
|
|
| `DeleteBidirectional` | When true on `DeleteReferences`, the inverse edge is also deleted only for an explicitly local target. Remote targets remain source-side operations. |
|
|
|
|
If AddNodes fails after registration recovered previously detached monitored
|
|
items, rollback detaches those items again before removing the node. They retain
|
|
their deleted-node status and remain recoverable when a later AddNodes succeeds
|
|
with the same NodeId.
|
|
|
|
## Error codes returned
|
|
|
|
| Service | Status | Reason |
|
|
| --- | --- | --- |
|
|
| AddNodes | `BadBrowseNameInvalid` | `BrowseName` is null. |
|
|
| AddNodes | `BadParentNodeIdInvalid` | `ParentNodeId` is null or unknown. |
|
|
| AddNodes | `BadReferenceTypeIdInvalid` | `ReferenceTypeId` is null or unknown. |
|
|
| AddNodes | `BadReferenceNotAllowed` | `ReferenceTypeId` is not a hierarchical reference. |
|
|
| AddNodes | `BadNodeIdRejected` | The requested or BrowseName namespace has no owning NodeManager, or `RequestedNewNodeId` is outside the selected NodeManager's namespace. |
|
|
| AddNodes | `BadNodeIdExists` | `RequestedNewNodeId` already exists. |
|
|
| AddNodes | `BadBrowseNameDuplicated` | A sibling beneath the same local parent already uses the browse name. |
|
|
| AddNodes | `BadNodeClassInvalid` | Only `Object` and `Variable` are supported by the default implementation. |
|
|
| AddNodes | `BadNodeAttributesInvalid` | The supplied attributes extension object does not match the node class. |
|
|
| AddNodes / DeleteNodes / AddReferences / DeleteReferences | `BadUserAccessDenied` | The owning NodeManager has not opted in to NodeManagement or the Session lacks `AddNode`, `DeleteNode`, `AddReference`, or `RemoveReference` permission. |
|
|
| AddNodes / DeleteNodes / AddReferences / DeleteReferences | `BadSecurityModeInsufficient` | An applicable namespace, parent, source, or local target `AccessRestrictions` policy requires a stronger SecureChannel. |
|
|
| DeleteNodes | `BadNodeIdInvalid` / `BadNodeIdUnknown` | The node to delete is null or unknown. |
|
|
| AddReferences | `BadSourceNodeIdInvalid` | Source is null or unknown to this NodeManager. |
|
|
| AddReferences | `BadTargetNodeIdInvalid` | Target is null. |
|
|
| AddReferences | `BadDuplicateReferenceNotAllowed` | The exact reference (type + direction + target) already exists on the source. |
|
|
| DeleteReferences | `BadNoMatch` | No reference with the exact triple (type + direction + target) was found on the source. |
|
|
|
|
## Audit events
|
|
|
|
Every NodeManagement service emits its corresponding audit event:
|
|
|
|
- `AuditAddNodesEventType`
|
|
- `AuditDeleteNodesEventType`
|
|
- `AuditAddReferencesEventType`
|
|
- `AuditDeleteReferencesEventType`
|
|
|
|
The audit `Status` is the first bad per-item status, or `Good` when
|
|
every item succeeded. Audit events are emitted from both the success
|
|
and failure paths so an early `ServiceResultException` (request-level
|
|
validation) still appears in the audit log.
|
|
|
|
## Client-side example
|
|
|
|
The standard `Session` / `ManagedSession` AddNodes / DeleteNodes /
|
|
AddReferences / DeleteReferences APIs work unchanged against any
|
|
server whose NodeManager has opted in. See OPC 10000-4 §5.8 for the
|
|
service contract.
|
|
|
|
## Custom behavior
|
|
|
|
After opting in, override the matching
|
|
`INodeManagementAsyncNodeManager` method to customize a NodeManager. For
|
|
example, you can enforce a custom NodeId scheme, persist a new node, or veto
|
|
specific node classes. The following example restricts additions to
|
|
Object nodes:
|
|
|
|
```csharp
|
|
public override async ValueTask<(ServiceResult result, NodeId addedNodeId)> AddNodeAsync(
|
|
OperationContext context,
|
|
AddNodesItem item,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
if (item.NodeClass != NodeClass.Object)
|
|
{
|
|
return (new ServiceResult(StatusCodes.BadNodeClassInvalid), NodeId.Null);
|
|
}
|
|
return await base.AddNodeAsync(context, item, cancellationToken).ConfigureAwait(false);
|
|
}
|
|
```
|
|
|
|
## See also
|
|
|
|
- [Reference Server](../samples/README.md) — opt-in example in
|
|
`ReferenceNodeManager`.
|
|
- [Source generated NodeManagers](NodeManagers.md#source-generated-node-managers) —
|
|
combine NodeManagement with generated models.
|
|
- [Model Change Tracking](ModelChangeTracking.md) — `AddNodes` and
|
|
`DeleteNodes` emit `GeneralModelChangeEvent` automatically when
|
|
model change tracking is enabled.
|