# 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>
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
Creating Nodes with Generated Code (Recommended)
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
-
Always Initialize: Never use a
NodeStatewithout callingInitialize()first. -
Use Generated Code: Prefer generated
Create*andCreateInstanceOf*methods for type safety and proper initialization. -
Clear Change Masks: Call
ClearChangeMasks()after making changes to trigger theOnStateChangedcallback. -
Thread Safety: Use appropriate locking when accessing nodes from multiple threads. The
NodeStateclass uses internal locks for children and references collections. -
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.
-
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 |