Skip to content

Source-generated, NativeAOT-clean fluent server API with event-source publish#3765

Merged
marcschier merged 20 commits into
OPCFoundation:masterfrom
marcschier:sgen-server-fluent
May 15, 2026
Merged

Source-generated, NativeAOT-clean fluent server API with event-source publish#3765
marcschier merged 20 commits into
OPCFoundation:masterfrom
marcschier:sgen-server-fluent

Conversation

@marcschier
Copy link
Copy Markdown
Collaborator

Proposed changes

This PR introduces a source-generated, NativeAOT-clean fluent server API that lets developers build OPC UA servers as single-file applications. It composes three layers, each landing as its own series of commits:

1. Async-aware fluent NodeManager runtime (Opc.Ua.Server.Fluent)

  • New INodeManagerBuilder / INodeBuilder API for wiring callbacks against the predefined-node tree by browse path, NodeId, or TypeId — no reflection, no Activator.CreateInstance, no Expression.Compile.
  • Async hooks plumbed end-to-end through BaseVariableState and AsyncCustomNodeManager so handlers can await outside the manager lock.
  • Typed IVariableBuilder<T> surface that erases ref-Variant boilerplate.
  • Relative-child traversal API on INodeBuilder for ad-hoc descents.

2. Source-generated typed wrappers + [NodeManager] opt-in

  • A new FluentBuilderGenerator emits one IntelliSense-aware accessor per predefined instance, child, variable, and method in the model. Wiring a non-existent node becomes a compile-time error rather than a startup ServiceResultException.
  • Typed OnRead / OnWrite / OnCall overloads with arguments — including the multi-argument shapes (int+int → int, double+double → double, string+string → string).
  • Generated Configure(I{Manager}NodeManagerBuilder) partial sits next to the untyped Configure(INodeManagerBuilder) partial; both run, and wiring the same node from both is detected at startup.
  • Wrapper classes are emitted as nested types, so consumers don't pollute the manager namespace.

3. Event-source publish runtime — typed Publish<TEvent> on notifier wrappers

  • New EventSourceRegistry owns the lifecycle of IAsyncEnumerable<TEvent> event sources: lazy activation gated on AreEventsMonitored, cooperative cancellation on the last unsubscribe, bounded shutdown on manager disposal.
  • New FluentNodeManagerBase is now the default base class emitted for source-generated managers; hand-written managers can opt in by deriving from it.
  • The fluent builder generator emits a typed Publish<TEvent> overload only on wrappers whose underlying node qualifies as an event source (SupportsEvents OR forward GeneratesEvent / AlwaysGeneratesEvent reference). TEvent is constrained to BaseEventState.
  • EventPublishOptions exposes AlwaysOn, SkipDefaultPopulation, RegisterAsRootNotifier, CancellationTimeout, OnError. The registry auto-populates EventId / EventType / SourceNode / SourceName / Time / ReceiveTime / Severity / Message so iterators only set the user-meaningful fields.
  • Server-wide root-notifier registration is eager (with rollback on failure) so monitored items on Server reach events emitted on any registered notifier.

Samples + AOT round-trip tests

  • Applications/MinimalBoilerServer — single-file Boiler server (~12-line Program.cs) that wires reads, async reads, async methods, and the new typed Publish<BaseEventState> on the drum notifier.
  • Applications/MinimalCalcServer — calculator server demonstrating typed methods-with-arguments end-to-end.
  • Tests/Opc.Ua.Aot.Tests/PublishedEventsAotTests.cs + BoilerNodeManagerAotTests.cs + CalculatorNodeManagerAotTests.cs — all run under NativeAOT (no JIT, no reflection) and exercise reads, calls, and event monitoring through real client Subscription / MonitoredItem / EventFilter.

Documentation

  • New Docs/SourceGeneratedNodeManagers.md (~500 lines) covers per-class [NodeManager] opt-in, both Configure partials, the typed model-traversal surface, methods-with-arguments overloads, the new event-source Publish<TEvent> API (lifecycle + options + two registration shapes), the single-file Program.cs shape, multi-namespace + manager-swap subclassing, and NativeAOT publishing.

Related Issues

  • Fixes #

