# Description _Describe the changes here to communicate to the maintainers why they should accept this pull request. By default - this will become the Commit message after merging and thus define history._ Add provider-backed MachineTool, bundled WMTP, GMS and CranesHoists companions using the existing Machinery/MachineVision fluent, DI and typed-client approach. **The PR is open/non-draft by user choice. Current head `ecc4a7d6ca3e8004cf28ab54da605da302d026bf` is mergeable and has 108 successful checks, six intentionally skipped checks, zero failures and zero pending checks. All five primary workflows completed successfully at that exact head.** | Family | Released specification/model | Packaging | | --- | --- | --- | | MachineTool | OPC 40501-1, 1.02.0 | Model, Server, Client and MCP; normal root versions | | WMTP | OPC 40505, 1.0.0 | Bundled in MachineTool; distinct CLR/wire namespace and independent WMTP-only hosting | | GMS | OPC 40210 reference 1.0.1, normative model 1.0.0 | Model, Server, Client and MCP; normal root versions | | CranesHoists | OPC 40020-1, 1.00 | Model, Server, Client and MCP; all four plus Debug variants forced preview | There are **12 new packable projects**. No separate WMTP package/profile/guide, root version bump, tag, release or package publication is included. Mandatory instance structure is the default; selected optional features require explicit providers. The implementation reuses Machinery results/jobs/files/ProcessValues and Robotics topology, including retained MachineTool Production, defined GMS jobs/parts, modern ISA95/Machinery jobs, shared-owner hosting and isolated instances alongside MachineVision. Runtime and integration fixes preserve raw quality/timestamps, coherent acquisition, namespace ownership, accepted-configuration state, borrowed-resource lifetime, session/node/file cleanup, terminal-value publication, event propagation and endpoint-aware capability publication. Firmware/licence transfer means provider acceptance, not installation/activation. Cancellation does not silently abort another caller's operation. The later upstream MCP expansion is integrated without losing either family set or its default-off privileged/live execution policy. ### Conformance and scope Canonical model inputs are pinned to UA-Nodeset revision `96a08945425d0295edb3607d2a38f6841ef44046`. **GMS upstream exception:** [Annex A.1](https://reference.opcfoundation.org/specs/OPC-40210/annex-a) makes the released NodeSet normative, but §12.3.2.7 requires undefined `GMS_ProductionStatisticsType`. Inherited statistics and defined jobs/parts are implemented without inventing a standard type/NodeId; the complete GMS Production facet is withheld. The requirement ledger records **102 positively evidenced CU rows, 96 directly negative-tested rows and six explicit negative-evidence limitations**. All 648 test-class/name/TRX links were independently resolved to passing cases. The six limitations concern Stacklight, retained legacy PowerOnDuration, Machinery PowerOnDuration, WorkingUnit, Obligation and MaintenanceMode. They are not claims that every invalid-input or hardware-control behavior was tested. The undefined GMS requirement is outside the 102 defined CUs. No external certification claim is made. Retained baseline complete-assembly line coverage, with generated code retained and no source exclusions: | Family | Model | Server | Client | | --- | ---: | ---: | ---: | | MachineTool/WMTP | 83.80% | 91.98% | 84.74% | | GMS | 95.51% | 90.63% | 88.38% | | CranesHoists | 92.76% | 94.45% | 92.24% | These nine measurements predate the later focused review fixes and merges; they are not relabelled as new `ecc4a7d6c` percentages. The current head's separate CI code-coverage check succeeded. No new exclusions or threshold relaxations were introduced. ## Related Issues - Fixes [#4764](https://github.com/OPCFoundation/UA-.NETStandard/issues/4764), the implementation/design and acceptance tracker. ## 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. - [x] 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. - [x] I have addressed **all** PR feedback received.
12 KiB
Cross-Assembly Model Dependencies
Contents
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
Namespacesclass entry for the model, - and — on self-declaration entries only — a base64-encoded Deflate-compressed
ModelDependencyV1type-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:
- Suppress the missing-dependency error in nodeset dependency collection when the missing namespace is known to be supplied by a referenced assembly.
- 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.
- Same C# prefix → local generation is silently skipped (the referenced
assembly already supplies the types; duplicate emission would cause
- 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'sPrefix(andName) 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. - 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. - 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/DataTypereferences in the consumer's own models then resolve against the imported types without needing the upstream NodeSet2/ModelDesign XML inAdditionalFiles.
Attribute shape
Opc.Ua.Vision produces the Perception and Part 1 Machine Vision
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):
[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):
ModelUristring + 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.