mirror of
https://github.com/OPCFoundation/UA-.NETStandard.git
synced 2026-10-16 21:02:51 +08:00
Complete CreateInstanceOf node lifecycle during registration (#4360)
# Description Generated `CreateInstanceOf<Type>` factories build typed node graphs and assign per-instance NodeIds, but intentionally leave the node create lifecycle open. Hand-written callers commonly registered those graphs without calling `CreateAsPredefinedNode`, so `OnBeforeCreate`, `OnAfterCreate`, and change-mask clearing never ran. This change: - adds `NodeState.IsCreated` and makes lifecycle completion idempotent per node; - completes newly materialized children, including children created by `OnAfterCreate`; - bounds late-child lifecycle convergence and fails explicitly if callbacks keep adding uncreated nodes; - completes uncreated nodes automatically before predefined-node registration and behavior wiring in both async and legacy custom node managers; - assigns/rebases async-node-manager NodeIds before lifecycle callbacks; - emits a null-safe Debug diagnostic when registration performs completion; - documents the generated-factory lifecycle contract and migration behavior; - adds regressions for condition/alarm method binding, finite-state-machine namespaces, Fluent state-node rebasing, behavior replacement, cloning, deletion, late children, non-converging callbacks, and synchronous/async registration. The tracked `src/` and `samples/` call-site audit covered 2,953 C# files. No additional ordering-sensitive call sites require explicit completion beyond the three existing DI/Robotics sites. ## Validation - `dotnet build UA.slnx -c Release -p:CustomTestTarget=net10.0`: passed. - `Opc.Ua.Server.Tests` net10.0: 4,811 passed, 5 skipped. - `NodeStateTests` net10.0 and net48: 213 passed on each. - Node-manager lifecycle and null-logger tests net48: 459 passed. - Source-generation core tests net10.0 and net48: 3,802 passed, 8 skipped on each. - DI, Robotics, ISA95, Positioning, Vision, Vision.Intent, and XRegistry suites passed on net10.0 and net48. - Generated alarm lifecycle regression passed on net10.0 and net48. The solution build reports six pre-existing CA warnings in untouched files. The OpenUSD suite has one pre-existing locale-sensitive failure in `StructuredCartesianCoordinatesAreAccepted` (`1.5` expected versus `1,5` under `en-DE`); all other OpenUSD tests passed on both tested frameworks. ## Related Issues - Fixes #4320 ## 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. - [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. - [x] I have addressed **all** PR feedback received. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 7ffdc400-0710-4a2e-a9d1-c0e1a9b3b81e
This commit is contained in:
@@ -113,6 +113,47 @@ protected override void OnAfterCreate(ISystemContext context, NodeState node, Ca
|
||||
}
|
||||
```
|
||||
|
||||
#### Generated instance factories and the create lifecycle
|
||||
|
||||
Source-generated `CreateInstanceOf<Type>` factories materialise the typed node
|
||||
graph and assign per-instance NodeIds, but intentionally leave the create
|
||||
lifecycle open so callers can finish assembling and configuring the subtree.
|
||||
In 2.0, registering the graph through `AddPredefinedNodeAsync`,
|
||||
`AddNodeAsync`, or the synchronous predefined-node registration path
|
||||
automatically runs `OnBeforeCreate`/`OnAfterCreate` and clears change masks
|
||||
before indexing. The asynchronous paths assign or rebase NodeIds before the
|
||||
callbacks, so they see the identifiers which enter the address space.
|
||||
Synchronous predefined-node registration preserves the caller-assigned
|
||||
NodeIds and expects them to be ready. `NodeState.IsCreated` exposes whether
|
||||
an individual node has completed that lifecycle.
|
||||
|
||||
`AddBehaviourToPredefinedNode` and its asynchronous equivalent receive the
|
||||
created node. An override which replaces a passive node may therefore observe
|
||||
the passive node's lifecycle before returning the active replacement; the
|
||||
replacement is also completed before indexing. Keep external lifecycle side
|
||||
effects idempotent when using that legacy replacement pattern.
|
||||
|
||||
If pre-registration code depends on values established by `OnAfterCreate`, or
|
||||
sets state or handlers which an `OnAfterCreate` override would replace, call
|
||||
`CreateAsPredefinedNode` first:
|
||||
|
||||
```csharp
|
||||
MyMachineState machine =
|
||||
context.CreateInstanceOfMyMachineType(parent, browseName);
|
||||
|
||||
machine.CreateAsPredefinedNode(context);
|
||||
SeedInitialState(machine);
|
||||
|
||||
await AddPredefinedNodeAsync(context, machine, cancellationToken);
|
||||
```
|
||||
|
||||
`CreateAsPredefinedNode` is idempotent per node and still completes newly
|
||||
added children. It does not re-run `OnAfterCreate` on an ancestor which was
|
||||
already created, so add children whose handlers are wired by a parent callback
|
||||
before the parent's first completion, or wire those late children explicitly.
|
||||
This prevents duplicate callback execution when an explicitly completed
|
||||
subtree is later registered.
|
||||
|
||||
#### NodeState FindChild and CreateChild state NodeId assignment
|
||||
|
||||
`NodeState.FindChild` and `NodeState.CreateChild` now take
|
||||
|
||||
@@ -217,8 +217,9 @@ ITopologyElementBuilder<TElement> TopologyElementByBrowseName<TElement>(
|
||||
sets BrowseName/SymbolicName/DisplayName and stamps the
|
||||
`TypeDefinitionId`.
|
||||
5. Calls the real `AsyncCustomNodeManager.AddPredefinedNodeAsync` so
|
||||
subscription wiring, type-tree registration, and root-notifier
|
||||
propagation all happen exactly as for nodes loaded from a NodeSet2.
|
||||
the node create lifecycle, subscription wiring, type-tree registration,
|
||||
and root-notifier propagation all happen exactly as for nodes loaded from
|
||||
a NodeSet2.
|
||||
|
||||
Machinery-aware subclasses can additionally reference managed machines
|
||||
from the OPC 40001 `Machines` entry point. `DiNodeManager`'s helper adds
|
||||
|
||||
@@ -1826,6 +1826,25 @@ pump.CreateChild(SystemContext, someBrowseName); // CreateOrReplace<Child
|
||||
await AddPredefinedNodeAsync(SystemContext, pump, cancellationToken);
|
||||
```
|
||||
|
||||
The generated factory intentionally returns a graph whose create lifecycle
|
||||
has not completed. Node manager registration completes
|
||||
`OnBeforeCreate`/`OnAfterCreate` and clears change masks before the graph is
|
||||
indexed. The asynchronous registration path performs any required NodeId
|
||||
rebasing first, so lifecycle callbacks see the identifiers which enter the
|
||||
address space. Synchronous predefined-node registration preserves the
|
||||
caller-assigned NodeIds and expects them to be ready. The behavior hook then
|
||||
receives a created node. `NodeState.IsCreated` reports whether an individual
|
||||
node has completed that lifecycle.
|
||||
|
||||
Most callers should configure the graph and then register it as shown above.
|
||||
If code must read state established by `OnAfterCreate`, or write state or
|
||||
handlers which an `OnAfterCreate` override would replace, call
|
||||
`CreateAsPredefinedNode` after assembling the subtree and before that
|
||||
ordering-sensitive code. The call is idempotent per node and still completes
|
||||
children added since an earlier call. It does not re-run `OnAfterCreate` on
|
||||
ancestors which were already created; assemble parent-wired children before
|
||||
the parent's first completion, or wire those late children explicitly.
|
||||
|
||||
Notes:
|
||||
|
||||
* An explicit `browseName` is what marks a *dynamically materialised
|
||||
@@ -1839,6 +1858,10 @@ Notes:
|
||||
`assignInstanceNodeIds: false`: they build declaration subtrees whose
|
||||
NodeIds `CreateInstanceOf<Type>` rebases in a single pass afterwards.
|
||||
The same parameter is available to you if you need that behaviour.
|
||||
* `CreateInstanceOf<Type>` assigns identity but does not call
|
||||
`CreateAsPredefinedNode`. `AddPredefinedNodeAsync`, `AddNodeAsync`, and the
|
||||
synchronous predefined-node registration paths complete the lifecycle
|
||||
automatically.
|
||||
* Assignment only happens when the context carries an
|
||||
`ISystemContext.NodeIdFactory`. `AsyncCustomNodeManager` supplies one
|
||||
that allocates from the manager's namespace; override `New` to derive
|
||||
|
||||
+2
-1
@@ -301,7 +301,8 @@ Instances are always materialised through the generated
|
||||
`ISystemContext.CreateInstanceOf<Type>` factories (for example
|
||||
`CreateInstanceOfMotionDeviceSystemType`), so each instance carries the full
|
||||
companion-type structure and its per-instance NodeIds, rather than only a
|
||||
type-definition reference on a bare `BaseObjectState`.
|
||||
type-definition reference on a bare `BaseObjectState`. Registration completes
|
||||
the node create lifecycle before the subtree enters the address space.
|
||||
|
||||
### Binding live values
|
||||
|
||||
|
||||
@@ -113,6 +113,47 @@ protected override void OnAfterCreate(ISystemContext context, NodeState node, Ca
|
||||
}
|
||||
```
|
||||
|
||||
#### Generated instance factories and the create lifecycle
|
||||
|
||||
Source-generated `CreateInstanceOf<Type>` factories materialise the typed node
|
||||
graph and assign per-instance NodeIds, but intentionally leave the create
|
||||
lifecycle open so callers can finish assembling and configuring the subtree.
|
||||
In 2.0, registering the graph through `AddPredefinedNodeAsync`,
|
||||
`AddNodeAsync`, or the synchronous predefined-node registration path
|
||||
automatically runs `OnBeforeCreate`/`OnAfterCreate` and clears change masks
|
||||
before indexing. The asynchronous paths assign or rebase NodeIds before the
|
||||
callbacks, so they see the identifiers which enter the address space.
|
||||
Synchronous predefined-node registration preserves the caller-assigned
|
||||
NodeIds and expects them to be ready. `NodeState.IsCreated` exposes whether
|
||||
an individual node has completed that lifecycle.
|
||||
|
||||
`AddBehaviourToPredefinedNode` and its asynchronous equivalent receive the
|
||||
created node. An override which replaces a passive node may therefore observe
|
||||
the passive node's lifecycle before returning the active replacement; the
|
||||
replacement is also completed before indexing. Keep external lifecycle side
|
||||
effects idempotent when using that legacy replacement pattern.
|
||||
|
||||
If pre-registration code depends on values established by `OnAfterCreate`, or
|
||||
sets state or handlers which an `OnAfterCreate` override would replace, call
|
||||
`CreateAsPredefinedNode` first:
|
||||
|
||||
```csharp
|
||||
MyMachineState machine =
|
||||
context.CreateInstanceOfMyMachineType(parent, browseName);
|
||||
|
||||
machine.CreateAsPredefinedNode(context);
|
||||
SeedInitialState(machine);
|
||||
|
||||
await AddPredefinedNodeAsync(context, machine, cancellationToken);
|
||||
```
|
||||
|
||||
`CreateAsPredefinedNode` is idempotent per node and still completes newly
|
||||
added children. It does not re-run `OnAfterCreate` on an ancestor which was
|
||||
already created, so add children whose handlers are wired by a parent callback
|
||||
before the parent's first completion, or wire those late children explicitly.
|
||||
This prevents duplicate callback execution when an explicitly completed
|
||||
subtree is later registered.
|
||||
|
||||
#### NodeState FindChild and CreateChild state NodeId assignment
|
||||
|
||||
`NodeState.FindChild` and `NodeState.CreateChild` now take
|
||||
@@ -323,4 +364,3 @@ bool isType = await cache.IsTypeOfAsync(sub, super);
|
||||
- Related: [types.md](types.md), [alarms-model-change.md](alarms-model-change.md), [sessions-subscriptions.md](sessions-subscriptions.md).
|
||||
- [2.0 migration index](README.md) — analyzer quick-start + symptom → sub-doc table.
|
||||
- [Migration Guide](../../MigrationGuide.md) — landing page across versions.
|
||||
|
||||
|
||||
@@ -970,8 +970,12 @@ namespace Opc.Ua.Server
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a created instance and its children to the NodeManagers AddressSpace. Assigns NodeIds if needed and fixes ReferenceTargets after assigning NodeIds.
|
||||
/// Adds an instance and its children to the NodeManager address space.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Assigns NodeIds if needed, updates reference targets, and completes
|
||||
/// the create lifecycle before indexing the subtree.
|
||||
/// </remarks>
|
||||
/// <param name="context">The operation context.</param>
|
||||
/// <param name="parentId">An optional parent identifier.</param>
|
||||
/// <param name="instance">The instance to create.</param>
|
||||
@@ -1040,6 +1044,11 @@ namespace Opc.Ua.Server
|
||||
/// children are kept verbatim and used as PredefinedNodes keys.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Registration completes the create lifecycle for nodes which have
|
||||
/// not already passed through
|
||||
/// <see cref="NodeState.CreateAsPredefinedNode"/>.
|
||||
/// </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.
|
||||
@@ -2042,8 +2051,14 @@ namespace Opc.Ua.Server
|
||||
/// </summary>
|
||||
protected virtual async ValueTask AddPredefinedNodeAsync(ISystemContext context, NodeState node, CancellationToken cancellationToken = default)
|
||||
{
|
||||
PrepareInstanceNodeIdsForRegistration(context, node);
|
||||
CompleteCreateLifecycleForRegistration(context, node);
|
||||
NodeState activeNode = await AddBehaviourToPredefinedNodeAsync(context, node, cancellationToken).ConfigureAwait(false);
|
||||
PrepareInstanceNodeIdsForRegistration(context, activeNode);
|
||||
if (!ReferenceEquals(activeNode, node))
|
||||
{
|
||||
PrepareInstanceNodeIdsForRegistration(context, activeNode);
|
||||
CompleteCreateLifecycleForRegistration(context, activeNode);
|
||||
}
|
||||
IndexPredefinedNode(activeNode);
|
||||
|
||||
var children = new List<BaseInstanceState>();
|
||||
@@ -2204,8 +2219,9 @@ namespace Opc.Ua.Server
|
||||
/// this path does not invoke
|
||||
/// <see cref="AddBehaviourToPredefinedNodeAsync"/> — it is intended
|
||||
/// for plain instance nodes that carry no asynchronous behaviour
|
||||
/// wiring. Registration is idempotent, so a subsequent tree
|
||||
/// registration that includes the same node is safe.
|
||||
/// wiring. It completes the create lifecycle before indexing.
|
||||
/// Registration is idempotent, so a subsequent tree registration
|
||||
/// that includes the same node is safe.
|
||||
/// </remarks>
|
||||
/// <param name="node">The node subtree to register.</param>
|
||||
/// <exception cref="ArgumentNullException">
|
||||
@@ -2223,6 +2239,7 @@ namespace Opc.Ua.Server
|
||||
|
||||
private void AddPredefinedNodeSynchronously(ISystemContext context, NodeState node)
|
||||
{
|
||||
CompleteCreateLifecycleForRegistration(context, node);
|
||||
IndexPredefinedNode(node);
|
||||
|
||||
var children = new List<BaseInstanceState>();
|
||||
@@ -2240,6 +2257,24 @@ namespace Opc.Ua.Server
|
||||
}
|
||||
}
|
||||
|
||||
private void CompleteCreateLifecycleForRegistration(
|
||||
ISystemContext context,
|
||||
NodeState node)
|
||||
{
|
||||
if (node.IsCreated)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
node.CreateAsPredefinedNode(context);
|
||||
if (m_logger != null)
|
||||
{
|
||||
m_logger.PredefinedNodeLifecycleCompletedAtRegistration(
|
||||
node.NodeId,
|
||||
node.BrowseName);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Indexes a single (already behaviour-resolved) node into the
|
||||
/// <see cref="PredefinedNodes"/> table, the type tree, and the
|
||||
|
||||
@@ -1095,7 +1095,8 @@ namespace Opc.Ua.Server
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Recursively indexes the node and its children.
|
||||
/// Completes the create lifecycle and recursively indexes the node and
|
||||
/// its children.
|
||||
/// </summary>
|
||||
protected virtual void AddPredefinedNode(ISystemContext context, NodeState node)
|
||||
{
|
||||
@@ -1110,7 +1111,12 @@ namespace Opc.Ua.Server
|
||||
Server.TypeTree);
|
||||
}
|
||||
|
||||
CompleteCreateLifecycleForRegistration(context, node);
|
||||
NodeState activeNode = AddBehaviourToPredefinedNode(context, node);
|
||||
if (!ReferenceEquals(activeNode, node))
|
||||
{
|
||||
CompleteCreateLifecycleForRegistration(context, activeNode);
|
||||
}
|
||||
PredefinedNodes.AddOrUpdate(activeNode.NodeId, activeNode, (key, _) => activeNode);
|
||||
|
||||
if (activeNode is BaseTypeState type)
|
||||
@@ -1173,6 +1179,24 @@ namespace Opc.Ua.Server
|
||||
}
|
||||
}
|
||||
|
||||
private void CompleteCreateLifecycleForRegistration(
|
||||
ISystemContext context,
|
||||
NodeState node)
|
||||
{
|
||||
if (node.IsCreated)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
node.CreateAsPredefinedNode(context);
|
||||
if (m_logger != null)
|
||||
{
|
||||
m_logger.PredefinedNodeLifecycleCompletedAtRegistration(
|
||||
node.NodeId,
|
||||
node.BrowseName);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Recursively indexes the node and its children.
|
||||
/// </summary>
|
||||
|
||||
@@ -47,5 +47,14 @@ namespace Opc.Ua.Server
|
||||
[LoggerMessage(EventId = ServerEventIds.NodeManager + 2, Level = LogLevel.Debug,
|
||||
Message = "Cleared historical access advertisement for NodeId={NodeId} because no historian provider is wired.")]
|
||||
public static partial void HistoryAdvertisementCleared(this ILogger logger, NodeId nodeId);
|
||||
|
||||
[LoggerMessage(EventId = ServerEventIds.NodeManager + 3, Level = LogLevel.Debug,
|
||||
Message = "Completed the create lifecycle while registering NodeId={NodeId}, " +
|
||||
"BrowseName={BrowseName}. Call CreateAsPredefinedNode before registration " +
|
||||
"when configuration depends on OnAfterCreate.")]
|
||||
public static partial void PredefinedNodeLifecycleCompletedAtRegistration(
|
||||
this ILogger logger,
|
||||
NodeId nodeId,
|
||||
QualifiedName browseName);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2463,6 +2463,15 @@ namespace Opc.Ua
|
||||
/// </summary>
|
||||
public bool Initialized { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// True if the node has completed its create lifecycle.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <see cref="Delete"/> resets this state. Removing a node from a node
|
||||
/// manager without deleting the instance does not.
|
||||
/// </remarks>
|
||||
public bool IsCreated { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// True if the node must be validated with the underlying system before use.
|
||||
/// </summary>
|
||||
@@ -3041,7 +3050,7 @@ namespace Opc.Ua
|
||||
Initialize(context);
|
||||
|
||||
// Call OnBeforeCreate on all children.
|
||||
CallOnBeforeCreate(context);
|
||||
CallOnBeforeCreate(context, true);
|
||||
|
||||
// override node id.
|
||||
if (!nodeId.IsNull)
|
||||
@@ -3063,13 +3072,16 @@ namespace Opc.Ua
|
||||
DisplayName = displayName;
|
||||
}
|
||||
|
||||
CreateInternal(context, assignNodeIds);
|
||||
CreateInternal(context, assignNodeIds, true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Internal create sequence without node assignments
|
||||
/// </summary>
|
||||
private void CreateInternal(ISystemContext context, bool assignNodeIds)
|
||||
private void CreateInternal(
|
||||
ISystemContext context,
|
||||
bool assignNodeIds,
|
||||
bool forceCreateLifecycle)
|
||||
{
|
||||
// get all children.
|
||||
var children = new List<BaseInstanceState>();
|
||||
@@ -3088,24 +3100,62 @@ namespace Opc.Ua
|
||||
UpdateReferenceTargets(context, children, mappingTable);
|
||||
}
|
||||
|
||||
CallOnAfterCreate(context, children);
|
||||
CallOnAfterCreate(context, children, forceCreateLifecycle);
|
||||
|
||||
const int maxLifecycleCompletionPasses = 100;
|
||||
for (int pass = 0; HasUncreatedNodes(context); pass++)
|
||||
{
|
||||
if (pass >= maxLifecycleCompletionPasses)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"The node create lifecycle did not converge because lifecycle callbacks " +
|
||||
"kept adding uncreated nodes.");
|
||||
}
|
||||
|
||||
CallOnBeforeCreate(context, false);
|
||||
CallOnAfterCreate(context, null, false);
|
||||
}
|
||||
|
||||
ClearChangeMasks(context, true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Recusivesly calls OnBeforeCreate for the node and its children.
|
||||
/// </summary>
|
||||
private void CallOnBeforeCreate(ISystemContext context)
|
||||
private bool HasUncreatedNodes(ISystemContext context)
|
||||
{
|
||||
OnBeforeCreate(context, this);
|
||||
if (!IsCreated)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var children = new List<BaseInstanceState>();
|
||||
GetChildren(context, children);
|
||||
|
||||
for (int ii = 0; ii < children.Count; ii++)
|
||||
{
|
||||
children[ii].CallOnBeforeCreate(context);
|
||||
if (children[ii].HasUncreatedNodes(context))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Recusivesly calls OnBeforeCreate for the node and its children.
|
||||
/// </summary>
|
||||
private void CallOnBeforeCreate(ISystemContext context, bool force)
|
||||
{
|
||||
if (force || !IsCreated)
|
||||
{
|
||||
OnBeforeCreate(context, this);
|
||||
}
|
||||
|
||||
var children = new List<BaseInstanceState>();
|
||||
GetChildren(context, children);
|
||||
|
||||
for (int ii = 0; ii < children.Count; ii++)
|
||||
{
|
||||
children[ii].CallOnBeforeCreate(context, force);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3133,7 +3183,11 @@ namespace Opc.Ua
|
||||
/// <summary>
|
||||
/// Recusivesly calls OnAfterCreate for the node and its children.
|
||||
/// </summary>
|
||||
private void CallOnAfterCreate(ISystemContext context, List<BaseInstanceState>? children, CancellationToken ct = default)
|
||||
private void CallOnAfterCreate(
|
||||
ISystemContext context,
|
||||
List<BaseInstanceState>? children,
|
||||
bool force,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
if (children == null)
|
||||
{
|
||||
@@ -3143,10 +3197,14 @@ namespace Opc.Ua
|
||||
|
||||
for (int ii = 0; ii < children.Count; ii++)
|
||||
{
|
||||
children[ii].CallOnAfterCreate(context, null, ct);
|
||||
children[ii].CallOnAfterCreate(context, null, force, ct);
|
||||
}
|
||||
|
||||
OnAfterCreate(context, this, ct);
|
||||
if (force || !IsCreated)
|
||||
{
|
||||
OnAfterCreate(context, this, ct);
|
||||
IsCreated = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -3155,8 +3213,8 @@ namespace Opc.Ua
|
||||
public virtual void Create(ISystemContext context, NodeState source)
|
||||
{
|
||||
Initialize(context, source);
|
||||
CallOnBeforeCreate(context);
|
||||
CreateInternal(context, false);
|
||||
CallOnBeforeCreate(context, true);
|
||||
CreateInternal(context, false, true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -3176,18 +3234,19 @@ namespace Opc.Ua
|
||||
|
||||
OnAfterDelete(context);
|
||||
|
||||
IsCreated = false;
|
||||
ChangeMasks = NodeStateChangeMasks.Deleted;
|
||||
ClearChangeMasks(context, false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when the predefined node was fully created. Called
|
||||
/// by generated code after all children have been created.
|
||||
/// Completes the create lifecycle for a predefined node and any
|
||||
/// children which have not already completed it.
|
||||
/// </summary>
|
||||
public void CreateAsPredefinedNode(ISystemContext context)
|
||||
{
|
||||
CallOnBeforeCreate(context);
|
||||
CreateInternal(context, false);
|
||||
CallOnBeforeCreate(context, false);
|
||||
CreateInternal(context, false, false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -121,23 +121,24 @@ sequenceDiagram
|
||||
|
||||
## Usage Inside the Server
|
||||
|
||||
The generated Create<SymoblicId> and CreateInstanceOf<SymoblicId> methods (static extension
|
||||
method to ISystemContext) create a new type or instance of a type (respectively) and returns
|
||||
it with all mandatory children set (for the instantiation) and with all children set for the
|
||||
type (but then the child will only define the "modelling rule" for the instance.
|
||||
The generated `Create<SymbolicId>` and `CreateInstanceOf<SymbolicId>`
|
||||
extension methods create a type declaration or a typed instance,
|
||||
respectively. The instance factory materialises all mandatory children and
|
||||
assigns per-instance NodeIds when a browse name and `NodeIdFactory` are
|
||||
available.
|
||||
|
||||
CreateInstanceOf<SymoblicId> does call Initialize(ITelemetryContext) method. TODO:
|
||||
The returned instance has not completed its create lifecycle. Node manager
|
||||
registration completes `OnBeforeCreate`/`OnAfterCreate` and clears change
|
||||
masks before indexing it. Asynchronous registration assigns any required
|
||||
NodeIds first; synchronous predefined-node registration preserves
|
||||
caller-assigned NodeIds. Call `CreateAsPredefinedNode` explicitly when code
|
||||
must read lifecycle-established state, or set state or handlers which
|
||||
`OnAfterCreate` would otherwise replace, before registration.
|
||||
|
||||
The caller can then add optional children and place holders as needed using "Add<ChildName>()"
|
||||
and "Add<ChildName>(QualifiedName browseName)" methods (for optional children and placeholders
|
||||
defined in the type definition).
|
||||
|
||||
These internally call Create<SymoblicId>(this, forInstance: true) to create the optional
|
||||
children of the instantiated type.
|
||||
|
||||
No "user" initialization calls are needed after the Create<SymoblicId> method is called.
|
||||
User can add additional references and children to all states using AddChild/AddReference
|
||||
and so on.
|
||||
The caller can add optional children and placeholders with the generated
|
||||
`Add<ChildName>()` methods. These call
|
||||
`Create<SymbolicId>(this, forInstance: true)` internally. Additional
|
||||
references and children can be attached with `AddReference` and `AddChild`.
|
||||
|
||||
### Legacy behavior
|
||||
|
||||
|
||||
@@ -81,6 +81,29 @@ namespace Opc.Ua.OpenUsd.Tests.Generator
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Completing the generated alarm instance binds the standard condition
|
||||
/// methods supplied by its base classes.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void CompletingFactoryCreatedAlarmBindsConditionMethods()
|
||||
{
|
||||
SystemContext context = CreateContext();
|
||||
GeneratorModel.GeneratorProtectionAlarmState alarm = CreateAlarm(context);
|
||||
|
||||
Assert.That(alarm.IsCreated, Is.False);
|
||||
Assert.That(alarm.Enable!.OnCallMethod, Is.Null);
|
||||
Assert.That(alarm.Disable!.OnCallMethod, Is.Null);
|
||||
Assert.That(alarm.AddComment!.OnCall, Is.Null);
|
||||
|
||||
alarm.CreateAsPredefinedNode(context);
|
||||
|
||||
Assert.That(alarm.IsCreated, Is.True);
|
||||
Assert.That(alarm.Enable.OnCallMethod, Is.Not.Null);
|
||||
Assert.That(alarm.Disable.OnCallMethod, Is.Not.Null);
|
||||
Assert.That(alarm.AddComment.OnCall, Is.Not.Null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The production path gives every published member a reference a browse can
|
||||
/// follow.
|
||||
|
||||
@@ -39,6 +39,7 @@ using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using Opc.Ua.Server.StateMachines;
|
||||
|
||||
namespace Opc.Ua.Server.Tests
|
||||
{
|
||||
@@ -168,6 +169,229 @@ namespace Opc.Ua.Server.Tests
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task AddPredefinedNodeCompletesCreateLifecycleAsync()
|
||||
{
|
||||
using ITestNodeManager manager = CreateManager();
|
||||
ushort namespaceIndex = manager.NamespaceIndexes[0];
|
||||
var node = new LifecycleProbeState(null)
|
||||
{
|
||||
NodeId = new NodeId("Lifecycle", namespaceIndex),
|
||||
BrowseName = new QualifiedName("Lifecycle", namespaceIndex),
|
||||
DisplayName = new LocalizedText("Lifecycle")
|
||||
};
|
||||
node.UpdateChangeMasks(NodeStateChangeMasks.Children);
|
||||
m_mockLogger
|
||||
.Setup(logger => logger.IsEnabled(LogLevel.Debug))
|
||||
.Returns(true);
|
||||
|
||||
await manager.AddPredefinedNodeAsync(manager.SystemContext, node).ConfigureAwait(false);
|
||||
await manager.AddPredefinedNodeAsync(manager.SystemContext, node).ConfigureAwait(false);
|
||||
|
||||
Assert.That(node.IsCreated, Is.True);
|
||||
Assert.That(node.BeforeCreateCount, Is.EqualTo(1));
|
||||
Assert.That(node.AfterCreateCount, Is.EqualTo(1));
|
||||
Assert.That(node.ChangeMasks, Is.EqualTo(NodeStateChangeMasks.None));
|
||||
Assert.That(manager.Find(node.NodeId), Is.SameAs(node));
|
||||
#pragma warning disable CA1873 // Expression tree in Moq Verify is not an executed logging call
|
||||
m_mockLogger.Verify(
|
||||
logger => logger.Log(
|
||||
LogLevel.Debug,
|
||||
It.Is<EventId>(eventId =>
|
||||
eventId.Name == "PredefinedNodeLifecycleCompletedAtRegistration"),
|
||||
It.IsAny<It.IsAnyType>(),
|
||||
It.IsAny<Exception>(),
|
||||
(Func<It.IsAnyType, Exception, string>)It.IsAny<object>()),
|
||||
Times.Once);
|
||||
#pragma warning restore CA1873
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task AddNodeCompletesCreateLifecycleAsync()
|
||||
{
|
||||
using ITestNodeManager manager = CreateManager();
|
||||
ushort namespaceIndex = manager.NamespaceIndexes[0];
|
||||
var node = new LifecycleProbeState(null)
|
||||
{
|
||||
NodeId = new NodeId("RuntimeLifecycle", namespaceIndex),
|
||||
BrowseName = new QualifiedName("RuntimeLifecycle", namespaceIndex),
|
||||
DisplayName = new LocalizedText("RuntimeLifecycle")
|
||||
};
|
||||
|
||||
await manager
|
||||
.AddNodeAsync(manager.SystemContext, NodeId.Null, node)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
Assert.That(node.IsCreated, Is.True);
|
||||
Assert.That(node.BeforeCreateCount, Is.EqualTo(1));
|
||||
Assert.That(node.AfterCreateCount, Is.EqualTo(1));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task PredefinedNodeIdIsAssignedBeforeCreateLifecycleAsync()
|
||||
{
|
||||
using ITestNodeManager manager = CreateManager();
|
||||
Assume.That(manager is TestableAsyncCustomNodeManager,
|
||||
"Requires AsyncCustomNodeManager NodeId preparation");
|
||||
var node = new LifecycleProbeState(null)
|
||||
{
|
||||
BrowseName = new QualifiedName("AssignedBeforeLifecycle", manager.NamespaceIndexes[0]),
|
||||
DisplayName = new LocalizedText("AssignedBeforeLifecycle")
|
||||
};
|
||||
|
||||
await manager
|
||||
.AddPredefinedNodeAsync(manager.SystemContext, node)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
Assert.That(node.NodeId.IsNull, Is.False);
|
||||
Assert.That(node.NodeIdAtAfterCreate, Is.EqualTo(node.NodeId));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task BehaviourReplacementIsCompletedAfterOriginalAsync()
|
||||
{
|
||||
using ITestNodeManager manager = CreateManager();
|
||||
ushort namespaceIndex = manager.NamespaceIndexes[0];
|
||||
var original = new LifecycleProbeState(null)
|
||||
{
|
||||
NodeId = new NodeId("BehaviourReplacement", namespaceIndex),
|
||||
BrowseName = new QualifiedName("BehaviourReplacement", namespaceIndex),
|
||||
DisplayName = new LocalizedText("BehaviourReplacement")
|
||||
};
|
||||
var replacement = new LifecycleProbeState(null)
|
||||
{
|
||||
NodeId = original.NodeId,
|
||||
BrowseName = original.BrowseName,
|
||||
DisplayName = original.DisplayName
|
||||
};
|
||||
bool hookSawCreatedNode = false;
|
||||
manager.AddBehaviourCallback = node =>
|
||||
{
|
||||
hookSawCreatedNode = node.IsCreated;
|
||||
return replacement;
|
||||
};
|
||||
|
||||
await manager
|
||||
.AddPredefinedNodeAsync(manager.SystemContext, original)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
Assert.That(hookSawCreatedNode, Is.True);
|
||||
Assert.That(original.IsCreated, Is.True);
|
||||
Assert.That(replacement.IsCreated, Is.True);
|
||||
Assert.That(manager.Find(original.NodeId), Is.SameAs(replacement));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task RegisteringGeneratedConditionBindsMethodsAsync()
|
||||
{
|
||||
using ITestNodeManager manager = CreateManager();
|
||||
ushort namespaceIndex = manager.NamespaceIndexes[0];
|
||||
ConditionState condition = manager.SystemContext.CreateInstanceOfConditionType(
|
||||
null,
|
||||
new QualifiedName("Condition", namespaceIndex));
|
||||
|
||||
Assert.That(condition.IsCreated, Is.False);
|
||||
Assert.That(condition.Enable!.OnCallMethod, Is.Null);
|
||||
Assert.That(condition.Disable!.OnCallMethod, Is.Null);
|
||||
Assert.That(condition.AddComment!.OnCall, Is.Null);
|
||||
|
||||
await manager
|
||||
.AddPredefinedNodeAsync(manager.SystemContext, condition)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
Assert.That(condition.IsCreated, Is.True);
|
||||
Assert.That(condition.Enable.OnCallMethod, Is.Not.Null);
|
||||
Assert.That(condition.Disable.OnCallMethod, Is.Not.Null);
|
||||
Assert.That(condition.AddComment.OnCall, Is.Not.Null);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task RegisteringStateMachineResolvesElementNamespaceAsync()
|
||||
{
|
||||
using ITestNodeManager manager = CreateManager();
|
||||
ushort instanceNamespaceIndex = manager.NamespaceIndexes[0];
|
||||
ushort elementNamespaceIndex = (ushort)manager.SystemContext.NamespaceUris
|
||||
.GetIndexOrAppend(NamespaceFiniteStateMachineState.ElementsNamespaceUri);
|
||||
var machine = new NamespaceFiniteStateMachineState(null)
|
||||
{
|
||||
NodeId = new NodeId("StateMachine", instanceNamespaceIndex),
|
||||
BrowseName = new QualifiedName("StateMachine", instanceNamespaceIndex),
|
||||
DisplayName = new LocalizedText("StateMachine")
|
||||
};
|
||||
|
||||
Assert.That(machine.GetStateNodeId(123), Is.EqualTo(new NodeId(123)));
|
||||
|
||||
await manager
|
||||
.AddPredefinedNodeAsync(manager.SystemContext, machine)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
Assert.That(machine.IsCreated, Is.True);
|
||||
Assert.That(
|
||||
machine.GetStateNodeId(123),
|
||||
Is.EqualTo(new NodeId(123, elementNamespaceIndex)));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task RegistrationRebasesLifecycleMaterializedStateNodesAsync()
|
||||
{
|
||||
using ITestNodeManager manager = CreateManager();
|
||||
Assume.That(manager is TestableAsyncCustomNodeManager,
|
||||
"Requires AsyncCustomNodeManager NodeId preparation");
|
||||
StateMachineDefinition definition = StateMachineBuilder
|
||||
.Create(
|
||||
null,
|
||||
manager.SystemContext,
|
||||
new NodeId("DefinitionSource", manager.NamespaceIndexes[0]),
|
||||
new QualifiedName("DefinitionSource", manager.NamespaceIndexes[0]))
|
||||
.AddState(1, "Off", isInitial: true)
|
||||
.AddState(2, "On")
|
||||
.StateMachine
|
||||
.Definition;
|
||||
var machine = new FluentFiniteStateMachineState(null, definition)
|
||||
{
|
||||
BrowseName = new QualifiedName("RuntimeMachine", manager.NamespaceIndexes[0]),
|
||||
DisplayName = new LocalizedText("RuntimeMachine")
|
||||
};
|
||||
|
||||
await manager
|
||||
.AddPredefinedNodeAsync(manager.SystemContext, machine)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
var children = new List<BaseInstanceState>();
|
||||
machine.GetChildren(manager.SystemContext, children);
|
||||
BaseInstanceState off = children.Single(child => child.BrowseName.Name == "Off");
|
||||
|
||||
Assert.That(machine.NodeId.IsNull, Is.False);
|
||||
Assert.That(off.NodeId.IsNull, Is.False);
|
||||
Assert.That(
|
||||
off.NodeId.IdentifierAsString,
|
||||
Is.EqualTo($"{machine.NodeId}_Off"));
|
||||
Assert.That(machine.GetStateNodeId(1), Is.EqualTo(off.NodeId));
|
||||
Assert.That(manager.Find(off.NodeId), Is.SameAs(off));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SynchronousPredefinedNodeRegistrationCompletesCreateLifecycle()
|
||||
{
|
||||
using ITestNodeManager manager = CreateManager();
|
||||
Assume.That(manager is TestableAsyncCustomNodeManager,
|
||||
"Requires AsyncCustomNodeManager synchronous registration");
|
||||
var asyncManager = (TestableAsyncCustomNodeManager)manager;
|
||||
ushort namespaceIndex = manager.NamespaceIndexes[0];
|
||||
var node = new LifecycleProbeState(null)
|
||||
{
|
||||
NodeId = new NodeId("SynchronousLifecycle", namespaceIndex),
|
||||
BrowseName = new QualifiedName("SynchronousLifecycle", namespaceIndex),
|
||||
DisplayName = new LocalizedText("SynchronousLifecycle")
|
||||
};
|
||||
|
||||
asyncManager.AddPredefinedNodeSynchronouslyPublic(node);
|
||||
|
||||
Assert.That(node.IsCreated, Is.True);
|
||||
Assert.That(node.BeforeCreateCount, Is.EqualTo(1));
|
||||
Assert.That(node.AfterCreateCount, Is.EqualTo(1));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task FindPredefinedNode_ReturnsNodeOnlyWhenTypeMatchesAsync()
|
||||
{
|
||||
@@ -4868,6 +5092,50 @@ namespace Opc.Ua.Server.Tests
|
||||
disposable.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class LifecycleProbeState : BaseObjectState
|
||||
{
|
||||
public LifecycleProbeState(NodeState parent)
|
||||
: base(parent)
|
||||
{
|
||||
}
|
||||
|
||||
public int BeforeCreateCount { get; private set; }
|
||||
|
||||
public int AfterCreateCount { get; private set; }
|
||||
|
||||
public NodeId NodeIdAtAfterCreate { get; private set; }
|
||||
|
||||
protected override void OnBeforeCreate(ISystemContext context, NodeState node)
|
||||
{
|
||||
BeforeCreateCount++;
|
||||
base.OnBeforeCreate(context, node);
|
||||
}
|
||||
|
||||
protected override void OnAfterCreate(
|
||||
ISystemContext context,
|
||||
NodeState node,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
AfterCreateCount++;
|
||||
NodeIdAtAfterCreate = NodeId;
|
||||
base.OnAfterCreate(context, node, ct);
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class NamespaceFiniteStateMachineState : FiniteStateMachineState
|
||||
{
|
||||
public const string ElementsNamespaceUri =
|
||||
"http://test.org/UA/FiniteStateMachineElements/";
|
||||
|
||||
public NamespaceFiniteStateMachineState(NodeState parent)
|
||||
: base(parent)
|
||||
{
|
||||
}
|
||||
|
||||
protected override string ElementNamespaceUri =>
|
||||
ElementsNamespaceUri;
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class TestableAsyncCustomNodeManager : AsyncCustomNodeManager, ITestNodeManager
|
||||
@@ -4907,6 +5175,8 @@ namespace Opc.Ua.Server.Tests
|
||||
|
||||
public Func<NodeState, CancellationToken, ValueTask> NodeRemovedCallback { get; set; } = null!;
|
||||
|
||||
public Func<NodeState, NodeState> AddBehaviourCallback { get; set; } = null!;
|
||||
|
||||
public Func<ServerSystemContext, NodeId, NodeState, bool> IsNodeInViewOverride { get; set; }
|
||||
|
||||
protected override bool IsNodeInView(ServerSystemContext context, NodeId viewId, NodeState node)
|
||||
@@ -4936,6 +5206,19 @@ namespace Opc.Ua.Server.Tests
|
||||
return NodeRemovedCallback?.Invoke(node, cancellationToken) ?? default;
|
||||
}
|
||||
|
||||
protected override ValueTask<NodeState> AddBehaviourToPredefinedNodeAsync(
|
||||
ISystemContext context,
|
||||
NodeState predefinedNode,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
return AddBehaviourCallback != null
|
||||
? new ValueTask<NodeState>(AddBehaviourCallback(predefinedNode))
|
||||
: base.AddBehaviourToPredefinedNodeAsync(
|
||||
context,
|
||||
predefinedNode,
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
public ValueTask AddPredefinedNodeWithExternalReferencesAsync(
|
||||
ISystemContext context,
|
||||
NodeState node,
|
||||
@@ -5039,6 +5322,11 @@ namespace Opc.Ua.Server.Tests
|
||||
return AddPredefinedNodeAsync(context, node, cancellationToken);
|
||||
}
|
||||
|
||||
public void AddPredefinedNodeSynchronouslyPublic(NodeState node)
|
||||
{
|
||||
AddPredefinedNodeSynchronously(node);
|
||||
}
|
||||
|
||||
ValueTask ITestNodeManager.AddPredefinedNodeAsync(
|
||||
ISystemContext context,
|
||||
NodeState node,
|
||||
@@ -5273,6 +5561,8 @@ namespace Opc.Ua.Server.Tests
|
||||
/// </summary>
|
||||
Func<ServerSystemContext, NodeId, NodeState, bool>? IsNodeInViewOverride { get; set; }
|
||||
|
||||
Func<NodeState, NodeState>? AddBehaviourCallback { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Tests whether a NodeId belongs to a managed namespace.
|
||||
/// </summary>
|
||||
@@ -5398,12 +5688,22 @@ namespace Opc.Ua.Server.Tests
|
||||
|
||||
public Func<ServerSystemContext, NodeId, NodeState, bool>? IsNodeInViewOverride { get; set; }
|
||||
|
||||
public Func<NodeState, NodeState>? AddBehaviourCallback { get; set; }
|
||||
|
||||
protected override bool IsNodeInView(ServerSystemContext context, NodeId viewId, NodeState node)
|
||||
{
|
||||
return IsNodeInViewOverride?.Invoke(context, viewId, node)
|
||||
?? base.IsNodeInView(context, viewId, node);
|
||||
}
|
||||
|
||||
protected override NodeState AddBehaviourToPredefinedNode(
|
||||
ISystemContext context,
|
||||
NodeState predefinedNode)
|
||||
{
|
||||
return AddBehaviourCallback?.Invoke(predefinedNode)
|
||||
?? base.AddBehaviourToPredefinedNode(context, predefinedNode);
|
||||
}
|
||||
|
||||
public void AddPredefinedNodePublic(ISystemContext context, NodeState node)
|
||||
{
|
||||
AddPredefinedNode(context, node);
|
||||
@@ -5575,6 +5875,12 @@ namespace Opc.Ua.Server.Tests
|
||||
set => m_cnm2.IsNodeInViewOverride = value;
|
||||
}
|
||||
|
||||
public Func<NodeState, NodeState>? AddBehaviourCallback
|
||||
{
|
||||
get => m_cnm2.AddBehaviourCallback;
|
||||
set => m_cnm2.AddBehaviourCallback = value;
|
||||
}
|
||||
|
||||
public bool IsNodeIdInNamespacePublic(NodeId nodeId)
|
||||
{
|
||||
return m_cnm2.IsNodeIdInNamespacePublic(nodeId);
|
||||
|
||||
@@ -432,7 +432,8 @@ namespace Opc.Ua.Types.Tests.State
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AddChildSetsParentAndAddsToChildren() {
|
||||
public void AddChildSetsParentAndAddsToChildren()
|
||||
{
|
||||
BaseObjectState parent = CreateObjectNode();
|
||||
PropertyState child = CreatePropertyChild(parent, "Child1");
|
||||
parent.AddChild(child);
|
||||
@@ -2652,15 +2653,105 @@ namespace Opc.Ua.Types.Tests.State
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void CreateAsPredefinedNodeSucceeds()
|
||||
public void CreateAsPredefinedNodeCompletesLifecycleOnce()
|
||||
{
|
||||
var node = new BaseObjectState(null)
|
||||
{
|
||||
NodeId = new NodeId(100, 0),
|
||||
BrowseName = QualifiedName.From("Predefined"),
|
||||
DisplayName = LocalizedText.From("Predefined")
|
||||
};
|
||||
Assert.DoesNotThrow(() => node.CreateAsPredefinedNode(m_context));
|
||||
var node = new LifecycleProbeState(null);
|
||||
|
||||
Assert.That(node.IsCreated, Is.False);
|
||||
|
||||
node.CreateAsPredefinedNode(m_context);
|
||||
node.CreateAsPredefinedNode(m_context);
|
||||
|
||||
Assert.That(node.IsCreated, Is.True);
|
||||
Assert.That(node.BeforeCreateCount, Is.EqualTo(1));
|
||||
Assert.That(node.AfterCreateCount, Is.EqualTo(1));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void CreateAsPredefinedNodeCompletesLateChild()
|
||||
{
|
||||
var parent = new LifecycleProbeState(null);
|
||||
parent.CreateAsPredefinedNode(m_context);
|
||||
|
||||
var child = new LifecycleProbeState(parent);
|
||||
parent.AddChild(child);
|
||||
|
||||
parent.CreateAsPredefinedNode(m_context);
|
||||
|
||||
Assert.That(parent.BeforeCreateCount, Is.EqualTo(1));
|
||||
Assert.That(parent.AfterCreateCount, Is.EqualTo(1));
|
||||
Assert.That(child.IsCreated, Is.True);
|
||||
Assert.That(child.BeforeCreateCount, Is.EqualTo(1));
|
||||
Assert.That(child.AfterCreateCount, Is.EqualTo(1));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void CreateAsPredefinedNodeCompletesChildAddedByOnAfterCreate()
|
||||
{
|
||||
var parent = new ChildCreatingState(null);
|
||||
|
||||
parent.CreateAsPredefinedNode(m_context);
|
||||
|
||||
Assert.That(parent.AddedChild, Is.Not.Null);
|
||||
Assert.That(parent.AddedChild.IsCreated, Is.True);
|
||||
Assert.That(parent.AddedChild.BeforeCreateCount, Is.EqualTo(1));
|
||||
Assert.That(parent.AddedChild.AfterCreateCount, Is.EqualTo(1));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void CreateAsPredefinedNodeRejectsNonConvergingLifecycle()
|
||||
{
|
||||
var node = new NonConvergingState(null);
|
||||
|
||||
Assert.That(
|
||||
() => node.CreateAsPredefinedNode(m_context),
|
||||
Throws.TypeOf<InvalidOperationException>()
|
||||
.With.Message.Contains("did not converge"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void DeleteResetsCreatedState()
|
||||
{
|
||||
var node = new LifecycleProbeState(null);
|
||||
node.CreateAsPredefinedNode(m_context);
|
||||
|
||||
node.Delete(m_context);
|
||||
|
||||
Assert.That(node.IsCreated, Is.False);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void CloneDoesNotCopyCreatedState()
|
||||
{
|
||||
BaseObjectState node = CreateObjectNode();
|
||||
node.CreateAsPredefinedNode(m_context);
|
||||
|
||||
var clone = (BaseObjectState)node.Clone();
|
||||
|
||||
Assert.That(node.IsCreated, Is.True);
|
||||
Assert.That(clone.IsCreated, Is.False);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void CreateAlwaysRunsLifecycle()
|
||||
{
|
||||
var node = new LifecycleProbeState(null);
|
||||
|
||||
node.Create(
|
||||
m_context,
|
||||
NodeId.Null,
|
||||
QualifiedName.Null,
|
||||
LocalizedText.Null,
|
||||
false);
|
||||
node.Create(
|
||||
m_context,
|
||||
NodeId.Null,
|
||||
QualifiedName.Null,
|
||||
LocalizedText.Null,
|
||||
false);
|
||||
|
||||
Assert.That(node.BeforeCreateCount, Is.EqualTo(2));
|
||||
Assert.That(node.AfterCreateCount, Is.EqualTo(2));
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -2712,5 +2803,72 @@ namespace Opc.Ua.Types.Tests.State
|
||||
return new NodeId((uint)(90000 + Count), 0);
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class LifecycleProbeState : BaseObjectState
|
||||
{
|
||||
public LifecycleProbeState(NodeState parent)
|
||||
: base(parent)
|
||||
{
|
||||
}
|
||||
|
||||
public int BeforeCreateCount { get; private set; }
|
||||
|
||||
public int AfterCreateCount { get; private set; }
|
||||
|
||||
protected override void OnBeforeCreate(ISystemContext context, NodeState node)
|
||||
{
|
||||
BeforeCreateCount++;
|
||||
base.OnBeforeCreate(context, node);
|
||||
}
|
||||
|
||||
protected override void OnAfterCreate(
|
||||
ISystemContext context,
|
||||
NodeState node,
|
||||
System.Threading.CancellationToken ct = default)
|
||||
{
|
||||
AfterCreateCount++;
|
||||
base.OnAfterCreate(context, node, ct);
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class ChildCreatingState : BaseObjectState
|
||||
{
|
||||
public ChildCreatingState(NodeState parent)
|
||||
: base(parent)
|
||||
{
|
||||
}
|
||||
|
||||
public LifecycleProbeState AddedChild { get; private set; } = null!;
|
||||
|
||||
protected override void OnAfterCreate(
|
||||
ISystemContext context,
|
||||
NodeState node,
|
||||
System.Threading.CancellationToken ct = default)
|
||||
{
|
||||
base.OnAfterCreate(context, node, ct);
|
||||
if (AddedChild == null)
|
||||
{
|
||||
AddedChild = new LifecycleProbeState(this);
|
||||
AddChild(AddedChild);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class NonConvergingState : BaseObjectState
|
||||
{
|
||||
public NonConvergingState(NodeState parent)
|
||||
: base(parent)
|
||||
{
|
||||
}
|
||||
|
||||
protected override void OnAfterCreate(
|
||||
ISystemContext context,
|
||||
NodeState node,
|
||||
System.Threading.CancellationToken ct = default)
|
||||
{
|
||||
base.OnAfterCreate(context, node, ct);
|
||||
AddChild(new NonConvergingState(this));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1788,6 +1788,13 @@ namespace Opc.Ua.SourceGeneration
|
||||
/// <summary>
|
||||
/// Creates an instance of the {{Tokens.SymbolicName}} Method node state.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The returned node graph has not completed its create lifecycle.
|
||||
/// Node manager registration completes it automatically. Call
|
||||
/// <see cref="global::Opc.Ua.NodeState.CreateAsPredefinedNode"/>
|
||||
/// before registration when configuration depends on
|
||||
/// <c>OnBeforeCreate</c> or <c>OnAfterCreate</c>.
|
||||
/// </remarks>
|
||||
public static {{Tokens.StateClassName}} CreateInstanceOf{{Tokens.SymbolicId}}(
|
||||
this global::Opc.Ua.ISystemContext context,
|
||||
global::Opc.Ua.NodeState parent = null,
|
||||
@@ -1893,6 +1900,13 @@ namespace Opc.Ua.SourceGeneration
|
||||
/// <summary>
|
||||
/// Creates an instance of the {{Tokens.TypeName}} type.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The returned node graph has not completed its create lifecycle.
|
||||
/// Node manager registration completes it automatically. Call
|
||||
/// <see cref="global::Opc.Ua.NodeState.CreateAsPredefinedNode"/>
|
||||
/// before registration when configuration depends on
|
||||
/// <c>OnBeforeCreate</c> or <c>OnAfterCreate</c>.
|
||||
/// </remarks>
|
||||
public static {{Tokens.StateClassName}} Create{{Tokens.SymbolicId}}(
|
||||
this global::Opc.Ua.ISystemContext context,
|
||||
global::Opc.Ua.NodeState parent = null,
|
||||
@@ -1951,6 +1965,13 @@ namespace Opc.Ua.SourceGeneration
|
||||
/// <summary>
|
||||
/// Creates an instance of the {{Tokens.TypeName}} variable type.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The returned node graph has not completed its create lifecycle.
|
||||
/// Node manager registration completes it automatically. Call
|
||||
/// <see cref="global::Opc.Ua.NodeState.CreateAsPredefinedNode"/>
|
||||
/// before registration when configuration depends on
|
||||
/// <c>OnBeforeCreate</c> or <c>OnAfterCreate</c>.
|
||||
/// </remarks>
|
||||
public static {{Tokens.StateClassName}} Create{{Tokens.SymbolicId}}(
|
||||
this global::Opc.Ua.ISystemContext context,
|
||||
global::Opc.Ua.NodeState parent = null,
|
||||
|
||||
Reference in New Issue
Block a user