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

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

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

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

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

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

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

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

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

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

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

## Related Issues

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

## Checklist

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

---------

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

NodeState System

The NodeState system is the in-memory representation of OPC UA nodes in the server address space. It provides the foundation for managing nodes, their attributes, children, references, and the events that occur during their lifecycle.

Overview

classDiagram
    direction TB
    NodeState <|-- BaseTypeState
    NodeState <|-- BaseInstanceState
    
    BaseTypeState <|-- BaseObjectTypeState
    BaseTypeState <|-- BaseVariableTypeState
    BaseTypeState <|-- ReferenceTypeState
    BaseTypeState <|-- DataTypeState
    
    BaseInstanceState <|-- BaseObjectState
    BaseInstanceState <|-- BaseVariableState
    BaseInstanceState <|-- MethodState
    BaseInstanceState <|-- ViewState
    
    BaseVariableState <|-- PropertyState
    BaseVariableState <|-- BaseDataVariableState
    
    class NodeState {
        +NodeId NodeId
        +NodeClass NodeClass
        +QualifiedName BrowseName
        +LocalizedText DisplayName
        +AddChild(child)
        +AddReference(refType, isInverse, target)
        +GetChildren(context, list)
        +GetReferences(context, list)
        +Create(context, nodeId, browseName, displayName, assignNodeIds)
    }
    
    class BaseInstanceState {
        +NodeState Parent
        +NodeId TypeDefinitionId
        +NodeId ReferenceTypeId
        +NodeId ModellingRuleId
    }
    
    class BaseTypeState {
        +NodeId SuperTypeId
        +bool IsAbstract
    }

Lifecycle of a NodeState

The lifecycle of a NodeState involves creation, initialization, usage in the server, and disposal.

stateDiagram-v2
    [*] --> Created: Constructor called
    Created --> Initialized: Initialize() called
    Initialized --> Active: Create() called / Added to NodeManager
    Active --> Modified: Attributes/Children changed
    Modified --> Active: ClearChangeMasks()
    Active --> Deleted: Delete() called
    Deleted --> [*]
    
    note right of Created
        Node exists but not ready for use.
        Attributes have default values.
    end note
    
    note right of Initialized
        All attributes, children, and
        references are configured.
    end note
    
    note right of Active
        Node is part of address space,
        responding to client requests.
    end note

Initialization Methods

The NodeState class provides several Initialize overloads for different scenarios:

Method Purpose
Initialize(ITelemetryContext) Sets up telemetry/logging. Called by all other overloads.
Initialize(ISystemContext) Initializes with default values. Called when creating new nodes.
Initialize(ISystemContext, NodeState) Copies attributes from a source node (for deserialization).
Initialize(ISystemContext, string) Initializes from XML or binary encoded string.
InitializeOptionalChildren(ISystemContext) Initializes optional children that have been set as properties.

Key Lifecycle Callbacks

The following virtual methods can be overridden to customize node behavior:

sequenceDiagram
    participant App as Application
    participant NS as NodeState
    participant NM as NodeManager
    
    App->>NS: Create(context, nodeId, ...)
    NS->>NS: Initialize(context)
    NS->>NS: OnBeforeCreate(context, node)
    NS->>NS: OnBeforeAssignNodeIds(context)
    NS->>NS: AssignNodeIds(context, mappingTable)
    NS->>NS: OnAfterCreate(context, node)
    NS->>NS: ClearChangeMasks(context, true)
    
    App->>NM: AddNode(nodeState)
    Note over NM: Node is now in address space
    
    App->>NS: Delete(context)
    NS->>NS: OnBeforeDelete(context)
    NS->>NS: Delete children recursively
    NS->>NS: OnAfterDelete(context)

Usage Inside the Server

The generated Create and CreateInstanceOf 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.

CreateInstanceOf does call Initialize(ITelemetryContext) method. TODO:

The caller can then add optional children and place holders as needed using "Add()" and "Add(QualifiedName browseName)" methods (for optional children and placeholders defined in the type definition).

These internally call Create(this, forInstance: true) to create the optional children of the instantiated type.

No "user" initialization calls are needed after the Create method is called. User can add additional references and children to all states using AddChild/AddReference and so on.

Legacy behavior

If the constructor is called directly the instance is created but still must be initialized in this case the caller must call Initialize(context) before using the instance which will call Initialize(context, CreateInstanceOf(context)) to initialize all attributes, references and mandatory children.

If the class is extended, the same applies.

If optional children are "set" as properties, the InitializeOptionalChildren(context) method must be called to call Initialize on all children (if they are not yet initialized).

Address Space Integration

Nodes are managed by NodeManager implementations within the server. The typical flow:

flowchart TD
    subgraph Server
        direction TB
        NM[NodeManager]
        AS[Address Space]
        SM[Session Manager]
    end
    
    subgraph NodeState
        direction TB
        NS[NodeState]
        CH[Children]
        RF[References]
        EV[Events]
    end
    
    Client -->|Browse/Read/Write| SM
    SM -->|Route Request| NM
    NM -->|Manage| NS
    NS -->|Contains| CH
    NS -->|Has| RF
    NS -->|Reports| EV
    NM -->|Register| AS

Reading and Writing Attributes

The server reads and writes node attributes through callback handlers:

// Example: Setting up attribute callbacks
myVariable.OnReadValue = (context, node, indexRange, dataEncoding, 
                          ref value, ref statusCode, ref timestamp) =>
{
    // Custom read logic
    value = GetCurrentSensorValue();
    return ServiceResult.Good;
};

