Files
UA-.NETStandard/docs/ModelDependencies.md
5899354441 Clarify documentation, migration guidance, and SDK selection (#4823)
# Description

Remove stock phrasing and correct misleading or inconsistent technical
documentation. The initial two passes covered 367 Markdown files,
followed by an interactive review of 169 editorial flags. The U5
interpretation originally deferred in that review has since received the
upstream Mantis answer recorded by #4843; remaining server/spec
follow-ups stay explicit in the CTT log.

- Correct package responsibilities, migration steps, API names,
ownership, protocol descriptions and links. Keep bundled migration
guides synchronized. Preserve historical conclusions as historical
records with separate re-check notes.
- Provide at least two rendered TOC levels throughout `docs/`. Convert
appropriate architecture figures to Mermaid without changing code
examples, transcripts or historical quotations.
- Address the October 19 feedback with concrete setup/migration
instructions, corrected certificate paths and PubSub names, shorter
repeated caveats, consistent XML comments, and a full review of the
Concepts guide. Retain #4839's API corrections.
- Integrate #4841's runtime/performance guidance and review
incoming/recent prose. Preserve dated measurements rather than claiming
new benchmark runs; clarify allocation ratios, startup-only processor
settings, JIT warm-up and the benchmark commit's scope. Missing raw
measurement artifacts and repeat ranges remain tracked by #4842.
- Merge master through `e8c5d800a` in `32c6394b7`. Resolve the CTT log
conflict using the updated Mantis states and OPC answers, retaining
historical framing and re-checks for C28/C32/C43.
- Address the latest naming/index feedback in `adb5bf693`: rename the
guides to `docs/WotCon.md` and `docs/WotNodeSetConversion.md`; move the
provider and DataType-declaration contracts into the conversion guide
and remove `docs/WoT.md`. Rename `samples/DI` to `samples/Di` and
`samples/Isa95` to `samples/ISA95`. Add content-index READMEs to Di,
FluentApi, ISA95, Lds and PubSub; update solution entries, project
references, Docker/CI paths, test fixture paths and documentation links
with exact casing.

**WoT guide scope.** This branch retains the detailed conversion guide
that #4758 had replaced with an anchor stub. Conversion limits, verified
document sets, preservation rules and provider ownership now live
together in `WotNodeSetConversion.md`; binding/runtime guidance remains
in `WotBindings.md` and `WotCon.md`. The design-rationale/test-ledger
prose was shortened, the oversized argument-table cell moved into its
section, and existing anchors preserved.

**SDK policy is a separate, explicit user-directed change.**
`global.json` sets a stable minimum of `10.0.400`, `rollForward:
latestMajor`, and `allowPrerelease: false`. Newer major SDKs are
permitted; CI's explicit SDK pins are unchanged. README, GettingStarted,
DeveloperGuide and Recycling instructions reflect the possible analyzer
differences. The image-contract regression fix accepts compatible stable
SDK images instead of requiring exact equality with the floor.

The latest four rename/index threads are resolved after publication.
Three older threads remain open: universal obsolete-removal timing,
unconditional nonce-strength wording, and a blanket Vision NativeAOT
claim. Existing implementation/evidence constraints are explained in
their inline replies. The two-level TOCs remain as requested.

## Validation

- `git diff --check` and staged diff checks: passed. The untouched
upstream AutoID NodeSet retains its provenance bytes, including two
existing license-header trailing spaces.
- `.\.azurepipelines\validate-migration-plugin.ps1`: passed; 15 bundled
migration documents and manifests synchronized.
- Documentation/path validation: **101** documents retain two rendered
TOC levels (**2,228** entries), with idempotent formatting. **52** new
relative links and their anchors passed exact-case checks. No tracked
references to the old guide/sample paths remain. Both relocated
provider-contract sections are preserved in the destination.
- `dotnet sln UA.slnx list`: passed; shows the renamed Di/ISA95 projects
and the existing FluentApi/Lds/PubSub projects. Updated solution and
pump ProjectReference paths also passed exact-case existence checks.
- Latest focused commands, run from the repository root:

```powershell
# Passed: 61 channel send-order and reconnect tests.
dotnet test tests\Opc.Ua.Core.Tests\Opc.Ua.Core.Tests.csproj -p:CustomTestTarget=net10.0 --filter 'FullyQualifiedName~UaSCBinaryChannelSendOrderTests|FullyQualifiedName~TcpReconnectKeyAgreementTests' --verbosity minimal

# Passed: 5 pump datasheet tests; builds the renamed pump sample.
dotnet test tests\Opc.Ua.Di.Tests\Opc.Ua.Di.Tests.csproj -p:CustomTestTarget=net10.0 --filter 'FullyQualifiedName~PumpDatasheetConformanceTests' --verbosity minimal

# Passed: 4 repository NodeSet path/round-trip tests, including the moved pump models.
dotnet test tests\Opc.Ua.Types.Tests\Opc.Ua.Types.Tests.csproj -p:CustomTestTarget=net10.0 --filter 'FullyQualifiedName~RepositorySourceSurvivesSerializedLinkedPartitionsAsync' --verbosity minimal

# Passed: build, zero warnings/errors.
dotnet build samples\ISA95\MinimalIsa95Server\MinimalIsa95Server.csproj -p:CustomTestTarget=net10.0 -f net10.0 --verbosity minimal

# Passed: dependent sample-test project build, zero warnings/errors.
dotnet build tests\Opc.Ua.WotCon.Samples.Tests\Opc.Ua.WotCon.Samples.Tests.csproj -p:CustomTestTarget=net10.0 -f net10.0 --verbosity minimal
```

Earlier focused integration checks passed 2,620 tests across AutoID,
event queues/scheduling, source generation, MCP/package profiles and
WoT. Both reviewed Mermaid diagrams rendered locally without network
access. These checks are not a full solution run.

The full solution has **not** been rerun for this review. Earlier
`dotnet test UA.slnx -p:CustomTestTarget=net10.0 --verbosity quiet` did
not pass: three AOT projects rejected VSTest and 20 Vision tests failed
during graphics initialization with `OutOfMemoryException`. Remaining
long-running tests were stopped at the user's request. A focused Vision
rerun passed 558 tests and reproduced those 20 failures. The earlier
`dotnet test UA.slnx -p:CustomTestTarget=net48 --verbosity quiet` was
blocked by the former SDK pin and was not retried after the policy
change. The user approved proceeding with those limitations documented;
current remote CI is not claimed green.

## Related Issues

- Follow-up #4842 tracks the requested 2.0.2+ benchmark/tuning and
diagnostic-tool documentation refresh. #4841 supplies documentation and
earlier results, not all newly requested measurements.
- #4758 explains the conversion guide's earlier consolidation; the
current provider contracts are now consolidated into
`WotNodeSetConversion.md`.
- #4839 supplies the reviewed API corrections; #4843 supplies the
updated CTT issue record.
- The original prose cleanup and SDK policy were user-requested without
a source issue. No new runtime architecture is introduced by these
documentation edits.

## 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.
- [ ] 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.
- [ ] I have verified that my changes do not introduce (new) build or
analyzer warnings.
- [ ] 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: Roman Ettlinger <romanett98@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 10:10:55 +02:00

228 lines
12 KiB
Markdown

# Cross-Assembly Model Dependencies
## Contents
- [Attribute shape](#attribute-shape)
- [Payload wire format (`ModelDependencyV1`)](#payload-wire-format-modeldependencyv1)
- [Nested instance declarations](#nested-instance-declarations)
- [Diagnostics](#diagnostics)
- [Implementation](#implementation)
The OPC UA source generator emits `[assembly: Opc.Ua.ModelDependencyAttribute(...)]`
metadata on every assembly that has nodesets or design files in `<AdditionalFiles>`.
A single attribute carries both the lightweight dependency-closure information
**and**, on the assembly's self-declaration entry, a compact binary type-table
payload that downstream source generators can decode without re-walking
`AdditionalFiles`. Each instance of the attribute records, for one model the
assembly emits or transitively consumes:
- the model URI,
- the C# namespace prefix the generator used,
- (when known) the version and publication date,
- the C# identifier of the assembly's `Namespaces` class entry for the model,
- and — on self-declaration entries only — a base64-encoded Deflate-compressed
`ModelDependencyV1` type-table payload.
Downstream consumers that reference such an assembly do not 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. **Cross-namespace prefix mapping** — when a local generator loads a
NodeSet2 that depends on another model, it auto-generates dependency
prefixes (e.g. `Opc.Ua.DI`) that may not match the C# namespace the
referenced assembly actually uses (`Opc.Ua.Di`). The override step rewrites
the dependency namespace's `Prefix` (and `Name`) to match the values
published by the referenced assembly's `[ModelDependencyAttribute]`, so
cross-namespace type references emitted into the local code compile
correctly. The target namespace itself is never rewritten.
4. **Resolve ownership** by preferring the payload-bearing producer over
payloadless transitive dependency entries. A dependency with the same prefix,
version, publication date, and name describes that producer; it is not a
competing provider and does not produce `MODELGEN012`. Distinct producers or
conflicting metadata still produce that info diagnostic. Among competing
entries, the generator applies the
OPC 10000-6 version/publication-date ordering, then the ordinal assembly name
(smallest first) to make equal-version producer selection deterministic.
5. **Import the type-table payload** on self-declaration entries so the
validator's node table is pre-populated with the upstream's types,
children, method arguments, DataType fields, and effective variable
metadata. Cross-namespace
`BaseType` / `TypeDefinition` / `DataType` references in the consumer's
own models then resolve against the imported types without needing the
upstream NodeSet2/ModelDesign XML in `AdditionalFiles`.
## Attribute shape
`Opc.Ua.Vision` produces the Perception and Part 1 [Machine Vision](Vision.md)
models with separate CLR namespaces and loaders. `Opc.Ua.MachineVision`
produces the Part 2 AMCM model, which consumes DI and Machinery independently
of Part 1. Recursive optional instance
declarations, such as AMCM SEMI E10 `SubStates`, keep their type information without
recursively materializing an unbounded default instance tree.
The attribute constructor accepts six arguments (the trailing four are
optional):
```csharp
[assembly: Opc.Ua.ModelDependencyAttribute(
modelUri: "http://opcfoundation.org/UA/DI/",
prefix: "Opc.Ua.Di",
version: "1.05.0",
publicationDate: "2025-11-15T00:00:00Z",
name: "OpcUaDi",
payload: "qscBA…<base64 ModelDependencyV1>…AAA=")]
```
The `name` parameter records the C# identifier the assembly used inside its
`Namespaces` class — i.e. the `name` consumers must use when emitting
`global::{Prefix}.Namespaces.{Name}` cross-namespace constant references.
The `payload` parameter is non-null only on the assembly's own
self-declaration entry. Transitive-dependency entries (the one-per-referenced-
model rows the generator re-emits) carry `null`. The producing assembly is the
canonical source of its type-table description.
## Payload wire format (`ModelDependencyV1`)
Encoding lives in
`tools/Opc.Ua.SourceGeneration.Core/Dependency/ModelDependencyV1.cs`:
- Magic header: `0xAA 0xC7`
- Version byte: `0x01`
- Compression byte: `0x01` (Deflate)
- Body (compressed): `ModelUri` string + node array, each carrying symbolic
name/namespace, class name, kind, base-type chain, numeric/string NodeId,
abstract / enumeration flags, DataType fields, and declared instance
children. Variable children also carry access and explicit user-access
bitmasks, minimum sampling interval, historizing state, the corresponding
presence flags, and serialized default-value XML. Method children carry
their argument lists and effective method identity. The payload is
deterministically sorted by
`(SymbolicNamespace, SymbolicName)` so the produced base64 string is
byte-reproducible across builds.
### Nested instance declarations
Direct child records retain the existing V1 layout. Producers with explicit
nested instance declarations append an optional, length-prefixed **nested-child
trailer** (capability bit `0x02`, trailer version `1`). It records each direct
child's descendants in declaration order, including their modelling rules,
qualified browse names, reference types, variable metadata/default-value
namespace tables and method signatures/identities. Type references are not
recursively expanded into this trailer: recursive optional types remain finite.
Payloads without nested declarations remain unchanged. A new reader accepts
older V1 payloads with no trailer and treats their descendant lists as empty.
Older readers can ignore the new trailer; the writer also includes the existing
extended-identifier trailer flag so those readers continue to interpret an
unknown fluent-accessor capability correctly. Unknown nested-trailer versions
are skipped using their length. Readers validate the frame extent and reject
inconsistent counts, more than 100 nested levels or more than 100,000 nested
declarations in one payload.
The consumer materializes and links these descendants before hierarchy merging.
This preserves mandatory parameters below inherited or redeclared generic
containers such as DI `ParameterSet`, without materializing unrequested
optional members. Explicit nested placeholder declarations retain their wire
identifiers and required properties; they are not confused with recursive
expansion of an inherited placeholder or created as default runtime instances.
Specialized placeholder helpers retain the inherited CLR method signature even
when the child has a different browse-name namespace. Such helpers explicitly
hide the base helper with `new`; changing the wire namespace does not make the
C# method name unique. Unrelated placeholder helpers do not receive `new`.
An explicit NodeSet `MethodDeclarationId` remains the authoritative wire
identity of a redeclared method. When a dependency payload supplies an inherited
method's signature and generated state class but not its instance NodeId, the
consumer resolves the inherited qualified member and attaches the explicit
identifier to a copy. It neither replaces that identifier with the consumer's
method NodeId nor modifies the dependency's signature or generated class.
Rebuild model-producing assemblies to publish the richer payload before
rebuilding consumers that require nested declarations. An older producer's
flat payload cannot supply descendants it never recorded; the consumer retains
the previous direct-child behavior rather than inventing missing declarations
or regenerating the dependency's namespace.
Readers reject unknown versions cleanly and the downstream pipeline falls
back to explicit `AdditionalFiles` resolution when a payload cannot be
decoded.
## Diagnostics
| ID | Severity | Meaning |
| ------------- | -------- | ------------------------------------------------------------------ |
| `MODELGEN010` | Warning | A `[NodeManager]` attribute could not be bound to a model (unmatched selector, or ambiguous when the project has multiple models). |
| `MODELGEN012` | Info | Distinct model producers or conflicting dependency metadata claim the same URI; matching payloadless transitive declarations are not additional producers. |
| `MODELGEN013` | Info | Local model skipped because a referenced assembly provides it. |
## 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. Each attribute is read into a `ModelDependencyReference` whose
`GetDependency()` method lazily decodes the payload through a memoising
`ConditionalWeakTable` keyed on the payload string.
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 (with the
`ModelDependencyV1` payload) and every model it consumes (with a `null`
payload). The template lives in `ModelDependencyTemplates.cs` and uses the
shared `Token` infrastructure for replacement.
The cross-namespace prefix override step lives in
`tools/Opc.Ua.SourceGeneration.Core/Generators.cs` as
`OverrideDependencyPrefixes`. It runs after `OpenModelDesign` and before
generation so that all downstream emitters see the harmonised prefix/name
values.
Both `GenerateCode` passes (NodeSet2 and ModelDesign) take the referenced
models as a single per-URI map of `ModelDependencyReference` and derive the
decoded payload map from it themselves via `BuildReferencedDependencyMap` —
the reference carries the raw base64 payload and memoises `GetDependency()`,
so the payload map is never threaded as a separate argument.
The payload-import surface lives directly on
`tools/Opc.Ua.SourceGeneration.Core/Schema/ModelDesignValidator.cs` (the
former `ModelDesignValidator.SnapshotImport.cs` partial was folded into the
main file). The validator's `ImportDependency(dependency, prefix, name)` API
queues a `ModelDependencyV1` for ingestion; `ApplyPendingDependencies()`
materialises the carried types into the validator's node table before the
dependency-loading pass walks `AdditionalFiles`, so consumer types can
resolve `BaseType` / `TypeDefinition` / `DataType` references against the
upstream's published types without those upstream models being present in
`AdditionalFiles`. Payloads for models that *are* present as design files in
the current compilation are skipped — the explicit file is authoritative.
After all design files are loaded, the validator links the
payload-materialised data types (base types, field data types, structure /
enumeration classification), so a local `ModelDesign` structure can subtype
a structure published only through a referenced assembly's payload.
Design files loaded as dependencies of the current target skip full
dictionary validation, so the validator links them the same way after
loading: their data types (as above) and their instances — the
`TypeDefinition` / `DataType` node references of the types' children,
variable-type data type restrictions, and method arguments including the
`InputArguments` / `OutputArguments` argument properties. A target
`ModelDesign` can therefore declare an `Object` or `Variable` whose
`TypeDefinition` is a type from another design file and have its inherited
children generate exactly as if the type were declared locally. Access level,
explicit user access, minimum sampling interval, historizing state, and
default values are retained when those inherited children come only from a
referenced assembly's payload.