Types of changes

  • Bugfix (non-breaking change which fixes an issue)
  • Enhancement (non-breaking change which adds functionality)
  • Test enhancement (non-breaking change to increase test coverage)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected, requires version increase of Nuget packages)
  • Documentation Update (if none of the other choices apply)

Checklist

  • I have read the CONTRIBUTING doc.
  • I have signed the CLA.
  • I ran tests locally with my changes, all passed.
  • I fixed all failing tests in the CI pipelines.
  • I fixed all introduced issues with CodeQL and LGTM.
  • I have added tests that prove my fix is effective or that my feature works and increased code coverage.
  • I have added necessary documentation (if appropriate).
  • Any dependent changes have been merged and published in downstream modules.

Local verification snapshot

  • Opc.Ua.SourceGeneration.Core.Tests: 3511 / 3511 on net10.0 (8 pre-existing skips).
  • Opc.Ua.Server.Tests (Fluent + AsyncCustomNodeManager): 345 / 345 on net472 / net48 / net8.0 / net9.0 / net10.0.
  • Opc.Ua.Aot.Tests (Boiler + Calculator + PublishedEvents): 78 / 78 on net10.0.
  • MinimalBoilerServer / MinimalCalcServer: build clean.

Further comments

The work is staged into 19 small, self-contained commits — easiest to review one commit at a time. The locked design decisions, especially around the event-publish surface (lazy activation, single typed overload per qualifying notifier, dispose timing derived from max(per-source CancellationTimeout) + 5 s), are documented inline in EventSourceRegistry.cs and in the new Docs/SourceGeneratedNodeManagers.md "Event sources" section.

marcschier and others added 19 commits May 12, 2026 12:32
…nager

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.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
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.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
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.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
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 e557ecd) and the BaseVariableState async

path (committed at 2e2efb4).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
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.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
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.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
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).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
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.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…lder) partial

Adds a section to Docs/SourceGeneratedNodeManagers.md walking through the source-generated typed builder: the second Configure partial whose builder parameter exposes IntelliSense accessors for every predefined instance, child, variable and method in the model. Includes a side-by-side untyped+typed example mirroring MinimalBoilerServer's BoilerNodeManager.Configure.cs and itemizes the per-model emit (interface, proxy, instance wrappers, method wrappers) so consumers know exactly what surface ships in {Manager}.FluentBuilders.g.cs.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The AOT integration test `ConnectAndCloseSessionAsync` failed because
`SessionManager.CloseSessionAsync` (server-side) had its
`ConcurrentDictionary.TryRemove` condition inverted in commit
70b4498: when the session was successfully removed the method
returned without actually closing it, and a stray
`Debug.Assert(session == null)` was placed on the success branch
where `session` is in fact the just-removed value. The assertion
was previously invisible because `Debug.Assert` only logs on most
runners, but TUnit installs a `ThrowListener` that converts
`Debug.Fail` calls into thrown `TUnitException` instances, which
the server then surfaces as `BadUnexpectedError` in the
`CloseSession` response. `Session.CloseAsync` propagates that
status to the caller, so the client test's
`Assert.That(result).IsEqualTo(StatusCodes.Good)` failed.

Restore the original logic: when `TryRemove` fails the entry was
already removed by a concurrent caller (so `session` stays null)
and the method bails; when it succeeds the loop breaks and the code
below disposes the session and updates the diagnostics counters.

After the fix the previously failing test passes and all 73 AOT tests
succeed; `Opc.Ua.Server.Tests` Fluent tests (82) also pass on
net10.0.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Refactors the FluentBuilderGenerator to emit each per-instance and
per-method wrapper as a nested type inside its lexical parent's
wrapper, instead of as a flat namespace-scope class with a long
underscore-joined name (e.g. Boilers_Boiler__1_PipeX001_FTX001Builder).
The user-facing fluent surface is unchanged because property names
on accessors do not change — only the underlying class names.

Changes:
- Add LeafName / ParentKey / ChildObjectKeys / ChildMethodKeys to
  InstanceWrapper and LeafName / ParentKey to MethodWrapper.
- Replace ComposeClassName with ResolveLeafName, ResolveParentKey
  and ComposeWrapperClassName helpers.