myVariable.OnWriteValue = (context, node, indexRange, dataEncoding,
                           ref value, ref statusCode, ref timestamp) =>
{
    // Custom write logic
    SetActuatorValue(value);
    return ServiceResult.Good;
};

Event Handling

Nodes can report events and track monitored items:

// Report an event from a node
nodeState.ReportEvent(context, eventInstance);

// Check if events are being monitored
if (nodeState.AreEventsMonitored)
{
    // Generate and report event
}

Change Tracking

The ChangeMasks property tracks what has changed since the last ClearChangeMasks() call:

// After modifying a node
nodeState.DisplayName = "New Name";  // Sets NodeStateChangeMasks.NonValue

// The OnStateChanged callback is invoked when clearing change masks
nodeState.OnStateChanged = (context, node, changes) =>
{
    // Handle changes (e.g., persist to database)
    if ((changes & NodeStateChangeMasks.Children) != 0)
    {
        // Children were added/removed
    }
};

nodeState.ClearChangeMasks(context, includeChildren: true);

Usage Guidelines for Developers

The source generator creates helper methods that properly initialize nodes:

// Create a type node (includes all children from the type definition)
var sensorType = context.CreateSensorType();

// Create an instance of a type (includes mandatory children)
var sensor = context.CreateInstanceOfSensorType();

// Add optional children defined in the type
sensor.AddOptionalProperty();  // Creates and initializes the optional property

// Add placeholder instances
sensor.AddSensorReading(new QualifiedName("Temperature", namespaceIndex));
flowchart LR
    subgraph Generated Methods
        CT[CreateSensorType]
        CI[CreateInstanceOfSensorType]
        AO[AddOptionalChild]
    end
    
    CT -->|Returns| Type[Type with all children]
    CI -->|Returns| Instance[Instance with mandatory children]
    AO -->|Adds| Optional[Initialized optional child]

Creating Nodes Manually (Legacy)

When not using generated code, manual initialization is required:

// Step 1: Create the node
var variable = new BaseDataVariableState(parent);

// Step 2: Initialize the node (REQUIRED before use)
variable.Initialize(context);

// Step 3: Set attributes
variable.NodeId = new NodeId(1234, namespaceIndex);
variable.BrowseName = new QualifiedName("MyVariable", namespaceIndex);
variable.DisplayName = "My Variable";
variable.TypeDefinitionId = VariableTypeIds.BaseDataVariableType;
variable.DataType = DataTypeIds.Double;
variable.ValueRank = ValueRanks.Scalar;
variable.Value = 0.0;

// Step 4: Create and assign node IDs
variable.Create(context, variable.NodeId, variable.BrowseName, 
                variable.DisplayName, assignNodeIds: true);

// Step 5: Add to parent
parent.AddChild(variable);

Managing Children

// Add a child node
parent.AddChild(childNode);

// Remove a child node
parent.RemoveChild(childNode);

// Find a child by browse name
var child = parent.FindChild(context, new QualifiedName("ChildName", ns));

// Get all children
var children = new List<BaseInstanceState>();
parent.GetChildren(context, children);

// Find child by symbolic path
var deepChild = parent.FindChildBySymbolicName(context, "Child1/Child2/Target");

Managing References

// Add a reference to another node
sourceNode.AddReference(
    ReferenceTypeIds.Organizes,
    isInverse: false,
    targetId: targetNode.NodeId);

// Check if reference exists
bool exists = sourceNode.ReferenceExists(
    ReferenceTypeIds.Organizes,
    isInverse: false,
    targetId: targetNode.NodeId);

// Remove a reference
sourceNode.RemoveReference(
    ReferenceTypeIds.Organizes,
    isInverse: false,
    targetId: targetNode.NodeId);

// Get all references
var references = new List<IReference>();
sourceNode.GetReferences(context, references);

Serialization and Deserialization

Nodes can be saved and loaded from streams:

// Save to XML
using var stream = new MemoryStream();
nodeState.SaveAsXml(context, stream);

// Save to binary
using var binaryStream = new MemoryStream();
nodeState.SaveAsBinary(context, binaryStream);

// Load from XML
nodeState.LoadFromXml(context, xmlStream);

// Load from binary
nodeState.LoadAsBinary(context, binaryStream);

Node State Factory

The NodeStateFactory creates node instances based on type definitions:

// Register a custom factory for a type
context.NodeStateFactory.RegisterType(
    typeDefinitionId,
    new MyCustomNodeActivator());

// Create an instance using the factory
var node = context.NodeStateFactory.CreateInstance(
    context,
    parent,
    NodeClass.Object,
    browseName,
    referenceTypeId,
    typeDefinitionId);

Best Practices

  1. Always Initialize: Never use a NodeState without calling Initialize() first.

  2. Use Generated Code: Prefer generated Create* and CreateInstanceOf* methods for type safety and proper initialization.

  3. Clear Change Masks: Call ClearChangeMasks() after making changes to trigger the OnStateChanged callback.

  4. Thread Safety: Use appropriate locking when accessing nodes from multiple threads. The NodeState class uses internal locks for children and references collections.

  5. Memory Management: All resources allocated for a node state must be managed outsidee of the node state, e.g. in the node manager that owns the node state.

  6. Use Callbacks Sparingly: Only attach callbacks (OnReadValue, OnWriteValue, etc.) when custom behavior is needed. Default behavior is often sufficient.

Common Pitfalls

Issue Solution
Node not appearing in address space Ensure Create() was called and node is added to NodeManager
Children not visible Verify children have valid NodeId and are added with AddChild()
Attribute changes not persisted Implement OnStateChanged callback and persist changes
Deserialization fails Check namespace URIs match and type definitions are registered
Memory leaks with events Unsubscribe from StateChanged and other events before disposal