Files
UA-.NETStandard/docs/ModelDependencies.md
Marc Schier c4e0721a29 Add a documentation learning path and fix issues found while verifying it (#4555)
## Summary

### Documentation
- Stage the documentation index as a learning path: a first application,
independent client, server, and PubSub paths, then data and services,
companion models, production, advanced integration, and draft or
experimental capabilities.
- Add Getting started (repository samples or NuGet packages), OPC UA
concepts, Your first information model, and a production readiness
checklist.
- Add Native AOT application publishing, a first PubSub publisher and
subscriber exchange, and the UDP transport options.
- Add or expand contents navigation throughout `docs/`, and keep
contributor documentation separate.
- Correct outdated SDK examples and capability guidance, consolidate
repeated explanations into canonical guides, and repair local links and
heading structure.
- Rewrite What's New in 2.0 as a concise feature overview with short
linked details; keep migration instructions in the Migration Guide.
- Expand the feature catalogue with specification IDs, GDS, LDS, the OPC
10000-21 onboarding registrar, ISA-95, xRegistry, OpenUSD, and
sample-local models; distinguish draft status, explain crypto
pluggability, and bring performance guidance forward.
- Address review feedback on wording, active monitored items during
shadow reload, and the `Samples.md` filename and references.
- Regenerate the `opcua-v20-migration` plugin's bundled copy of the
migration guides so it matches the edited `docs/migrate/2.0.x` guides.
- Merge the latest `master`, preserving upstream resource-isolation
guidance within the task-based documentation index.
- Retain deferred Lingity findings where appropriate; this PR does not
claim that every document meets the Lingity score thresholds.

### Fixes found while verifying the guides
Each fix has regression tests that fail without it.
- **Client:** V2 subscriptions compared their lifetime with the
operation timeout instead of the session timeout, and logged a spurious
warning. `ISubscriptionEngineContext`, which is new in 2.0, gains a
`SessionTimeout` property; implementations must add it.
- **PubSub:** a subscriber now registers the `DataSetMetaData`
configured on its readers (OPC 10000-14 6.2.9.4), and the UADP decoder
decodes DataSetMessages that carry no ConfigurationVersion. Before, a
secured subscriber of the reference sample received no DataSets.
- **Source generator:** a ModelDesign method that declares its arguments
inline, without a method type, produced code that did not compile; it
now uses `MethodState`. The generated code of existing models is
unchanged.
- **MinimalClient sample:** waits for its first data-change notification
before it disconnects.

### Known issues, not addressed here
- UDP multicast: the publisher does not send on the interface that the
subscriber joins, and `MulticastLoopback` defaults to false. The PubSub
guide uses unicast for the exchange on one computer.
- The UDP metadata announcement is sent unsecured on secured writer
groups; a secured subscriber drops it with a warning.
- A server does not start in globalization-invariant mode, because
`ResourceManager` creates `CultureInfo("en-US")`. The Native AOT guide
cautions against the mode.

## Validation
- `git diff --check` and
`./.azurepipelines/validate-migration-plugin.ps1` (15 bundled migration
docs validated, plugin manifests synchronized).
- Local Markdown links, anchors, file-link casing, and contents lists
checked across all 87 documents under `docs/`. The index links every
top-level guide and every target of the previous index.
- Both Getting started routes, Your first information model, the PubSub
exchange, and the Native AOT publish of the Getting started client and
server were run as documented. The NuGet route and the tutorial use the
public `2.0.0-preview.5` packages.
- The changed libraries build for all target frameworks in Release
without warnings.
- Tests on .NET 10: Opc.Ua.SourceGeneration.Tests 199 passed;
Opc.Ua.SourceGeneration.Core.Tests 3,977 passed, 8 skipped;
Opc.Ua.Server.Tests `Fluent` category 621 passed; Opc.Ua.Client.Tests
subscription-engine tests 70 passed; Opc.Ua.PubSub.Tests 1,374 passed, 2
skipped; PubSub fuzz replay 145 passed. The new and affected tests also
pass on .NET Framework 4.8.
- Earlier merge validation: 174 resource-isolation tests passed on .NET
10.
- The full solution suite was not run locally.

Review and validation artifacts are maintained outside the repository.

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-09-28 19:19:39 +00:00

172 lines
8.9 KiB
Markdown

# Cross-Assembly Model Dependencies
## Contents
- [Attribute shape](#attribute-shape)
- [Payload wire format (`ModelDependencyV1`)](#payload-wire-format-modeldependencyv1)
- [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. **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.
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
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.
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 | Multiple referenced assemblies provide the same model URI. |
| `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.