- Add LinkChildWrappers post-pass to wire each parent to its
  direct child wrappers (sorted by ordinal leaf name).
- Refactor Emit to walk only top-level wrappers and recurse via
  the new ChildObjectKeys / ChildMethodKeys.
- Thread an explicit indent parameter through EmitInstanceWrapper,
  EmitChildAccessor, EmitMethodWrapper, EmitMethodOnCall,
  EmitInputUnpack and EmitOutputBox so each nesting level adds
  another 4 spaces to every emitted line. Avoid the writer's
  Push/Pop indentation API which discards pending newlines.
- Add three new snapshot tests: TopLevelInstanceWrapperLivesAt
  NamespaceScope, MethodWrapperIsNestedInsideOwningObject, and
  ChildAccessorReturnsSimpleLeafName.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds a new MathInstance predefined Object instance to TestModel.xml
that hosts two methods declared with inline InputArguments and
OutputArguments — Compute(x: Int32 -> result: Int32) and
Add(a: Int32, b: Int32 -> sum: Int32). This finally exercises the
FluentBuilderGenerator code path that emits typed OnCall(...)
overloads (EmitInputUnpack / EmitOutputBox / FormatInputTypeList),
which previously had zero snapshot coverage because the test model
contained no predefined method instance with arguments.

Adds five FluentBuilderGenerator snapshot tests asserting:
  * MethodWithIntInputAndOutputEmitsTypedSyncOverload — sync
    OnCall(Func<int,int>) is emitted on ComputeMethodBuilder.
  * MethodWithIntInputAndOutputEmitsTypedAsyncOverload — async
    OnCall(Func<int,CancellationToken,ValueTask<int>>) is emitted.
  * MethodInputUnpackUsesVariantTryGetValue — generated lambda body
    unpacks Variants via TryGetValue and short-circuits with
    BadInvalidArgument / BadArgumentsMissing as appropriate.
  * MethodOutputBoxUsesVariantFrom — single-output methods box the
    handler return value via Variant.From(__r).
  * MethodWithMultipleInputArgsEmitsCorrectArity — multi-input
    methods (Add) produce Func<int,int,int> sync and
    Func<int,int,CT,ValueTask<int>> async overloads, and unpack
    each Variant by index.

FluentBuilderGenerator tests grow from 12 to 17; full source-gen
suite passes 3511/0/8 across net8/9/10/472/48 (was 3506/0/8).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds an end-to-end sample exercising the typed source-generated
`OnCall` overloads on methods with input/output arguments — sync
`int+int -> int`, async `double+double -> double`, and sync
`string+string -> string` — plus AOT round-trip tests covering each
shape via `Session.CallAsync`.

Fixes a runtime bug in `FluentBuilderGenerator.EmitOutputBox`
discovered by the new tests: the typed wrapper appended boxed return
values to the outputs list with `__outputs.Add(...)` while the base
`MethodState.Call` dispatcher pre-populates the list with default
`Variant` slots — one per declared output argument — before invoking
the user handler. The result was double-counted outputs at the wire.
The wrapper now assigns by index (`__outputs[i] = ...`); the
matching unit assertion in `FluentBuilderGeneratorTests` is updated.

The sample model uses the proven `ModelDesign` pattern (top-level
`MethodType` declarations + `CalculatorType` `ObjectType` +
`Calculator` predefined instance with an explicit inverse
`Organizes` reference back to `ObjectsFolder`) so the typed
`MethodState` subclasses, fluent builders and a published instance
all reach the address space.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds a "Methods with arguments — typed `OnCall` overloads" section
to `Docs/SourceGeneratedNodeManagers.md` describing the typed sync
and async `OnCall(Func<...>)` overloads, `Variant.TryGetValue<T>`
input-unpack semantics, `Variant.From<T>` output-box semantics, and
the multi-output `ValueTuple` shape. Updates the "What the generator
emits per model" bullet to point at the new section, and lists
`Applications/MinimalCalcServer/` alongside the existing Boiler
sample with a pointer at the companion AOT round-trip tests.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds the runtime that lets fluent users register IAsyncEnumerable<TEvent> sources on event-notifier nodes via builder.Node<...>().Publish(...).

