3 Commits
Author SHA1 Message Date
Marc Schier 5795e50b58 Source-generated, NativeAOT-clean fluent server API with event-source publish (#3765)
* Add async read/write hooks to BaseVariableState and AsyncCustomNodeManager

Introduces an end-to-end async path for variable Value-attribute reads
and writes that does not require holding lock(this) while the user
callback executes:

* New AttributeReadResult, AttributeSimpleReadResult and AttributeWriteResult
  readonly record structs in NodeState carry the typed result of an async
  Value-attribute hook (no ref/out params across await).
* Four new async delegates (NodeValueEventHandlerAsync,
  NodeValueSimpleEventHandlerAsync, NodeValueWriteEventHandlerAsync,
  NodeValueSimpleWriteEventHandlerAsync) sit alongside the existing sync
  hooks.
* NodeState gains virtual ReadAttributeAsync / WriteAttributeAsync methods.
  The default implementation wraps the synchronous call in lock(this) so
  callers that have not opted into async hooks see bit-identical behaviour.
* BaseVariableState exposes OnReadValueAsync, OnSimpleReadValueAsync,
  OnWriteValueAsync and OnSimpleWriteValueAsync slots and overrides the new
  virtuals to dispatch to them WITHOUT holding the lock during the await.
  On simple-read the framework still applies index range, data encoding and
  copy policy; on simple-write the cached value/status/timestamp are
  updated under lock after the hook completes.
* AsyncCustomNodeManager.ReadAsync and WriteAsync now await the new
  ReadAttributeAsync / WriteAttributeAsync entry points instead of taking
  lock(source) around the synchronous call. Existing per-source locking
  semantics are preserved by the default async wrappers.
* The fluent INodeBuilder surface gains four new async OnRead/OnWrite
  overloads that wire to the new BaseVariableState slots, with the same
  ThrowIfSlotOccupied + null-check pattern as the sync overloads.

* Add typed IVariableBuilder<T> fluent surface

Adds typed sub-interfaces and impls so the existing fluent surface

can expose simple Func<T> / Action<T> / Func<CancellationToken,

ValueTask<T>> overloads for variables — without losing the lower-level

Variant-based overloads on INodeBuilder.

* IVariableBuilder<TValue> + VariableBuilder<TValue> derive from

  INodeBuilder<BaseVariableState> / NodeBuilder<BaseVariableState>

  (NodeBuilder<TState> unsealed).

* Marshalling uses Variant.AsBoxedObject(BoxingBehavior.Legacy) for

  reads and Variant(object) for writes; the AOT-unsafe write path is

  scoped-suppressed with a TODO pointing at the planned per-type

  generated walker.

* INodeManagerBuilder gains Variable<T>(string),

  Variable<T>(NodeId), VariableFromTypeId<T>(NodeId),

  VariableFromTypeId<T>(NodeId, QualifiedName).

Server library builds clean across all 6 TFMs.

* Add relative-child traversal API on INodeBuilder

Adds INodeBuilder.Child(QualifiedName), Child<TState>(QualifiedName)

and Variable<TValue>(QualifiedName) so source-generated typed

wrappers can walk one segment at a time without re-resolving from

the manager root. Resolution uses NodeState.FindChild and reuses

NodeManagerBuilder.ToVariableBuilder to keep the typed-variable

marshalling story consistent.

Adds 7 NUnit cases (Fluent category) covering the happy path,

type-mismatch, null/missing browse-name, and non-variable

rejection. All 66 fluent tests pass.

* Mix old + new fluent APIs in MinimalBoilerServer demo

BoilerNodeManager.Configure now exercises five wiring styles:

  (1) browse-path with the legacy ref-Variant callback

  (2) absolute NodeId with the legacy ref-Variant callback

  (3) typed Variable<double>(NodeId) + simple Func<double> sync getter

  (4) typed Variable<double>(NodeId) + Func<CancellationToken, ValueTask<double>>

      async getter — exercises the BaseVariableState.OnReadValueAsync

      slot end-to-end through AsyncCustomNodeManager.ReadAsync

  (5) NodeFromTypeId with OnNodeAdded lifecycle hook

All five styles compose against the same INodeManagerBuilder, with

the typed and async callbacks pulling in the new IVariableBuilder<T>

surface (committed at e557ecdd9) and the BaseVariableState async

path (committed at 2e2efb4ad).

* Add async-hook tests for BaseVariableState

15 NUnit cases covering the new On{Read,Write}ValueAsync and On
Simple{Read,Write}ValueAsync slots plus the
ReadAttributeAsync/WriteAttributeAsync overrides on
BaseVariableState:

* full async slot routes Value reads/writes
* lock(this) is released around the awaited handler
* simple async slot reapplies cached StatusCode and respects index
  ranges (BadIndexRangeInvalid for non-null ranges)
* exceptions and OperationCanceledException from the hook are
  caught and surfaced as BadUnexpectedError, mirroring the sync flow
* CancellationToken propagates to the hook
* cache (m_value, m_statusCode, m_timestamp) is updated on success
  and skipped on Bad return
* fallback to base sync ReadAttribute/WriteAttribute when no async
  slot is set (preserves today's lock(this) semantics)
* non-Value attributes never invoke the async hook
* CurrentRead/CurrentWrite access checks short-circuit before the
  hook runs

All 596 State tests pass on net10.0.

* Add round-trip tests for typed IVariableBuilder<T>

Covers the typed Func<T>/Action<T>/Func<...,ValueTask<T>> overloads
on IVariableBuilder<TValue>: each registers the appropriate hook
slot on the underlying BaseDataVariableState, and driving the
variable through ReadAttribute/WriteAttribute (sync) and
ReadAttributeAsync/WriteAttributeAsync (async) reproduces the
typed values the user supplied.

* Add typed model-traversal source generator

Adds FluentBuilderGenerator that walks the design's predefined-instance tree and emits a typed sibling for every NodeManagerBuilder. Each generated wrapper exposes IntelliSense-friendly accessors for child instances, variables (IVariableBuilder<T>) and methods (sync/async OnCall) so wiring sites become builder.Boilers.Boiler__1.LCX001.Measurement.OnRead(...) instead of a stringly typed browse path. The NodeManager template now also emits a Configure(I{Manager}NodeManagerBuilder) partial alongside the existing Configure(INodeManagerBuilder); both partials run, both are optional, and the typed surface is fully AOT-safe (no dynamic, no MakeGenericType, no reflection).

* Add tests for the typed model-traversal source generator

Adds 9 snapshot tests in Tests/Opc.Ua.SourceGeneration.Core.Tests covering the FluentBuilderGenerator's opt-in flag, typed-manager-interface and proxy emission, internal-sealed visibility, lazy child resolution via Context.NamespaceUris.GetIndexOrAppend, IVariableBuilder<TValue> accessors, sync+async OnCall overloads on method wrappers, and the dual Configure(INodeManagerBuilder)/Configure(I{Manager}NodeManagerBuilder) partial wiring in the NodeManager template. Adds 2 end-to-end AOT tests in Tests/Opc.Ua.Aot.Tests that exercise typed-traversal sync read on LCX001/Measurement and typed-traversal async OnCall on Simulation/Halt through the AOT-compiled MinimalBoilerServer fixture.

* Document the typed model-traversal Configure(I{Manager}NodeManagerBuilder) partial
2026-05-15 07:52:49 +02:00
Marc Schier 13612c03b9 Fix CreateInstanceOf<X> access restrictions emission and PrioritizedChannel polyfill TryRead bug (#3747) 2026-05-09 20:35:00 +02:00
Marc Schier f66ceffd75 Immutable built in types and code generation (#3534)
Generate stack and application code using source generators
Change built in types to readonly structs
  Update localized text implementation to use JSON text serializer
  Status code with symbolic names
  Immutable node ids
  No boxing for all built in types <= 8 bytes in Variant.
Split user identity token and token handling via token handler
Add migration guide for breaking changes
2026-02-12 16:27:07 +01:00