Skip to content

Initial commit. - #1

Closed
barnstee wants to merge 1 commit into
OPCFoundation:masterfrom
barnstee:master
Closed

Initial commit.#1
barnstee wants to merge 1 commit into
OPCFoundation:masterfrom
barnstee:master

Conversation

@barnstee

Copy link
Copy Markdown
Member

No description provided.

@barnstee barnstee closed this Feb 23, 2016
@barnstee barnstee reopened this Feb 23, 2016
@barnstee barnstee closed this Feb 23, 2016
AlinMoldovean pushed a commit that referenced this pull request Sep 17, 2017
AlinMoldovean added a commit that referenced this pull request Mar 8, 2018
Merge remote-tracking branch 'origin/v1.03' into v1.03
mregen added a commit that referenced this pull request Dec 7, 2020
Archie-Miller referenced this pull request in DreamsAndAdventures/UA-.NETStandard Jul 22, 2024
@CLAassistant

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.


Erich Barnstedt seems not to be a GitHub user. You need a GitHub account to be able to sign the CLA. If you have already a GitHub account, please add the email address used for this commit to your account.
You have signed the CLA already but the status is still pending? Let us recheck it.

marcschier added a commit that referenced this pull request Apr 21, 2026
… input

- Rename Applications/ConsoleBoilerServer to MinimalBoilerServer.
- Drop Design/BoilerDesign.xml; drive source generation from
  Model/Boiler.NodeSet2.xml exported via the reference client.
- Source generator: extend the NodeSet2 input pipeline to honor
  [NodeManager] attribute bindings (was only wired on the design path).
- Source generator: emit AddChild for real instance children of
  top-level (non-typed) parents unconditionally so folder hierarchies
  like Boilers/Boiler #1 materialize at runtime instead of only when
  the parent is treated as a type template.
- Update AOT tests, docs, and solution to match the rename.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
marcschier added a commit that referenced this pull request Apr 21, 2026
Adds four new overloads to INodeManagerBuilder that locate nodes by
their TypeDefinitionId, so well-known singletons can be wired without
fragile browse paths:

  builder.NodeFromTypeId(ObjectTypeIds.HistoryServerCapabilitiesType)
         .OnRead(...);

For multi-instance types, an optional QualifiedName disambiguator
picks one match:

  builder.NodeFromTypeId(ObjectTypeIds.BoilerType, 'Boiler #1')
         .OnRead(...);

Resolution is backed by a new Func<NodeId, IReadOnlyList<NodeState>>
ctor parameter on NodeManagerBuilder; the source-generator template
emits a BFS walk over PredefinedNodes + children as __FindByType-
DefinitionId. Error matrix: BadNodeIdInvalid / BadNodeIdUnknown /
BadBrowseNameDuplicated / BadTypeMismatch.

- Tests: 8 new fluent-builder tests covering all resolution paths.
- MinimalBoilerServer: switches Boiler-lifecycle wiring to the
  typeId lookup, exercising the path under NativeAOT.
- Docs: SourceGeneratedNodeManagers.md documents the four addressing
  modes and the error matrix.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
marcschier added a commit that referenced this pull request May 4, 2026
Addresses PR #3730 review feedback from @AnatolyKochnev: when ProcessMessageAsync receives its first message (LastSequenceNumberProcessed == 0) and that message has SequenceNumber > 1, the gap-walk republish branch was skipped, so any retransmissible messages between #1 and #curSeqNum-1 were silently dropped.

This mirrors the V1 fix in PR #3565 (which added placeholders for missing pre-first-message sequence numbers in the classic Subscription).