FluentNodeManagerBase opt-in base owns an EventSourceRegistry that activates and deactivates streams in lock-step with NodeState.AreEventsMonitored, dispatches events through node.ReportEvent, and tears down cleanly on Dispose. EventPublishOptions exposes lazy / eager activation, RegisterAsRootNotifier, OnError, and a CancellationTimeout for shutdown.

Wires the registry into NodeManagerBuilder via FluentNodeManagerBase.AttachToBuilder(...) which the source-generator-emitted CreateAddressSpaceAsync calls right before the user's Configure partial runs (a no-op for non-fluent managers, so behavior is unchanged for existing CustomNodeManager2-based generators).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds 21 unit tests covering the runtime introduced in cf3f1f3: lazy/eager activation, monitor/unmonitor cycles, event delivery and default-population, SkipDefaultPopulation, factory and iterator error paths with OnError, duplicate registration, timeout validation, null-arg validation, EventNotifier auto-promotion, root-notifier registration, dispose cancellation propagation, and the Publish extension on attached vs. non-attached builders.

Tests use a TestablePublishManager that exposes the protected RootNotifiers/PredefinedNodes accessors and a helper to drop a notifier into the predefined-nodes table without requiring a full master-node-manager wiring. Race-free assertions use TaskCompletionSource hooks instead of polling.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Generated NodeManager partials now inherit from FluentNodeManagerBase instead of AsyncCustomNodeManager. Behavior is unchanged for managers without Publish bindings — FluentNodeManagerBase only adds an idle EventSourceRegistry that is disposed with the manager.

With the base type fixed, the conditional cast in CreateAddressSpaceAsync collapses to a direct AttachToBuilder(__m_builder) call which makes Publish(...) extensions usable without any user opt-in. Updates the snapshot assertion in NodeManagerGeneratorTests accordingly.

Also stabilizes PublishTests under parallel CPU load: bumps s_signalTimeout from 5s to 15s and replaces the two hardcoded 50ms warm-up sleeps with TaskCompletionSource hooks signaled when the worker has actually entered the iterator.

Verified: source-generator tests 3511/3511, AOT end-to-end 77/77, fluent server tests 103/103 (×5 TFMs).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Closes Phase 3b (pub-gen-typed + pub-snapshot-tests) of the event-source
publish runtime work. The fluent builder generator now emits typed
Publish<TEvent> overloads on every wrapper whose underlying NodeDesign
qualifies as an event notifier in the model.

Detection (locked decision: spec-accurate, not blanket):
- ObjectDesign.SupportsEvents is set (the design-XML form of
  EventNotifier=SubscribeToEvents; the model validator already
  auto-promotes nodes carrying forward HasEventSource/HasNotifier
  references), OR
- the node has any forward GeneratesEvent or AlwaysGeneratesEvent
  reference in its References collection.

Each qualifying wrapper now exposes two overloads matching the runtime
extension's shape:
- Publish<TEvent>(IAsyncEnumerable<TEvent>, EventPublishOptions?)
- Publish<TEvent>(Func<TNotifier, ISystemContext, CancellationToken,
  IAsyncEnumerable<TEvent>>, EventPublishOptions?)

Both forward to EventNotifierBuilderExtensions.Publish with the
wrapper's underlying state type bound as TNotifier so callers don't
need to spell it out. EventPublishOptions is annotated as nullable
because the generated file uses #nullable enable.

Snapshot coverage in TestModel.xml + FluentBuilderGeneratorTests:
- NotifierObject (SupportsEvents=true) -> Publish<TEvent> emitted.
- EventEmittingObject (forward GeneratesEvent reference) -> emitted.
- TestObject (plain object) -> Publish<TEvent> NOT emitted.

Verification:
- Source-generator tests: 3511/3511 (8 pre-existing skips) on net10.0.
- Fluent + AsyncCustom server tests: 345/345 across 5 TFMs
  (net472/net48/net8.0/net9.0/net10.0).
- AOT integration: 77/77 on net10.0 with Boiler/Calculator generated
  managers — the Boiler model legitimately produces wrappers with
  Publish<TEvent> on its state-machine notifiers via SupportsEvents
  inherited from FiniteStateMachineType.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Phase 4-5 of the event-source publish runtime:

* MinimalBoilerServer: wire `builder.Boilers.Boiler__1.DrumX001
  .Publish<BaseEventState>(GenerateDrumHeartbeatAsync)` in the typed
  Configure partial. The async iterator emits a synthetic 500 ms
  heartbeat event with Severity/Message; the registry auto-populates
  EventId/EventType/SourceNode/SourceName/Time/ReceiveTime. Lazy
  activation: the iterator only runs while a client is monitoring.

* Tests/Opc.Ua.Aot.Tests/PublishedEventsAotTests.cs: end-to-end AOT
  round-trip. A real client subscription with EventFilter on
  EventNotifier of the DrumX001 node receives the heartbeats; asserts
  SourceName/Severity/Message on the EventFieldList. 78/78 AOT tests
  pass on net10.0 (was 77).

* Docs/SourceGeneratedNodeManagers.md: new `Event sources` section
  covering the typed `Publish<TEvent>` overload, the two
  registration shapes (direct stream + factory),
  `EventPublishOptions` for lifecycle tuning, where the typed
  overload appears (SupportsEvents OR GeneratesEvent), and the
  hand-written-manager opt-in via `FluentNodeManagerBase`.

Verification:
* MinimalBoilerServer: build clean.
* Sgen tests: build clean.
* Fluent + AsyncCustomNodeManager server tests: 345/345 on net10.0.
* AOT tests: 78/78 on net10.0.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@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 all sign our Contributor License Agreement before we can accept your contribution.
1 out of 2 committers have signed the CLA.

✅ marcschier
❌ Copilot
You have signed the CLA already but the status is still pending? Let us recheck it.

# Conflicts:
#	Libraries/Opc.Ua.Server/Session/SessionManager.cs
@marcschier marcschier marked this pull request as draft May 14, 2026 04:44
@codecov
Copy link
Copy Markdown

codecov Bot commented May 14, 2026

Codecov Report

❌ Patch coverage is 76.47059% with 332 lines in your changes missing coverage. Please review.
✅ Project coverage is 72.19%. Comparing base (96f8964) to head (9aac669).
⚠️ Report is 1 commits behind head on master.

Files with missing lines Patch % Lines
...neration.Core/Generators/FluentBuilderGenerator.cs 83.33% 76 Missing and 45 partials ⚠️
...raries/Opc.Ua.Server/Fluent/EventSourceRegistry.cs 66.05% 70 Missing and 22 partials ⚠️
Stack/Opc.Ua.Types/State/BaseVariableState.cs 72.26% 22 Missing and 16 partials ⚠️
...braries/Opc.Ua.Server/Fluent/NodeManagerBuilder.cs 51.35% 15 Missing and 3 partials ⚠️
...ries/Opc.Ua.Server/Fluent/FluentNodeManagerBase.cs 43.33% 17 Missing ⚠️
...pc.Ua.Server/NodeManager/AsyncCustomNodeManager.cs 0.00% 13 Missing ⚠️
Libraries/Opc.Ua.Server/Fluent/VariableBuilder.cs 81.53% 6 Missing and 6 partials ⚠️
Libraries/Opc.Ua.Server/Fluent/NodeBuilder.cs 76.74% 8 Missing and 2 partials ⚠️
...Ua.Server/Fluent/EventNotifierBuilderExtensions.cs 71.87% 7 Missing and 2 partials ⚠️
Libraries/Opc.Ua.Server/Fluent/IVariableBuilder.cs 77.77% 1 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #3765      +/-   ##
==========================================
+ Coverage   72.14%   72.19%   +0.05%     
==========================================
  Files         597      605       +8     
  Lines      122192   123587    +1395     
  Branches    20582    20817     +235     
==========================================
+ Hits        88154    89227    +1073     
- Misses      27997    28219     +222     
- Partials     6041     6141     +100     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copy link
Copy Markdown
Contributor

@romanett romanett left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice work

@marcschier marcschier marked this pull request as ready for review May 14, 2026 21:42
@marcschier marcschier merged commit 5795e50 into OPCFoundation:master May 15, 2026
126 of 129 checks passed
@marcschier marcschier deleted the sgen-server-fluent branch May 15, 2026 05:52
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.

4 participants