Files
UA-.NETStandard/Docs/ModelDependencies.md
Marc Schier d14cd10a86 Model dependency annotations using source generator + flaky test fixes (#3687)
* Fix publish/dispose race that hangs Client tests

Subscription.Dispose pre-disposed m_publishTimer and m_messageWorkerCts
before calling ResetPublishTimerAndWorkerState, causing Cancel() to throw
ObjectDisposedException and leaving the publish worker stuck. Removed the
duplicate disposal so the reset method is the single owner of those resources,
and guarded Cancel/Dispose with try/catch ObjectDisposedException for defense
in depth.

Session.OnPublishComplete could run after Session.Dispose disposed
m_reconnectLock, throwing ObjectDisposedException from SemaphoreSlim.Wait()
and re-arming via the throttle path indefinitely. Added an early Disposed
guard, wrapped Wait/Release in try/catch ObjectDisposedException, and added
a Disposed guard in QueueBeginPublish so the throttle cannot reschedule
publishes after disposal.

Fixes the macOS net10.0 hang seen in Azure pipeline build 13257
(TransferSubscriptionAsync, ReconnectWithSavedSessionSecretsAsync).

* fix lockout test and use UserName token for SuccessfulAuthenticationClearsFailedAttempts test

After c1e11b1ab made anonymous activations no-op for clearing the lockout
counter, the test must use a real (non-anonymous) credential to demonstrate
that successful authentication resets failed attempts. Use user1/password
with PolicyId=1 (UserName_1 in the reference server config).

* Fix DirectoryCertificateStore cache staleness across instances

When two DirectoryCertificateStore instances point at the same directory (e.g. a CertificateValidator's internal store and a separate store opened by a caller), the directory LastWriteTimeUtc seen by one DirectoryInfo handle does not reliably reflect modifications made through another handle on Windows, even after Refresh(). The mtime-based cache check therefore returned stale entries indefinitely, causing VerifyRejectedCertsDoNotOverflowStoreAsync to flake when the test fixture's RejectedStore failed to observe deletes performed by the validator's internal store.

Add a cheap on-disk file count cross-check in the cache-hit branch: if the count of cert files on disk does not match the cached entry count, force a reload. This avoids re-parsing every certificate when nothing has changed but reliably detects external modifications.

* test: fix keep-alive mock to return Int32 ServerState

The mock for the Server_ServerStatus_State read returned new Variant(0u)
(UInt32). The keep-alive worker expects Int32 (ServerState enum is
wire-encoded as Int32), so ClientBase.ValidateDataValue logged a
BadTypeMismatch warning on every keep-alive throughout the test run.

Real servers always emit Int32 for enum variables (BinaryEncoder
downgrades the enum encoding byte to Int32), so the warning was purely a
test-mock artifact. Switch the mock to new Variant((int)ServerState.Running)
to eliminate the spurious warnings.

* Source generator: emit and consume ModelDependencyAttribute. Emit one [assembly: ModelDependencyAttribute(...)] per model the
  generated assembly emits and per upstream model it transitively
  consumes (new ModelDependencyGenerator in
  Tools/Opc.Ua.SourceGeneration.Core).
* Replace the unused PEReader-based ReferenceMetadataScanner with a
  Roslyn IAssemblySymbol.GetAttributes() scan
  (ReferencedModelDependencyScanner in
  Tools/Opc.Ua.SourceGeneration). Hooks the per-symbol incremental
  cache, works for both PortableExecutableReference and
  CompilationReference, AOT-safe.
* Wire the scanner result through ModelSourceGenerator into
  ModelCompilation. The reduced URI-keyed map is passed to the public
  Generators.GenerateCode extension methods.
* NodesetFileCollection.GetDesignFileListForModel now consults the
  referenced-model map, so transitive nodeset dependencies that are
  satisfied by a referenced assembly no longer raise the
  missing-dependency error.
* Override resolution: when a local AdditionalFile and a referenced
  assembly both declare the same model URI under the same C# prefix
  the local generation is silently skipped (avoids CS0101); different
  prefixes still generate so downstream consumers can intentionally
  override types under a different namespace.
* New diagnostics MODELGEN010 (override skipped) and MODELGEN012
  (multiple referenced assemblies provide same model URI -> tie-break
  on highest version+publication date).
* Bump asserted GeneratedSources counts in ModelGeneratorTests for the
  new *.ModelDependencies.g.cs files (one per generated model).
* Fix unrelated CS1574 broken cref in NodeId.TryParseLongForm xmldoc
  that was blocking the Opc.Ua.Types build.
* Document the new flow in Docs/ModelDependencies.md.
* tests for ModelDependencyAttribute flow

* Refactor recently added "parsing" constructors to ParseLongForm static method. Method delegates to Parse(IServiceMessageContext)

NodeId, ExpandedNodeId and QualifiedName ParseLongForm overloads are now thin wrappers around Parse(context, options) using NodeIdParsingOptions with RequireResolvedUris=true and updateTables=false. Adds the FallbackToStringIdentifier opt-in to preserve the legacy lenient quirk for callers that need it (off by default for strict parity).

Tests for the refactored long-form parsers were added to the existing per-type test fixtures.

* Update nuget dependencies to latest
2026-04-22 10:15:12 +02:00

3.1 KiB

Cross-Assembly Model Dependencies

The OPC UA source generator emits [assembly: Opc.Ua.ModelDependencyAttribute(...)] metadata on every assembly that has nodesets or design files in <AdditionalFiles>. The attribute records, for each model the assembly emits or transitively consumes, the model URI, the C# namespace prefix the generator used, and (when known) the version and publication date.

Downstream consumers that reference such an assembly no longer need to re-add those upstream nodesets to their own <AdditionalFiles>. The generator scans the attributes on referenced assemblies and uses them to:

  1. Suppress the missing-dependency error in nodeset dependency collection when the missing namespace is known to be supplied by a referenced assembly.
  2. Apply override resolution when a local <AdditionalFiles> entry refers to a model URI that is also in a referenced assembly:
    • Same C# prefix → local generation is silently skipped (the referenced assembly already supplies the types; duplicate emission would cause CS0101).
    • Different C# prefix → local generation proceeds; types live in a different C# namespace so no symbol conflict occurs.
  3. Tie-break when more than one referenced assembly provides the same model URI by selecting the entry with the highest (Version, PublicationDate) lexicographic tuple. The losing entries are reported as MODELGEN012 info diagnostics.

Diagnostics

ID Severity Meaning
MODELGEN010 Info Local model skipped because a referenced assembly provides it.
MODELGEN012 Info Multiple referenced assemblies provide the same model URI.

Implementation

The Roslyn-side scan lives in Tools/Opc.Ua.SourceGeneration/ReferencedModelDependencyScanner.cs and uses IAssemblySymbol.GetAttributes() so it works for both PortableExecutableReference and CompilationReference, hooks Roslyn's per-symbol incremental cache, avoids file IO, and is AOT-safe inside the generator.

The emitter lives in Tools/Opc.Ua.SourceGeneration.Core/Generators/ModelDependencyGenerator.cs and produces one {prefix}.ModelDependencies.g.cs per generated model containing assembly-attribute lines for the model itself and every model it consumes (including the closure recovered from referenced assemblies).

Limitations (Phase 1)

This first cut keeps cross-assembly type resolution out of scope. When a nodeset's transitive namespace is satisfied only by a referenced assembly the generator skips the missing-dependency error but does not wire that namespace's type definitions into the validator. Downstream users that need to emit references to types from those upstream models (e.g. subtypes, complex type fields, method input/output arguments) must still add the upstream design or nodeset files to <AdditionalFiles> for now. A follow-up will register the referenced model prefixes with ModelDesignValidator so that no AdditionalFile duplication is required.