Implementation: when prevSeqNum == 0, iterate AvailableInRetransmissionQueue (populated from the publish response's availableSequenceNumbers list) and call TryRepublishAsync for each entry whose unsigned-distance places it before curSeqNum. The cost is bounded by the server's retransmission queue size (typically a few hundred entries) instead of the raw seq-number gap, which can be arbitrarily large after a transfer.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
marcschier pushed a commit that referenced this pull request May 12, 2026
ReferenceNodeManager.Dispose previously disposed m_semaphore BEFORE
m_simulationTimer. The simulation Timer fires on the threadpool, and
Timer.Dispose() does NOT wait for in-flight callbacks. So a callback
already scheduled by the threadpool could race past the semaphore
disposal and hit ObjectDisposedException inside DoSimulation:
    SERVER ... [ReferenceNodeManager] Unexpected error doing simulation #1.
    [ObjectDisposedException] Cannot access a disposed object.
    Object name: 'System.Threading.SemaphoreSlim'.
    ---    at System.Threading.SemaphoreSlim.Release(...)
    ---    at ReferenceNodeManager.DoSimulation(...) line 5375
DoSimulation catches and logs the exception so it doesn't fault the
test, but the noise destabilises CI runs (observed on the
ubuntu-latest Conformance job at 16:36:55 UTC).
Fix: snap m_simulationTimer to a local, dispose it via the
Timer.Dispose(WaitHandle) overload, wait up to 2 s for the in-flight
callback to drain, then dispose the semaphore.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
marcschier added a commit that referenced this pull request May 27, 2026
Comments from @marcschier addressed:

1. **Remove mention of phases throughout** (Program.cs:59, ClientHelpers.md:4,
   all Docs/Opc.Ua.Di/*.md, MinimalPumpServer/README.md, ComplianceMatrix.md).
   Removed 'Phase 8X' / 'G1..G11' wording from comments, docs, table
   headers, and ComplianceMatrix.md sub-sections — kept the technical
   content intact.

2. **Add a separate software-update server sample**
   (PumpSoftwareUpdateSeeder.cs). Created
   Applications/MinimalSoftwareUpdateServer/ — a self-contained DI
   server that demonstrates the software-update facet end-to-end
   with a single 'UpdateableDevice #1' under DeviceSet and a seeded
   ISoftwarePackageStore. Removed the SoftwareUpdate seeding code
   from MinimalPumpServer/Program.cs and deleted
   MinimalPumpServer/SoftwareUpdate/ and its corresponding test.

3. **Implement model as library dependency spec/plan now**
   (Model/Opc.Ua.Di.NodeSet2.xml, PumpNodeManager.cs:113,
   SourceGeneratedNodeManagers.md:791/809). Removed the orphan
   Opc.Ua.Di.NodeSet2.xml from MinimalPumpServer (DI is already
   consumed as a project reference). Rewrote
   Docs/SourceGeneratedNodeManagers.md multi-model section to
   document source-generated library references as the **primary**
   mode and runtime XML loading as the fallback. Removed the
   'cross-namespace XmlPrefix' workaround documentation. Added
   plans/SourceGeneratedCompanionSpecLibraries.md with the
   spec/plan for converting Machinery + Pumps to source-generated
   libraries (mirroring Opc.Ua.Di), including the two source-
   generator gaps that must be fixed first.

4. **Fix this / Needs still work** (PumpNodeManager.Configure.cs:46-51).
   Simplified the [NodeManager] explanation block — removed the
   G9 workaround narrative (G9 has been fixed); the class is
   hand-written for clarity, not as a workaround.

5. **Use With instead of Wire everywhere** (Configure.cs:92).
   Renamed WireIdentification / WireMeasurements / WireActuation /
   WireSignals / WireSupervision / WireMaintenance to
   With* throughout PumpNodeManager.Configure.cs.

6. **TryAdd instead of TryWire** (Configure.cs:133). Renamed
   TryWireVariable / TryWireVariableReadWrite / TryWireMeasurement /
   TryWireSupervisionEdge to TryAdd*.

7. **Need to use source generated code here** (PumpNodeManager.cs:113).
   Rewrote LoadPredefinedNodesAsync comment to describe the
   library-first model and point at the
   SourceGeneratedCompanionSpecLibraries.md plan instead of the
   global::DI.* workaround narrative.

8. **Needs fixing** (README.md:77). Rewrote the architecture section:
   removed the cross-namespace 'wrong namespace prefix' explanation,
   replaced with a forward-looking reference to the new plan doc.
   Updated method-name references in the feature table to match the
   With*/TryAdd* renames.

9. **Consider IAsyncEnumerable for all 3 api** (ClientHelpers.md:66).
   Changed DiTopologyClient.EnumerateDevicesAsync /
   EnumerateNetworksAsync / EnumerateChildrenAsync from
   ValueTask<IReadOnlyList<TopologyEntry>> to IAsyncEnumerable<TopologyEntry>.
   Production: switched to �sync IAsyncEnumerable + yield. Tests
   updated to use �wait foreach (via a CollectAsync helper).

Verification (net10.0):
- Full UA.slnx build: 0 errors, 0 warnings (CA1014 unchanged).
- 163/163 Opc.Ua.Di.Tests pass.
- 9/9 Opc.Ua.Pumps.Tests pass (3 SoftwareUpdateSeeder tests removed
  with the seeder).
- 215/215 Opc.Ua.Server.Tests Fluent category pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
marcschier added a commit that referenced this pull request May 28, 2026
Materialises the OPC 10000-100 §10.3 SoftwareUpdateType address-space
surface under a DI device via a one-line fluent extension and wires
its method handlers to user callbacks (with library-supplied
'succeed immediately' defaults so the sample is runnable out of the
box).

Fluent surface (Libraries/Opc.Ua.Di.Server/Builders/):
  device.WithSoftwareUpdate(packageStore, su => su
      .UsePackageLoading() | .UseDirectLoading() | .UseCachedLoading()
      .WithSoftwareFolder(folder)
      .OnPrepare(...).OnInstall(...).OnConfirm(...).OnUninstall(...));

Address-space layout created under the device:
  SoftwareUpdate (SoftwareUpdateType, OPC-DI i=1)
    ├─ Loading           : Package|Direct|CachedLoadingType (default Package)
    ├─ PrepareForUpdate  : Prepare / Abort / Resume methods
    ├─ Installation      : InstallSoftwarePackage / InstallFiles /
    │                       Uninstall / Resume methods
    ├─ PowerCycle        : (FiniteStateMachine, no method bindings)
    └─ Confirmation      : Confirm method

Wiring details (SoftwareUpdateFacetWiring.cs):
- Uses the source-generated CreateInstanceOf*Type extensions on
  ISystemContext to create the typed state nodes.
- The factory only auto-populates a subset of the children
  (CurrentState + Prepare/Abort/Confirm/Resume). The remaining
  methods (InstallSoftwarePackage / InstallFiles / Uninstall /
  PrepareForUpdate.Resume) are materialised explicitly via
  CreateInstanceOf* method factories and added as HasComponent
  children. Each new node gets a per-instance NodeId from the
  manager's INodeIdFactory and has ModellingRuleId cleared.
- Method handlers use the async OnCallMethod2Async delegate so the
  user callback runs without sync-over-async.
- Default install handler records the supplied SoftwareRevision
  into the per-device ISoftwareFolder (auto-registered
  MemorySoftwareFolder unless overridden via WithSoftwareFolder).
- All catch blocks translate exceptions into ServiceResult so a
  buggy callback never crashes the server's method dispatcher.

Sample (Applications/MinimalSoftwareUpdateServer/Program.cs):
- Updated to call .WithSoftwareUpdate(store, su => su.UsePackageLoading())
  so a client connecting to the sample now actually sees the SU
  subtree on UpdateableDevice #1 instead of a bare DI device.

Tests (Tests/Opc.Ua.Di.Tests/DeviceBuilderSoftwareUpdateTests.cs):
- WithSoftwareUpdateAttachesSoftwareUpdateSubtree
- WithSoftwareUpdateMaterialisesAllStateMachineChildren
- UseDirectLoadingSwapsLoadingSubtype
- UseCachedLoadingSwapsLoadingSubtype
- OnPrepareCallbackFiresOnPrepareForUpdate
- DefaultOnInstallRecordsVersionInFolder

Docs (Docs/SoftwareUpdate.md):
- New end-to-end document covering the address-space layout, the
  fluent builder surface, callback signatures, storage abstractions,
  client side, the MinimalSoftwareUpdateServer walkthrough, and
  spec references.

Validation:
- Opc.Ua.Di.Server net10.0/net9.0/net8.0/netstandard2.1/net48/net472:
  0 Warning(s) 0 Error(s).
- UA.slnx Debug build: 0 Warning(s) 0 Error(s).
- Opc.Ua.Di.Tests (net10.0): 265/265 passed (the previous 259 plus
  6 new SU tests).

Follow-ups deferred for a later iteration:
- File-transfer pipeline binding: GenerateFileForWrite +
  CloseAndCommit currently delegate to the typed method handlers
  without streaming the binary into ISoftwarePackageStore.AddAsync.
- Client helpers: SoftwareUpdateClient currently exposes only
  ReadSoftwareVersionAsync; UploadPackageAsync / PrepareAsync /
  InstallAsync / ConfirmAsync convenience wrappers will follow.
marcschier added a commit that referenced this pull request Jun 1, 2026
…eviceSet

Bug: Pump #1 was referenced in PumpNodeManager.Configure.cs via
  builder.Node("Pumps/Pump #1/...") but never created anywhere.
  Every wiring call threw BadNodeIdUnknown which was silently swallowed by
  try/catch blocks, so the entire fluent simulation pipeline was dead code.
  Only Pump #2 (declared via ConfigureDevicesFor in Program.cs) actually
  existed, with no measurements / supervision / simulation.

Fix: in PumpNodeManager.CreateAddressSpaceAsync, after the base call but
  before the fluent Configure step, materialise a typed PumpState
  (OPC 40223 PumpType) named "Pump #1" as a child of the DI DeviceSet.
  Uses the source-generated CreateInstanceOfPumpType factory + the
  manager's NodeIdFactory + AddPredefinedNodeAsync.

  Then strip the "Pumps/" prefix from every browse path in Configure.cs
  (Pumps/Pump #1/... -> Pump #1/...) so the fluent root resolver finds
  the freshly-created device by browse name in PredefinedNodes.

  Pump #2 stays as-is in Program.cs, demonstrating the parallel DI
  declarative path (CreateDeviceAsync + WithIdentification).

README: update browse instruction to Objects > DeviceSet > Pump #1.
  Rewrite the "Add a second pump" section to document both patterns
  (hand-rolled CreateInstanceOfPumpType vs DI ConfigureDevicesFor).

Verified locally: server starts on port 62599 and logs
  Materialised 'Pump #1' (PumpType) under DeviceSet, NodeId=ns=4;s=5001_Pump #1.
  followed by PumpNodeManager: address space ready (10140 predefined nodes).
  No skipping/path-not-found messages remain.
marcschier added a commit that referenced this pull request Jun 1, 2026
Resolves PR feedback threads 3333380238 / 3333396295 (DeviceHealth
simulation): the pump sample exercised supervision flags
(cavitation / motor-overheat) but never reflected them on the DI
DeviceHealth variable that NAMUR NE 107 clients expect to observe.

Important model nuance: PumpType inherits from TopologyElementType
(not DeviceType), so PumpState does NOT carry DeviceHealth.
Pump #1 stays unchanged. Instead the simulation drives the
DeviceHealth variable on Pump #2 (a declarative DeviceState device
created via ConfigureDevicesFor), reusing the source-generator
AddDeviceHealth() helper.

Manager-side (PumpDeviceIntegrationServer/PumpNodeManager.cs):
* New 'public void RegisterSupervisedDeviceHealth(
    BaseDataVariableState<DeviceHealthEnumeration>?)' to attach an
  externally-materialised health variable to the simulation loop.
* New supervised-health field on PumpNodeManager.Configure.cs
  (alongside the existing simulation state).
* New 'public static DeviceHealthEnumeration
    MapSupervisionToDeviceHealth(bool cavitation, bool motorOverheat)'
  pure helper applying NAMUR NE 107 severity ordering:
  FAILURE > MAINTENANCE_REQUIRED > NORMAL. Motor overheat wins
  when both flags are active.
* AdvanceSimulation now calls UpdateDeviceHealth() each tick which
  ClearChangeMasks-es the registered variable on transition so
  subscriptions receive the NORMAL ↔ MAINTENANCE_REQUIRED ↔ FAILURE
  flow.

Program.cs:
* After WithIdentification on Pump #2, calls pump.Device.AddDeviceHealth
  (source-gen helper), pump.WithDeviceHealth(NORMAL) to seed the
  value, and casts ctx.Manager to PumpNodeManager to register the
  variable with the simulation loop.

Tests/Opc.Ua.Di.Tests/PumpDeviceHealthMappingTests.cs (new):
* 4 unit tests on the pure mapping helper covering NORMAL,
  MAINTENANCE_REQUIRED (cavitation-only), FAILURE (motor-overheat-
  only), and FAILURE-wins (both flags) severity ordering.

Validation:
* dotnet build PumpDeviceIntegrationServer + Opc.Ua.Di.Tests
  (net10.0): 0 errors.
* dotnet vstest TestCategory=Pumps|Hosting: 48/48 pass (was 44; the
  4 new mapping tests join the suite).
* Smoke test: server boots clean on
  opc.tcp://localhost:62612/PumpDeviceIntegrationServer with the
  RegisterSupervisedDeviceHealth wiring active and no warnings about
  missing DeviceHealth child.
marcschier added a commit that referenced this pull request Jun 1, 2026
Partial response to PR feedback threads 3333309679 / 3333300092 /
3333312536 / 3333325207 / 3333298144 (typed-node API + remove try/catch).
The full typed-accessor generator (.Components() / .Properties()
split per the user's preference) remains a follow-up; this commit
delivers the building block that makes those typed accessors viable
and addresses one of the two reasons callers had to wrap fluent
wiring in try/catch (namespace mismatch on inherited type children).

Problem:
Browse paths like 'Pump #1/Operational/Measurements/FluidTemperature'
fail strict resolution because PumpType inherits from
MachineComponentType, so Operational/Measurements carry the
Machinery namespace (ns=3) — not the Pumps namespace (ns=2) that
the resolver uses for unqualified segments. Authors are forced to
sprinkle 'ns=N;' prefixes on every cross-namespace segment or wrap
each builder call in a try/catch to swallow BadNodeIdUnknown.

Fix (Libraries/Opc.Ua.Server/Fluent/BrowsePathResolver.cs):
* When the exact-namespace FindChild lookup fails on a non-root
  segment, fall back to a single GetChildren scan that matches by
  local name only. Returns the lone match.
* If two or more children share the same local name in different
  namespaces, throw BadBrowseNameDuplicated with a message
  instructing the caller to disambiguate with an explicit ns=N;
  prefix. Bad ergonomics never silently picks the wrong child.
* Root-segment lookup is intentionally NOT changed — the root
  resolver is namespace-aware and ambiguity at the root would
  silently match the wrong manager.

Tests (Tests/Opc.Ua.Server.Tests/Fluent/BrowsePathResolverTests.cs):
* ResolveFallsBackToLocalNameForCrossNamespaceChild — exercises
  the OPC 40223 pump pattern (root in ns=2, child in ns=3).
* ResolveAmbiguousLocalNameThrowsBadBrowseNameDuplicated — covers
  the explicit-disambiguation contract.
* ResolveExactNamespaceMatchSkipsFallback — confirms an explicit
  ns=N; prefix picks the right child without engaging the fallback
  (so ambiguous names don't surface a false-positive duplicate).

Sample (Applications/PumpDeviceIntegrationServer/PumpNodeManager.Configure.cs):
* Pump wiring keeps the existing try/catch blocks. The other reason
  callers needed them — optional children like Operational not
  being materialised by CreateInstanceOfPumpType — is a deeper
  source-generator gap (the factory only materialises mandatory
  children of the declared type) that the upcoming typed-accessor
  generator will fix via lazy AddXxx() helpers per child.

Validation:
* dotnet build PumpDeviceIntegrationServer + Opc.Ua.Server.Tests +
  Opc.Ua.Di.Tests (net10.0): 0 errors.
* dotnet vstest BrowsePathResolverTests: 21/21 pass (was 17, with
  4 new cross-namespace fixtures).
* dotnet vstest TestCategory=Pumps|Hosting: 48/48 pass.
* dotnet vstest TestCategory=Fluent|NodeManager: 245/245 pass.

Follow-up (FB-3 phase 2):
* Source-generator change emitting IComponentAccessor<T> /
  IPropertyAccessor<T> typed-child accessors with .Components() /
  .Properties() split (per user-confirmed design).
* AddXxx() lazy-materialisation helpers on each generated *State
  class so optional children (PumpType.Operational, etc.) auto-
  instantiate on first builder access.
* Sample rewrite to use the typed accessors + drop the remaining
  try/catch blocks.
marcschier added a commit that referenced this pull request Jun 2, 2026
Address 7 of the 16 unresolved review comments from round 3:

- #6 Tools/Opc.Ua.SourceGeneration/ModelCompilation.cs:300 - add
  `using System.Collections.Generic` and
  `using Opc.Ua.SourceGeneration.Dependency` so the file no longer
  spells fully-qualified type names inline. Same sweep for the
  `System.Collections.Generic.`/`Opc.Ua.SourceGeneration.Dependency.`
  prefixes at other call sites in the file.
- #7 Tools/Opc.Ua.SourceGeneration.Core/Generators.cs:634 (and 3 other
  sites in FluentBuilderGenerator + GeneratorOptions + ModelCompilationOptions)
  - drop `FB-3 phase 3` / phase / track references from comments and
  xmldoc; describe the feature, not the rollout.
- #9 Libraries/Opc.Ua.Di.Client/Hosting/OpcUaClientDiBuilderExtensions.cs:69
  - re-wrap the long `<item><description>...</description></item>` doc
  list to fit inside the 140-char editorconfig limit.
- #12 Strip `global::` qualification from all hand-written PR code
  (Libraries/Opc.Ua.Di.*, Applications/PumpDeviceIntegrationServer).
  Only kept where required to disambiguate (test-side `Pumps.PumpNodeManager`
  / `Pumps.PumpNodeManagerFactory` collide with the source-generated
  `Opc.Ua.Pumps` namespace; restored `global::` on those refs).
  Hand-written code now matches the .editorconfig convention of letting
  `using` directives carry the namespace burden; source-generated code
  still uses `global::` (unchanged).
- #15 Remove `// -----` / `// =====` ASCII-banner `#region`-style
  comments from SoftwareUpdateClient.StateMachine.cs,
  SoftwareUpdateClient.Upload.cs, SoftwareUpdateFacetWiring.cs and
  SoftwareUpdateFileTransferManager.cs. Inline section comments
  remain unchanged.
- #16 Expand 344 one-line `/// <summary>text</summary>` declarations
  across 72 files in Applications/, Libraries/, Stack/, Tests/ and
  Tools/ into the three-line
  `/// <summary>\n/// text\n/// </summary>` form.
- Carry-over from FB-3 phase 3: while sweeping `global::` also
  un-qualify the hand-written Pumps server callsites
  (Pump #1 wiring in PumpNodeManager.cs / PumpNodeManager.Configure.cs)
  so the new code matches the reviewer's convention.

Validation:
- `dotnet build UA.slnx` — clean (0 errors, 15 unrelated warnings
  reported transiently by the build engine).
- `Opc.Ua.SourceGeneration.Core.Tests` — 3535 passed / 8 skipped.
- `Opc.Ua.Di.Tests` (Pump + DI client + SoftwareUpdate filter) —
  85/85 passed.

The remaining 9 round-3 items (architecture relocation #8,
`=>`→`{}` body sweep #10, ObjectType-proxy refactor #11/#14,
brace-formatting sweep #13, pump server Configure refactor #1/#2,
DeviceHealth + functional-group exposure #3/#5 plus DI doc
disambiguation #4) are tracked separately and will land in
subsequent commits.
marcschier pushed a commit that referenced this pull request Jun 2, 2026
Mirror the sentinel-keep-alive pattern from the stable sister test

ProcessReceivedMessagesAsyncShouldProcessMessagesInOrderAsync so the

worker is parked inside the keep-alive dispatch before any data

messages are queued. Without the sentinel, a fast consumer could read

the first arrival (e.g. #3) before the rest queued, leaving the

priority channel nothing to re-order; later #1/#2 arrivals were then

dropped as stale and the test would observe [2,3,4,5] instead of

[1,2,3,4,5].

Also updated the XML remark to accurately describe the

priority-channel guarantee (re-orders items present at the same read,

not 'any burst regardless of consumer speed') and asserted on the

sentinel + reordered tail separately.
marcschier added a commit that referenced this pull request Jun 2, 2026
…uilder + ad-hoc functional group

Addresses 3 PR review threads from round 3:

- FB-R3-1 (PumpNodeManager.cs:133) — split OnAddressSpaceReadyAsync
  into two clearly-named phases: an async ConfigureInstancesAsync(ct)
  that materialises predefined instances (currently Pump #1) and a
  synchronous fluent Configure(builder) pass that wires callbacks.
  CreatePumpInstanceAsync renamed to MaterialisePumpInstanceAsync to
  match the new phase boundary. The reviewer's suggestion to use the
  DiNodeManager.CreateDeviceAsync<TDevice> helper does not apply here
  because PumpType in OPC 40223 derives from MachineType (Machinery),
  not from the DI ComponentType hierarchy that CreateDeviceAsync's
  generic constraint requires — documented in the xmldoc on the new
  ConfigureInstancesAsync partial.
- FB-R3-2 (PumpNodeManager.cs:150) — collapse the six-arg
  `new NodeManagerBuilder(SystemContext, this, nsIndex, lookup, lookup, lookup)` +
  `AttachToBuilder(builder)` + `Configure(builder)` + `builder.Seal()`
  quadruple to a single fluent chain. New
  `FluentNodeManagerBase.CreateFluentBuilder(ushort nsIndex)` helper
  constructs the builder, wires the three default PredefinedNodes
  resolvers, and attaches the event-source + simulation registries.
  New `NodeManagerBuilder.Configure(Action<INodeManagerBuilder>)`
  extension preserves chaining. Pump callsite now reads
  `this.CreateFluentBuilder(nsIndex).Configure(Configure).Seal();`.
- FB-R3-5 (DeviceIntegration.md:230) — demonstrate the non-typed
  `WithFunctionalGroup(QualifiedName)` builder on Pump #2 with an
  ad-hoc `Diagnostics` functional group that surfaces LastError /
  ErrorCount / LastSelfTest as plain properties. Doc snippet appended
  to Docs/DeviceIntegration.md §'Functional groups'.

New public surface in Opc.Ua.Server.Fluent:
- FluentNodeManagerBase.CreateFluentBuilder(ushort nsIndex)
- FluentNodeManagerBuilderExtensions.Configure(NodeManagerBuilder, Action<INodeManagerBuilder>)

Validation:
- Full build: 0 errors, 0 warnings (server + pump server + tests).
- Tests/Opc.Ua.Di.Tests pump suite — 14/14 passing.
- Tests/Opc.Ua.Server.Tests fluent suite — 247/247 passing.
marcschier added a commit that referenced this pull request Jun 5, 2026
- Move all channel-related types into `Stack/Opc.Ua.Core/Stack/Client/Channels/`
  subfolder (#13).
- Un-obsolete `SessionReconnectHandler` and the SessionExtensions
  `ReconnectAsync(connection, ct)` / `(channel, ct)` extensions; remove
  all SRH-related `#pragma warning disable CS0618` suppressions from
  applications and tests. AttachChannel/DetachChannel remain obsolete (#1, #4).
- Remove the shared-budget section from MigrationGuide.md (#3).
- Mark `ManagedSession.AttachChannel`/`DetachChannel` `[Obsolete]` so the
  warning surfaces all the way up the supported API surface (#9).
- Break the `<see cref=…>` line in `Session.ChannelManager.cs` to stay under
  140 chars (#10).
- Move `ChannelManagerSessionFactory` from `Opc.Ua.Gds.Client.Common` into
  `Opc.Ua.Client` as a public, documented session-factory option (#11).
- Audit other client classes for `IClientChannelManager`-aware overloads;
  add `ISessionFactory`-accepting overloads on `GlobalDiscoveryServerClient`,
  `LocalDiscoveryServerClient`, `ServerPushConfigurationClient` so any
  session factory (including `ChannelManagerSessionFactory`) works (#12).
- Refactor MCP server (`OpcUaSessionManager`, `Program`) to use
  `ManagedSession` + DI-resolved `IClientChannelManager`; remove
  `SessionReconnectHandler` and manual keep-alive reconnect (#2).
- ConnectionStateMachine code-style fixes: use named delegate types,
  collapse multi-line callback properties to single lines, swap
  `timeProvider`/`maxTotalReconnectTime` constructor param order,
  fix multi-line declaration (#6, #7, #8).
- HTTPS resilience: confirmed `Microsoft.Extensions.Http` and
  `Microsoft.Extensions.Http.Resilience` 10.6.0 support
  `net472`/`net48`/`netstandard2.1`; removed `#if NET8_0_OR_GREATER`
  gating from `ManagedSessionBuilder` and `OpcUaClientBuilderExtensions`
  and HTTPS csproj package refs (#5).

Verification:
- `Opc.Ua.Client` + `Opc.Ua.Core` + `Opc.Ua.Gds.Client.Common` build
  clean (0 warnings, 0 errors).
- 56/56 core channel-manager / retry-budget / HTTPS factory tests pass.
- 129/129 client (ManagedSession + legacy SRH back-compat + SessionExtensions)
  tests pass.
marcschier added a commit that referenced this pull request Jun 11, 2026
Phase 1 (security-agent MCP, svc 59eec07a-...) + Phase 2 (security-
review SAST sub-agent on the PR diff) + Phase 3 (deep-dive) findings
on branch `vsubs`. All CRITICAL/HIGH/MEDIUM findings fixed in-line;
the MCP secrets-usage, code-integrity, policy-governance, and tls
scans report no PR-relevant items.

CRITICAL #1 — Unbounded partition fan-out under hostile server
---------------------------------------------------------------
A hostile or buggy server returning `Bad_TooManyMonitoredItems`
on every `CreateMonitoredItems` reply caused the wrapper to mint
a fresh server-side `Subscription` per `TryAdd`, growing client
memory and server-side handle space without bound.
`MaxMonitoredItemsPerPartition = null` (the documented default)
made the per-partition cap unlimited, so the reactive fallback was
the only growth gate and it kept minting forever.

Fix: introduce `SubscriptionOptions.MaxPartitionCount` (default
`32`, comfortably above legit fan-out), wire it through
`PartitionPlacementPolicy`, and stop minting at the cap. The
composite collection returns the new
`PlacementDecision.RejectMaxPartitionCountReached` state and
`TryAdd` returns `false` — the caller observes the standard
add-failed path.

HIGH #2 — Snapshot consistency across LogicalGroupId not validated
-------------------------------------------------------------------
`ManagedSessionExtensions.ValidateAndSortGroup` checked only
`PartitionIndex` contiguity. Two snapshots crafted to share a
`LogicalGroupId` but disagree on durability / publishing
interval / lifetime would be merged into one logical wrapper
with inconsistent partitions — letting a corrupted or attacker-
controlled snapshot file smuggle non-durable behaviour into a
durable subscription (or vice versa) at restore time.

Fix: extend `ValidateAndSortGroup` to assert all snapshots in
the group agree on every subscription-wide option
(`PublishingIntervalMs`, `KeepAliveCount`, `LifetimeCount`,
`MaxNotificationsPerPublish`, `MinLifetimeIntervalMs`,
`Priority`, `PublishingEnabled`, `Disabled`,
`SendInitialValuesOnTransfer`). Mismatches throw
`BadDecodingError` with a descriptive message.

MEDIUM #3 — Restore-path secondaries bypass forwarding handler
---------------------------------------------------------------
`SubscriptionManager.AttachSecondaryPartitionAsync` (snapshot-
restore fan-out path) passed the raw user handler directly to
`MintPreloadedPartition` instead of the wrapper's
`PartitionForwardingHandler`. This broke the documented
single-threaded dispatch contract for restored multi-partition
subscriptions (per `Docs/UnboundedSubscriptions.md`): the
serialising semaphore was bypassed and the handler's
`subscription` parameter pointed at the partition instead of
the logical wrapper. Application handlers that mutate non-thread-
safe state could silently race after restore.

Fix: expose `LogicalSubscription.ForwardingHandler` as an
internal accessor; `AttachSecondaryPartitionAsync` now uses the
wrapper's existing forwarding handler when present (falling back
to the raw user handler only when the wrapper was constructed in
single-partition fast-path mode, i.e. `DisableUnboundedItemMode
= true`).

Validation (net10.0)
--------------------
* `Opc.Ua.Client.csproj` builds clean (0 warnings, 0 errors).
* `Opc.Ua.Client.Tests` V2 subscription unit tests: **43 / 43**.
* `Opc.Ua.Subscriptions.Tests.UnboundedSubscriptionTests`
  integration tests: **3 / 3** (1 durable-only skipped).
* `Opc.Ua.Subscriptions.Durable.Tests.*V2Async` integration
  tests: **6 / 6**.

Findings dismissed
------------------
* Affinity dictionary growth (`PartitionPlacementPolicy`) —
  uses `StringComparer.Ordinal` so no CVE-2022-29117-class
  culture sensitivity; growth is application-controlled, not
  attacker-controlled.
* Idle-timer race vs `DisposeAsync` — race window exists but
  worst observable outcome is an `ObjectDisposedException`
  swallowed by the disposer's `try/catch`. No attacker trigger
  and no integrity/confidentiality loss.
* Cross-wrapper notification routing — each `Add` allocates its
  own `PartitionForwardingHandler`; `BindLogical` rejects
  re-binding; routing keys are partition server-id → partition's
  own handler; no structural path for partition of wrapper B to
  reach wrapper A's handler.
* OPC UA spec compliance — `SetTriggering` enforces same-
  partition (Part 4 §5.13.6); durable hook ordering (Part 4
  §5.13.9) is uniform across primary and secondaries.
marcschier added a commit that referenced this pull request Jun 11, 2026
…t doc (audit follow-up #2)

Expanded the Certificates fuzz corpus from 9 seeds to 21 (+ 12) across
all five categories. The security assessment doc flagged the corpus as
"adequate but limited diversity" -- this commit closes the gap.

Generator (Fuzzing/Opc.Ua.Certificates.Fuzz.Tools/Certificates.Testcases.cs)
now produces, in addition to the original RSA application cert / CRL /
CSR / PEM / extension seeds:

  X509Cert/   certificate-expired.der        RSA cert with NotAfter in past
              certificate-selfsigned.der     RSA cert with no separate issuer
              certificate-ecc.der            P-256 ECC cert with ECC issuer

  X509CRL/    crl-multi-revoked.der          CRL listing 3 revoked entries
              crl-empty.der                  CRL with no revoked entries

  X509CSR/    request-ecc.der                ECC-signed PKCS#10 CSR
              request-multi-san.der          CSR with 4 SAN entries

  PEM/        certificate-ecc.pem            ECC PEM cert
              private-key-ecc.pem            ECC PEM private key

  ASN1/       nested-sequence.der            SEQUENCE containing inner SEQUENCE + OCTET STRING
              large-integer.der              256-bit BigInteger (past AsnReader fast paths)
              subject-alt-name-many.der      SAN with 9 entries (5 DNS + 4 IP)

Exercises code paths the original corpus missed:
- ECDsa decode branches in cert + PEM + PKCS#10 parsers
- CRL revoked-list loop with multiple entries + empty-list boundary
- Validity-window logic for expired cert
- Self-signed (no chain) decode path
- Multi-entry SAN decode loop
- AsnReader.ReadInteger multi-limb branch (past 32 / 64-bit fast paths)
- Nested OCTET STRING containing further DER content

A dedicated ECC issuer ("CN=Fuzzing Test ECC Root") is constructed
because CertificateBuilder.CreateForECDsa() signs with the issuer's
ECDsa private key, so an RSA-only issuer is rejected.

Verified (net10.0):
  - Opc.Ua.Certificates.Fuzz.Tests: 460 passed (up from 220), 0 failed.
    The doubling reflects the 12 new seeds * the harness's
    Fuzz*Assets/FuzzGoodTestcases theory expansion across the 20 cert
    targets.
  - Opc.Ua.Encoders.Fuzz.Tests baseline: 4131/4131 passed.
  - Opc.Ua.Network.Fuzz.Tests baseline: 330/330 passed.
  - Combined: 4921 passed, 0 failed.

DER seed files are committed via `git add -f` because the repo-wide
`*.der` gitignore would otherwise exclude them (matches the original
F2 commit's precedent of force-adding cert test seeds).

Side effect: deleted plans/security-assessment.md. The doc was an
untracked working-copy audit summary that has now had every item
addressed:
  - 7.1 #1 (MUST FIX, parser bound): commit 7744146
  - 7.2 #2 (corpus diversity):       this commit
  - 7.2 #3 (IOORE whitelist):        commit 80832f1
  - 7.3 future work:                 explicitly out-of-scope, tracked
                                     in the PR description instead.

Audit ref: section 7.2 #2 of the security assessment doc.
marcschier added a commit that referenced this pull request Jun 11, 2026
CI on PR #3869 was green for all functional tests after re-running
the 4 flaky failures (aot-windows `BrowseAndReadComplexTypesAsync`,
test-ubuntu-Subscriptions.Durable `DurableSubscriptionSurvivesSession
CloseV2Async`, test-windows-History
`NonExclusiveLimitAlarmActiveNormalCycleAsync`, test-windows-PubSub),
all confirmed unrelated to branch changes (also fail on master CI).

The only remaining gate was `codecov/patch` reporting 68.23% of
diff hit against an 80% target (with 5pp tolerance ⇒ 75% effective
threshold). Closes the gap by adding focused unit tests on the
lowest-coverage new code paths from the security commit (#3869
`c6997689`):

* `PartitionPlacementPolicyTests` (+5 tests):
  - `ConstructorTreatsZeroMaxPartitionCountAsUnbounded`
  - `ConstructorPreservesExplicitMaxPartitionCount`
  - `DecideRejectsOnceMaxPartitionCountReached`
  - `DecideAllowsExistingPartitionEvenAtMaxPartitionCount`
  - `DecideAllowsMintUntilMaxPartitionCount`
  Cover the CRITICAL #1 DoS guard end-to-end: the
  `maxPartitionCount` constructor parameter, the new
  `MaxPartitionCount` accessor, and the
  `RejectMaxPartitionCountReached` decision path.

* `SnapshotGroupValidationTests` (NEW file, 14 tests):
  - `ValidateAndSortGroupSortsByPartitionIndex`
  - `ValidateAndSortGroupAcceptsSinglePartition`
  - `ValidateAndSortGroupRejectsDuplicatePartitionIndex`
  - `ValidateAndSortGroupRejectsPartitionIndexGap`
  - 8 individual mismatch checks (one per option:
    `PublishingIntervalMs`, `KeepAliveCount`, `LifetimeCount`,
    `MaxNotificationsPerPublish`, `MinLifetimeIntervalMs`,
    `Priority`, `PublishingEnabled`, `Disabled`,
    `SendInitialValuesOnTransfer`)
  - `ValidateAndSortGroupAcceptsAllOptionsMatching`
  Cover every branch of the HIGH #2 snapshot-trust guard.
  `ManagedSessionExtensions.ValidateAndSortGroup` promoted from
  `private static` to `internal static` so it is reachable
  from `Opc.Ua.Client.Tests` (existing `InternalsVisibleTo`).

Validation (net10.0):
* `Opc.Ua.Client` builds clean.
* `Opc.Ua.Client.Tests` V2 subscription unit tests: **62 / 62**
  (previous 43 + 5 new placement policy + 14 new snapshot
  validation).

Re-runs:
* The 4 flaky tests above all PASS on re-run with no code change
  to them, confirming pre-existing CI flakiness (also observed on
  master in commits `4d613d11` and `a81d9817`).
marcschier pushed a commit that referenced this pull request Jun 12, 2026
…anager.DoSimulation

DoSimulation can race with Dispose() on shutdown: the simulation timer fires
and gets past the m_simulationEnabled guard, then Dispose() runs (which
intentionally does NOT wait for in-flight timer callbacks - see the comment
on Dispose, lines 79-87) and disposes m_semaphore while DoSimulation is
about to call m_semaphore.Wait(). The resulting ObjectDisposedException is
caught by the outer try/catch and logged at LogLevel.Error as "Unexpected
error doing simulation", producing misleading noise in every CI test log
that disposes a ReferenceNodeManager.

The race itself is acknowledged and accepted in the Dispose() comment as a
better trade-off than blocking Dispose on a Timer.Dispose(WaitHandle). The
log entry is the only problem: it labels an expected, documented teardown
race as an unexpected error.

Filter ObjectDisposedException specifically, gated on
`m_simulationTimer is null` so the suppression only kicks in once Dispose
has actually run. A genuine premature semaphore-disposal bug elsewhere
would still surface loudly via the generic catch.

Observed in build 14598 (mac PR build for #3868), History.Tests fixture
teardown for AlarmClientIntegrationTests:
  SERVER 04:54:05.298 [Quickstarts.ReferenceServer.ReferenceNodeManager]
    Unexpected error doing simulation #1.
  [ObjectDisposedException] Cannot access a disposed object.
  Object name: 'System.Threading.SemaphoreSlim'.
marcschier added a commit that referenced this pull request Jun 22, 2026
…, docs merge

#4 (csproj): McpServer now needs only the Opc.Ua.PubSub.Diagnostics project
reference; it transitively brings PubSub/Udp/Mqtt. Removed the 3 redundant
direct refs.

#3 (architecture): replace the in-transport capture taps with a DI-injected
capturing decorator so the UDP/MQTT transports carry NO capture code (mirrors
the UA-SC CapturingMessageSocket):
- New CapturingPubSubTransport (wraps IPubSubTransport, taps send/receive to the
  observer, delegates the rest) + CapturingPubSubTransportFactory.
- AddPubSubPcap() decorates every registered IPubSubTransportFactory; capture is
  injected only when DI-configured (call after AddUdpTransport/AddMqttTransport).
- Reverted the S1 taps from UdpDatagramTransport/MqttBrokerTransport, their
  factories, and core AddPubSub; moved the capture seam types
  (IPubSubCaptureObserver/Registry, PubSubCaptureContext/Direction) into the
  Diagnostics lib (namespace Opc.Ua.PubSub.Pcap). Moved the seam test + added a
  decorator test. McpServer PubSubRuntimeManager updated to the reverted factory.

#1, #2 (docs): merged Docs/PubSubDiagnostics.md into Docs/Diagnostics.md as
section 5 (with the decorator architecture); the in-doc MCP-tools section now
links to McpServer.md, which gained a PubSub tool catalogue. Deleted the
standalone file; updated README link + UA.slnx.

Builds: diagnostics lib net8/9/10 0/0, MCP net10 0/0, core PubSub net48 0/0.
Tests: diagnostics 48 (incl. decorator), PubSub 1058, no regressions.
marcschier added a commit that referenced this pull request Jul 18, 2026
Implements the spec's pluggable per-format SchemaId fingerprint provider (Schema
Registry SS6.6) and surfaces SchemaIdAlg (review findings #1/#4):
- New ISchemaIdProvider + SchemaIdProviders static registry (Opc.Ua.Types) with the
  built-in Avro (CRC-64-AVRO), Arrow (SHA-256/ApacheArrow) and JSON (SHA-256/JCS)
  providers pre-registered; a host can Register additional providers for any format.
- SchemaCache.ComputeSchemaId now delegates to the registry (identical fingerprint
  bytes for the built-ins, so the handshake/round-trip stay byte-compatible).
- SchemaCacheEntry gains a computed SchemaIdAlg (derived from Format via the registry),
  additive - no constructor change.

Verified net8.0 (also compiles netstandard2.0): PubSub.Tests 1379/0 (1 skipped),
Build 0 warnings / 0 errors.
marcschier added a commit that referenced this pull request Jul 30, 2026
…4117)

# Description

A live OPC UA compliance audit of `samples/PumpDeviceIntegrationServer`
against the **OPC 40223 Pumps** and **OPC 10000-100 (DI)** companion
specifications found **13 address-space violations** plus a
serialization bug in the MCP tool. Every fix lands at its **root cause
in the shared stack** rather than as a workaround in the sample, so all
servers benefit.

The sample now exposes **N fully simulated pumps** (`--pumps N`) and
advertises the DI conformance facets it actually satisfies.

> This branch has been merged with `master` including **#4116** ("Add
simulation to PumpDeviceIntegrationServer and Event modelling"), which
independently reworked the same pump simulation and the same fluent
alarm builders. See [Relationship to #4116](#relationship-to-4116) below
for how the two were reconciled.

## Findings and fixes

| # | Finding | Root cause | Fix |
|---|---------|-----------|-----|
| 1 | `OverTempAlarm` created by the fluent API did not exist in the
address space | `AttachAlarm` attached the alarm with `AddChild` but
never indexed it into the node manager | Register the node; the same gap
existed in `AddObject`, `CreateInstance` and the state-machine creators
|
| 2 | Alarms were unsubscribable — `EventNotifier = 0` everywhere, no
`HasNotifier` from `Server` | No event-source wiring at all | Set
`HasCondition`, initialise
`SourceNode`/`SourceName`/`ConditionName`/`InputNode`, promote
`EventNotifier` up the ancestor chain, publish `HasNotifier` from the
`Server` object |
| 3 | Every instance NodeId was minted in the **DI standard namespace**
(`ns=4;s=5001_Pump #1`) | `DiNodeManager.New` inherited the parent's
namespace index | New `DiNodeManager.InstanceNamespaceIndex`; instances
move to the server's application namespace |
| 4 | `Server/Namespaces` described only 2 of 6 namespaces | The
NodeSet2 importer drops nodes parented to namespace 0, so companion-spec
metadata objects never reached the address space — and no route covered
the server's own namespace | New `NamespaceMetadataPublisher` walks the
`NamespaceArray` and fills version/publication date from
`ModelDependencyAttribute` |
| 5 | Vendor BrowseNames (`Diagnostics`, `LastError`, …) sat in the DI
namespace | Builders defaulted to the parent's namespace | Default to
the server namespace; **spec-defined BrowseNames are unchanged** |
| 6 | `AccessLevel="5"` from the Pumps NodeSet was reported as `1` | The
ModelDesign `AccessLevel` enum is not `[Flags]` and cannot represent
`CurrentRead \| HistoryRead` | Carry the verbatim NodeSet2 bitmask and
emit named `AccessLevels` constants |
| 7 | `Historizing = true` while `HistoryRead` returned
`BadHistoryOperationUnsupported` | Nothing reconciled the declared
history surface with the wired historians | Startup clears the
advertisement (including masking the attribute read callbacks) when no
provider resolves; the sample wires a real historian |
| 8 | Mandatory `ProductInstanceUri` unset | Sample omission | Populated
on every pump |
| 9 | Mandatory `TrueState`/`FalseState` empty on every supervision
boolean | Absent in the official NodeSet; a server must still populate
them | Populated on every pump |
| 10 | Machinery `Machines` folder empty (OPC 40001-1 §9.2 says
**shall**) | `TryAddToMachinesFolder` used `AddChild`, which
*re-parents* the device away from `DeviceSet` | Add the `Organizes`
reference instead |
| 11 | BrowseName contained a space and `#` | Sample naming | `Pump_1`
with the readable label in `DisplayName` |
| 12 | `Maintenance` group materialised but empty | Sample omission |
Populated |
| 13 | Uninitialised values reported `Good` | No initial-value status |
`BadWaitingForInitialData` until the first simulation tick |
| — | `ServerProfileArray` advertised only `StandardUA2017` |
`DiNodeManager` did not implement the existing `IConformanceContributor`
| Contribute DI 1.05 server facets computed at runtime from what is
actually wired, merged with `StandardUA2017` |
| — | MCP tool serialised boolean `false` and numeric `0` as JSON `null`
| `Variant.Null` is `default`, so `Variant.Equals` adopts the non-null
type and compares against the default value | Dispatch on `BuiltInType`
and use typed `TryGetValue` accessors instead of the prohibited
`AsBoxedObject` |

## Relationship to #4116

#4116 landed on `master` while this branch was in flight and targeted
overlapping ground. The merge is a **union of both intents**, not a
fast-forward:

- **`AlarmBuilderExtensions`** — kept both sides. #4116 contributes the
parent-must-be-an-Object guard, `SetEnableState`, and the
`HasEventSource` references; this branch contributes the `HasCondition`
reference, condition source initialisation, **registration of the alarm
with the node manager so it is browsable**, notifier promotion up the
whole ancestor chain, and root-notifier registration.
- **`SupervisionBuilderExtensions`** — took #4116's `SetAlarmActive`. It
is more spec-correct than this branch's version: it honours
`EnabledState`, resets Acked/Confirmed on activation, and computes
`Retain` per OPC 10000-9 so an unacknowledged Condition stays retained
after going inactive. This branch's null guard was kept so a malformed
alarm cannot dereference a missing `EnabledState`.
- **`PumpNodeManager.Configure.cs`** — took #4116's **push-based**
simulation (`RegisterPumpSimulation`, `CreatePumpSimulation`,
`IValueUpdater<T>`, `Initialize`/`Advance`/`Publish`, deterministic
per-pump phase offsets) as the structure, then folded in this branch's
compliance work: configurable pump count, Maintenance group,
`TrueState`/`FalseState`, historian wiring, per-pump mandatory
Identification values, and `BadWaitingForInitialData` until the first
tick.
- **README** — union, keeping #4116's address-space validation workflow
section.

Two test adjustments were needed, both because the merge legitimately
changed behaviour the tests pinned:

- `PumpHostedReferenceTests` now resolves pump NodeIds from the browse
result instead of hard-coding the DI namespace, and waits for the first
published value. `PumpCreatedAfterStartupJoinsTheLiveSimulationAsync`
was **added** to restore #4116's coverage that a pump created after
startup through `ConfigureDevicesFor` joins the live simulation.
- `FluentAlarmRegistrationIntegrationTests` no longer asserts that
`Retain` clears when an alarm goes inactive — that pinned this branch's
weaker semantics, and #4116's Part 9 behaviour is correct.

## Breaking change

Runtime-created DI device instance NodeIds move from the companion-spec
namespace to the server's application namespace, e.g.
`ns=<DI>;s=5001_Pump #1` becomes `ns=<application>;s=5001_Pump #1`. This
invalidates NodeIds cached by clients from earlier 2.0 previews. It is
**not** a model change — standard DI/Machinery/Pumps type NodeIds and
spec-defined BrowseNames are untouched.

Device integration hosting is new in 2.0, so there is nothing to migrate
from 1.5.378 and this is deliberately *not* in `docs/MigrationGuide.md`.
Anyone tracking 2.0 previews should rediscover devices by browsing
`DeviceSet` rather than persisting NodeIds, and resolve namespace
indexes from `NamespaceArray` per connection.

## Verification

Compliance is pinned by regression tests that assert **through the
server's service surface** (`BrowseAsync` / `ReadAsync` /
`HistoryReadAsync`), not by inspecting `NodeState` objects in memory.
That distinction mattered: three defects survived in-memory assertions
and were only caught through the read path — most subtly, `Historizing`
was cleared while `AccessLevel` still advertised `HistoryRead`, because
attribute read callbacks re-applied the NodeSet bits.

Post-merge on net10.0:

| Suite | Result |
|---|---|
| `Opc.Ua.Di.Tests` | 326 passed |
| `Opc.Ua.Server.Tests` | 3846 passed, 5 skipped |
| `Opc.Ua.Tools.Tests` | 288 passed |
| `Opc.Ua.History.Tests` | 506 passed, 27 skipped |
| `Opc.Ua.SourceGeneration.Core.Tests` | 3722 passed, 8 skipped |

The full solution was also built and tested on **net48** before the
merge.

Review feedback from `copilot-pull-request-reviewer` has been addressed
in commit `badad31dc`: the shared `ServerObjectState` lock was removed
(the forward `HasNotifier` edge is published through the owning node
manager instead), MCP array serialization now preserves element types,
and the pump option guards report the specific rejected option. The only
failure is
`ConfigureApplicationBuildsSharedClientAndServerConfigurationAsync`, a
pre-existing certificate-store issue that **passes when run in
isolation** and is unrelated to this change.

## Documentation

Updated `docs/DeviceIntegration.md`, `docs/MigrationGuide.md`,
`docs/HistoricalAccess.md`, `docs/DeveloperGuide.md`,
`docs/McpServer.md`, `docs/README.md`,
`samples/PumpDeviceIntegrationServer/README.md` and
`tools/Opc.Ua.Mcp/README.md`, and consolidated the four node-manager
documents into a single [`docs/NodeManagers.md`](docs/NodeManagers.md) —
overview, built-in managers (including `MasterNodeManager`), core vs
custom, registration, server address-space metadata, and source
generation — with a TOC and all inbound links repointed.

## Related Issues

No tracking issue exists for this work yet — it originated from an
ad-hoc compliance audit of the pump sample. Given the size of the change
and the runtime-breaking NodeId move, please open (or link) a tracking
issue so the design is recorded as an ADR before merging.

- Fixes #

## Checklist

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

- [ ] I have signed the
[CLA](https://opcfoundation.org/license/cla/ContributorLicenseAgreementv1.0.pdf)
and read the
[CONTRIBUTING](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/CONTRIBUTING.md)
doc.
- [x] I have added tests that prove my fix is effective or that my
feature works and increased code coverage.
- [x] I have added all necessary documentation.
- [x] I have verified that my changes do not introduce (new) build or
analyzer warnings.
- [x] 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.

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 0d50c3fc-58b1-4c99-85a4-1e32c88666d9
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants