# 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 ``. 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 ``. 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 `` 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, and DataType fields. 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……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 (with method-argument lists). 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.