# Description Implements the **Asset Administration Shell** companion specifications on top of `marcschier/wot-type-binding`, in two halves that a Server chooses between: * **AAS V3** — the [draft](https://github.com/marcschier/opcua-drafts/tree/main/companion-specs/AAS) (`http://opcfoundation.org/UA/I4AAS/v3/`), all sixteen conformance units: the metamodel, the registry built on the abstract xRegistry base, generational materialization, federation, packages with digest-bound integrity, the Digital Product Passport vocabulary, and the Annex F bridge to WoT Thing Descriptions. * **AAS V2** — the published [OPC 30270](https://reference.opcfoundation.org/specs/OPC-30270/full) (`http://opcfoundation.org/UA/I4AAS/`), which maps the AAS V2.0.1 metamodel: object model, ingestion of XML, JSON and AASX, materialization, server and client. The two are different metamodel generations rather than two revisions of one model — V2 keeps `Asset` as a first-class Identifiable, `identification.idType`, Views and ordered collections, none of which survive into V3 — so they are **alternatives**. A Server hosts one or the other; this is documented rather than enforced at startup. Each is named for the generation it maps (`Opc.Ua.Aas.V2`, `Opc.Ua.Aas.V3`, `AddAasV2Server`, `AddAasV3Server`), and what genuinely serves both carries no marker and sits at the root of `Opc.Ua.Aas`: the deterministic NodeId encoding and idShortPath conventions, `AasOptional<T>`, the digest rule, the materialization diagnostics, the AASX container walk, and the value and operation providers, which are keyed on `NodeId` and `Variant` and so carry no version at all. ## What is in here **New libraries**: `Opc.Ua.Aas`, `Opc.Ua.Aas.Server`, `Opc.Ua.Aas.Client`, `Opc.Ua.Aas.WoT`, plus `samples/Aas` and `docs/Aas.md`. **Enabling changes to existing code**, each needed by the above: * The OPC UA `Decimal` DataType added to `Opc.Ua.Types`, with its Part 6 §5.1.10 wire encoding. `BinaryDecoder` gained one internal member, `TryReadRemainingBodyBytes`, because a trailing field with no self-describing length cannot otherwise be read; `Decimal.Decode` is its only caller. Part 6 Table 3 types the unscaled `Value` as an **OctetString whose byte count is `Length` − 2**, not a length-prefixed `ByteString`, so it carries no length of its own — `DecimalEncodingTests` pins the body byte-for-byte against that mistake, and against the `Length ≤ 2` values the same table calls invalid. * The xRegistry projection engine extracted into `Opc.Ua.XRegistry.Server` and `Opc.Ua.WotCon.Server` migrated onto it (−848/+387 lines), with the WoT Connectivity tests passing **unedited**. * Two source-generator fixes, both of which made a model impossible to build (see below). ## Defects found and fixed along the way Running the stack end to end, rather than trusting per-area test suites, surfaced defects that no unit test caught. The ones worth a maintainer's attention: * **The generator emitted non-terminating constructors.** A structure field with no declared default was initialized with `new T()`, so a self-referential DataType (`AASReferenceDataType.referredSemanticId`) recursed until the stack overflowed and `AddOpcUaAas` could not load the model at all. The test host *died* rather than failing. Only the field that closes a cycle is now left null. * **The generator omitted `new` on a redeclared placeholder.** `AASOrderedSubmodelElementCollectionType` redeclares the `SubmodelElement` slot of its base type, and the generated `Add` method hid the base one without saying so, so the V2 model would not compile. No existing model could have hit either defect — both are build failures, so anything that builds today is unaffected. * **The metamodel half had never worked outside its unit tests**: the emitted NodeSet declared no reference-type aliases and was rejected by the loader; the Mandatory `Invoke` Method was never emitted, because a NodeSet is imported as written rather than instantiated from its type; the runtime bound at the authored namespace index instead of the one the Server assigns; and `MethodDeclarationId` was addressed in the wrong namespace. Every materializer test inspected the in-memory object graph, which cannot see whether the graph is loadable — two tests now import it exactly as `RuntimeNodeSetNodeManager` does. * **The client could not see any Property member.** It browsed `HasComponent` with subtypes, but `HasProperty` is a sibling under `Aggregates`, so reading a value failed and invoking failed argument validation. * **Every repeated registry write forked a duplicate group and resource** instead of adding a version, because the identifier was re-derived against a sibling set that already contained the entity itself. This also made the persistence bounds unreachable. * `.gitignore`'s `**/packages/*` silently swallowed the whole package-integrity implementation and its tests, which would have vanished on a fresh clone. ## Specification findings Nine findings against the V3 draft are fixed in [marcschier/opcua-drafts#75](https://github.com/marcschier/opcua-drafts/pull/75). The substantive one: clause 6.1.5's absent-versus-empty rule was **unsatisfiable** for five container fields that materialize as parent components with no node of their own, which made `AAS-LosslessRoundTrip` unachievable. OPC 30270 contradicts itself twice. Both are recorded in `AasV2ConformanceUnits.cs` and the documentation rather than smoothed over: Table 83 spells the unit `I4AAS MultiLangaugeProperty` where Table 85 spells it correctly, and Table 85 lists an `I4AAS Security` unit that Table 83 never defines. Table 84, which would assign profile URIs, is empty, so `ServerProfileArray` gains no I4AAS entry. ## Known limitation Calling an `Operation` **with arguments** answers `BadTooManyArguments`. Method resolution works from both the instance NodeId and the `MethodDeclarationId`, and the argument definitions are published, but `NodeState.AddChild` does not route through `MethodState.FindChild`, so a Method imported from a NodeSet leaves its typed `InputArguments` null. Closing this means changing how the shared NodeSet import claims typed children, which affects every consumer of runtime NodeSets and is out of scope here. It is documented in `docs/Aas.md` §11, the conformance matrix marks `AAS-OperationInvoke` partial rather than claiming it, and `samples/Aas` exercises the path and reports the status rather than working around it. V2 is unaffected — its `Operation` is an OptionalPlaceholder with no declared arguments. ## Related Issues - No tracking issue yet. Opening as a **draft** for design discussion first, per the ADR guidance below; happy to open one and link it once the shape is agreed. ## Checklist - [x] 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. - [x] I have verified that my changes do not introduce (new) build or analyzer warnings. - [x] 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. ### Verification | Check | Result | |---|---| | AAS tests | **670 passed, 0 failed** on net472, net48, net8.0, net9.0, net10.0 | | Coverage | Client 80.8 %, Server 81.2 %, `Opc.Ua.Aas` 82.0 %, WoT 82.4 % — all above the 80 % gate | | Libraries | `Opc.Ua.Aas`, `.Server`, `.Client`, `.WoT` build clean on every target framework, zero warnings | | Regression | `Opc.Ua.Types` 8677, `Opc.Ua.WotCon` 1094, `Opc.Ua.XRegistry` 198, `Opc.Ua.Server` 4050 — all green | | Sample | `samples/Aas` builds warning-free and runs end to end | | Merge validation | Solution builds clean on net10.0 and net48; WoT Types 504, WotCon 1111, AAS 670, XRegistry 198 tests passed | | CI | All 99 checks green on `ff9298e44`; only the CLA signature status is outstanding | ### Review pass A full review against both specifications, and for API consistency and correctness, produced twelve fixes. Each one has a test that fails without it. | Defect | Where | Why it mattered | |---|---|---| | V2 projection aborted on **every** `File` read from a document | `AasV2EnvironmentRuntime` | The runtime bound `File/Value` unconditionally, the materializer emitted it only with content, and the builder throws `BadNotFound` for an unknown NodeId — aborting the whole registration. No reader ever populates `AasFile.File`, so no AAS V2 document could be hosted at all. | | `aas-suppl` anchored on the wrong part | `AasxPackageReaderCore`, `AasxPackageWriter` | IDTA and the Package Explorer anchor it on the environment part; we used the origin part. Every real `.aasx` returned zero supplementary files, silently. Affects both halves. | | Federation failed closed on pooled connections | `AasFederation` | `ConnectCallback` fires only for new connections, so the second fetch to an origin — and all of net472/net48/netstandard2.1 — failed with a security-flavoured error. The peer is now validated *inside* the callback, which makes reuse safe by construction. | | Malformed `Decimal` escaped as `ArgumentOutOfRangeException` | `BinaryDecoder` | A declared body shorter than `Scale` produced a negative read length. Two of the three test cases had been passing for the wrong reason. | | V2 and V3 cross-wired their value providers | `OpcUaAasServerBuilderExtensions` | Both registered `IAasValueProvider`; whichever was added first served both AddressSpaces. V2 now has its own contract, so co-hosting is harmless rather than silently wrong. `EnvironmentFolder` was also bound and ignored on the V2 path. | | Export filter skipped three nesting kinds | `AasEnvironmentExporter` | Content under `Entity`, `AnnotatedRelationshipElement` and `Operation` was exported unevaluated *and* stamped with a Digest presenting it as complete (clause 6.5.10). List authorization paths also shifted after a denial. | | Concealment held at the Method but not in the projection | `AasRegistryModel` | `GetSubmodel` answered `BadNotFound` while an anonymous Browse revealed the node, its identifier, semanticId and digest (clause 6.5.7). | | File handles were unsynchronized and unowned | `AasElementFileManager` | The async node manager holds no lock, so concurrent Calls raced a bare `Dictionary`; sequential handles let any Session read, seek or commit another's. Writes were also unbounded. | | Projection host was unimplementable outside the stack | `AasEnvironmentProjection` | The handle demanded a type with an internal constructor — our own tests used `FormatterServices.GetUninitializedObject` to get round it. Now mirrors WotCon's interface-typed registration. | | Embedded `FileType` omitted 7 of 10 Mandatory members | `AasEnvironmentMaterializer` (V2) | OPC 10000-3 6.4.4 requires them all; without `Size` and `OpenCount` a Client cannot size a read or detect an abandoned handle. | | Round-trip oracle ignored most of the model | `AasRoundTripComparer` | `Blob`, `File`, `ReferenceElement` and `BasicEventElement` had no case arm, so their fields compared equal by falling off the switch; `valueType`, `semanticId`, `entityType` and `Key.type` were never read. The clause 6.4 losslessness claim rested on it. | Known, unchanged: calling an `Operation` with arguments answers `BadTooManyArguments`, because `NodeState.AddChild` does not route through `MethodState.FindChild`. Documented in `docs/Aas.md` §11. Not addressed in this pass, and worth a maintainer decision: OPC 30270 §9.2 says all NodeIds of Nodes not defined in the companion specification **shall not** use the standard namespaces, and Table 87 marks a vendor-specific instance namespace Mandatory. Both halves currently mint instances in the companion namespace. Separating them touches NodeId minting, the runtime and the client together, so it is better done deliberately than folded into a review pass. This is a large change and touches shared code (`Opc.Ua.Types`, the source generator, the xRegistry projection). It is deliberately a draft: the enabling changes are separable if maintainers would prefer them split into their own PRs. The latest `master` is merged into this branch in `0dd0ff0e1`. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9e6a5abf-3299-4cd1-9855-010fedbf0ad8 Copilot-Session: c3d88950-4a65-4ff6-87d2-1f545a875c9b Copilot-Session: b8ab3763-b9bd-46ae-b0ef-045db26e5782
36 KiB
OPC UA for Asset Administration Shell (I4AAS)
This repository implements two OPC UA for Asset Administration Shell metamodel generations in one assembly set. They are separate alternatives, not revision levels of one model:
- OPC 30270, "OPC UA for Asset Administration Shell" maps the AAS V2.0.1
metamodel into namespace
http://opcfoundation.org/UA/I4AAS/and lives inOpc.Ua.Aas.V2. - OPC UA for Asset Administration Shell V3 draft (release
3.00-draft3) maps the AAS V3 draft metamodel into namespacehttp://opcfoundation.org/UA/I4AAS/v3/and lives inOpc.Ua.Aas.V3.
The shared root Opc.Ua.Aas namespace contains only code that is genuinely
metamodel-neutral: deterministic NodeId encoding and idShortPath handling,
AasOptional<T>, digest calculation, materialization diagnostics, the AASX
container walk and the value/operation provider contracts.
| Project | Purpose |
|---|---|
Opc.Ua.Aas |
Source-generated I4AAS V2 and V3 models from src/Opc.Ua.Aas/Design, the V2 object model and ingestion readers, the V3 object model and read/write serializers, shared identity/value helpers, materialization diagnostics and AASX container support. |
Opc.Ua.Aas.Server |
Server-side projection for both metamodels over INodeManagerLifecycle; V2 ingestion materialization and runtime callbacks; V3 registry service, generational materialization, environment export, federation, package integrity and provider contracts. |
Opc.Ua.Aas.Client |
High-level V2 and V3 clients for deterministic metamodel NodeIds, plus the V3 AAS registry client and typed group/resource clients. |
Opc.Ua.Aas.WoT |
The V3 informative Annex F bridge between an AAS environment and a WoT Connectivity Thing Description projection. |
Opc.Ua.Aas.Tests |
NUnit tests for V2 model ingestion/materialization/server/client behavior and V3 model, identity, values, serialization, registry, materialization, packages, federation, DPP and WoT bridge behavior. |
The V3 AAS implementation builds on the shared xRegistry registry base and reuses the runtime NodeManager lifecycle described in Runtime NodeSets. V2 is the OPC 30270 metamodel half only: it has no registry, packages, federation, DPP or WoT bridge. The IDTA HTTP API described by Annex G of the V3 draft is intentionally out of scope; this implementation exposes AAS through OPC UA services and, for V3, the xRegistry-compatible registry AddressSpace.
Choosing V2 or V3
Host one AAS generation in a server: either V2 or V3. The stack does not enforce that rule at startup, because the two NodeManagers can technically be registered side by side, but the product decision is that a deployment chooses one alternative and exposes one metamodel generation. Running both would publish two different AAS metamodels for the same product domain and leave clients to guess which one is authoritative.
Choose V2 when the published OPC 30270 specification and the AAS V2.0.1
metamodel are the interoperability target. The implementation reads V2 JSON, XML
and AASX documents, materializes them into the http://opcfoundation.org/UA/I4AAS/
AddressSpace, serves Read + Write + Call, and publishes the OPC 30270 Table 83
conformance units. It does not export the AddressSpace back to AAS documents,
because OPC 30270 does not require a lossless round trip.
Choose V3 when the draft AAS V3 metamodel and everything built on the draft registry half are required: registry, updateable materialization, environment export, packages, package integrity, federation, DPP vocabulary and the WoT bridge. V3 reads and writes JSON, XML and AASX because its draft requires a lossless document/AddressSpace round trip.
1. V3 core architecture
The V3 draft companion specification has two independent halves:
- Metamodel half — shells, submodels, concept descriptions and submodel
elements are materialized as typed OPC UA nodes, following clauses 6.1 and
6.2. The metamodel surface is Read + Write + Call: value Variables are served
through an injectable
IAasValueProvider, andAASOperationType.Invokeis served through an injectableIAasOperationHandler. - Registry half — a catalogue of shell, submodel, concept-description,
package and environment documents is served as folders of
FileTyperesources using the xRegistry model (clause 6.5). The registry answers discovery and document-retrieval questions even when the live metamodel tree is hosted elsewhere.
Where both halves are hosted, the stored documents are canonical and the live
nodes are derived. A write to a materialized value writes back to the document
and bumps a version; a refresh derives a new generation from the document. The
default retirement policy is AasProjectionRetirementPolicy.Graceful, so
existing monitored items can keep reading the old generation until they drain.
The V3 half also adds the OPC UA Decimal DataType wire encoding needed
by clause 6.3.1, and the source generator fix needed for recursive structure
DataTypes.
2. Shared identity and V3 value fidelity
Deterministic NodeIds and BrowseNames
Clause 6.1.3 assigns deterministic String NodeIds. AasNodeIdEncoding is the
public implementation of the reversible, node-kind-discriminated encoding:
string shellId = AasNodeIdEncoding.CreateIdentifiableId(
AasNodeKind.Shell,
"https://example.com/aas/42");
string elementId = AasNodeIdEncoding.CreateElementId(
"https://example.com/submodels/nameplate",
"Nameplate.ManufacturerName");
if (AasNodeIdEncoding.TryParse(elementId, out AasParsedNodeId parsed))
{
Console.WriteLine(parsed.IdShortPath);
}
AasIdShortPath builds and parses the metamodel idShortPath convention, and
AasBrowseNameAllocator implements the derived BrowseName rule for
identifiables that do not carry idShort.
xsd value mapping
AasXsdTypeMap maps every AASDataTypeDefXsdDataType value to exactly one OPC
UA DataType, as required by clauses 6.1.2 and 6.3.1. AasLexicalCanonicalizer
parses and canonicalizes xsd lexical values, and AasValueSpaceComparer
compares values in the XML Schema value space rather than by string equality:
ExpandedNodeId dataTypeId = Opc.Ua.Aas.V3.AasXsdTypeMap.ToDataTypeId(
Opc.Ua.Aas.V3.AASDataTypeDefXsdDataType.Decimal);
bool parsed = Opc.Ua.Aas.V3.AasLexicalCanonicalizer.TryParse(
"1.500000",
Opc.Ua.Aas.V3.AASDataTypeDefXsdDataType.Decimal,
out Variant value,
out string? error);
bool equivalent = Opc.Ua.Aas.V3.AasValueSpaceComparer.AreEquivalent(
"1.500000",
"1.5",
Opc.Ua.Aas.V3.AASDataTypeDefXsdDataType.Decimal);
The object model uses AasOptional<T> so absent remains distinct from
present-but-empty (clause 6.1.5). Serializers and round-trip comparison preserve
that distinction.
3. V3 reading, writing and materializing AAS documents
V3 AAS JSON, XML and AASX are read into the same object model. JSON and XML readers return diagnostics instead of throwing for malformed documents:
await using FileStream input = File.OpenRead("environment.json");
Opc.Ua.Aas.V3.AasDocumentReadResult read = await new Opc.Ua.Aas.V3.AasJsonReader()
.ReadAsync(input, ct);
if (!read.Succeeded)
{
Console.WriteLine(read.Error);
return;
}
Opc.Ua.Aas.AasMaterializationResult materialized = Opc.Ua.Aas.V3.AasEnvironmentMaterializer
.Materialize(read.Environment!);
await using FileStream output = File.Create("environment.aasx");
await new Opc.Ua.Aas.V3.AasxPackageWriter().WriteAsync(output, read.Environment!, ct);
The inverse path serializes a clause 6.1.6 NodeSet back to an AAS environment:
Opc.Ua.Aas.V3.AasSerializationResult serialized = Opc.Ua.Aas.V3.AasEnvironmentSerializer
.Serialize(materialized.NodeSet);
await using FileStream json = File.Create("roundtrip.json");
await new Opc.Ua.Aas.V3.AasJsonWriter().WriteAsync(json, serialized.Environment, ct);
The round-trip guarantee in clause 6.4 is equivalence, not byte identity. A
canonical value rewrite such as "1.500000" to "1.5" is equivalent for
xs:decimal; losing digits, conflating absent with empty or changing ordered
arrays is not equivalent.
4. Hosting the V3 metamodel server
AddAasV3Server registers the environment NodeManager, default providers and the
runtime projection host. It must be used with the normal server hosting feature:
services.AddSingleton<IAasValueProvider, MyAasValueProvider>();
services
.AddOpcUa()
.AddServer(options =>
{
options.EndpointUrls.Add("opc.tcp://localhost:4840/AasServer");
})
.Services
.AddOpcUa()
.AddAasV3Server(options =>
{
options.EnvironmentFolder = "aas-environments";
options.RetirementPolicy = AasProjectionRetirementPolicy.Graceful;
})
.AddOperationHandler<MyAasOperationHandler>();
EnvironmentFolder is read by FolderAasEnvironmentProvider; when it is not
set, InMemoryAasEnvironmentProvider is used. The metamodel server can also be
constructed directly with AasEnvironmentNodeManagerFactory when an application
manages StandardServer.NodeManagerFactories itself.
The runtime uses IAasValueProvider for reads and writes of materialized value
Variables:
public sealed class MyAasValueProvider : IAasValueProvider
{
public ValueTask<AasValueReadResult> ReadValueAsync(
NodeId valueNodeId,
CancellationToken cancellationToken = default)
{
return new ValueTask<AasValueReadResult>(new AasValueReadResult(
ServiceResult.Good,
Variant.From("running"),
StatusCodes.Good,
DateTime.UtcNow));
}
public ValueTask<ServiceResult> WriteValueAsync(
NodeId valueNodeId,
Variant value,
CancellationToken cancellationToken = default)
{
return new ValueTask<ServiceResult>(ServiceResult.Good);
}
}
IAasOperationHandler.InvokeAsync receives already-marshalled input and
in-output values and returns the four outputs required by AASOperationType.Invoke.
A failed AAS operation is represented as a Good OPC UA Call with Success = false
and a diagnostic, which is distinct from a failed Call service result.
5. Using the V3 metamodel client
Opc.Ua.Aas.Client.AasClient computes the deterministic NodeIds of clause 6.1.3, opens the
generated *TypeClient proxies, reads values with their declared xsd type and
writes lexical values using that type:
var client = new Opc.Ua.Aas.Client.AasClient(session, aasNamespaceIndex, telemetry);
Opc.Ua.Aas.V3.AASPropertyTypeClient temperature = client.OpenProperty(
"https://example.com/submodels/process",
"Temperature");
Opc.Ua.Aas.Client.AasValueReadResult current = await client.ReadValueAsync(temperature.ObjectId, ct);
Console.WriteLine($"{current.ValueType}: {current.LexicalValue}");
StatusCode writeStatus = await client.WriteLexicalValueAsync(
temperature.ObjectId,
"42.5",
ct);
Operations keep the OPC UA Call status separate from the AAS success flag:
Opc.Ua.Aas.V3.AASOperationTypeClient reset = client.OpenOperation(
"https://example.com/submodels/process",
"Reset");
Opc.Ua.Aas.Client.AasOperationInvokeResult result = await client.InvokeAsync(
reset.ObjectId,
ArrayOf<Variant>.Empty,
ArrayOf<Variant>.Empty,
0,
ct);
if (StatusCode.IsGood(result.CallStatusCode) && !result.Success)
{
Console.WriteLine(result.Diagnostic);
}
Dependency injection registers either a factory over an already connected
ManagedSession, or a lazy factory that asks the registered OPC UA client for a
session:
services
.AddOpcUa()
.AddClient(options => { /* endpoint and application options */ })
.AddAasV3Client(options =>
{
options.InstanceNamespaceUri = "urn:example:aas:instances";
});
Func<CancellationToken, Task<Opc.Ua.Aas.Client.AasClient>> clientFactory =
sp.GetRequiredService<Func<CancellationToken, Task<Opc.Ua.Aas.Client.AasClient>>>();
Opc.Ua.Aas.Client.AasClient aas = await clientFactory(ct);
6. V3 AAS registry
The V3 AAS registry is a concrete xRegistry specialization (clause 6.5). It exposes
the well-known AASRegistry object under Server, projects shell groups,
submodel files, concept dictionaries, packages and environment documents, and
wires the AAS discovery methods:
LookupShellsByAssetLink(Name, Value)returns matching shell group NodeIds.GetSubmodel(SubmodelIdentifier)returns the document bytes,FormatandContentType, while preservingBad_UserAccessDeniedandBad_NotFoundas meaningful outcomes.
The service keeps immutable snapshots. Every mutation advances Generation, and
existing readers keep their previous snapshot:
var registry = new Opc.Ua.Aas.Server.Registry.AasRegistryService();
await registry.UpsertResourceAsync(new Opc.Ua.Aas.Server.Registry.AasUpsertResourceRequest
{
GroupSourceIdentity = "https://example.com/aas/42",
ResourceSourceIdentity = "https://example.com/submodels/nameplate",
GroupKind = Opc.Ua.Aas.Server.Registry.AasRegistryEntityKind.Shell,
ResourceKind = Opc.Ua.Aas.Server.Registry.AasRegistryEntityKind.Submodel,
Content = ByteString.From(File.ReadAllBytes("nameplate.json")),
ContentType = "application/aas+json",
Format = "aas/3.0+json"
});
Opc.Ua.Aas.Server.Registry.AasGetSubmodelResult document = await registry.GetSubmodelAsync(
"https://example.com/submodels/nameplate");
To expose the registry in a manually configured server, add the registry node manager factory to the server's node manager factories:
var registry = new Opc.Ua.Aas.Server.Registry.AasRegistryService();
server.NodeManagerFactories.Add(
new Opc.Ua.Aas.Server.Registry.AasRegistryNodeManagerFactory(registry));
The V3 AAS registry projection uses the shared XRegistryProjectionEngine from
Opc.Ua.XRegistry.Server, so base xRegistry lifecycle, labels and file-transfer
semantics match the WoT registry implementation.
Registry client
AasRegistryClient derives from XRegistryClient. It resolves the well-known
root, inherits the base xRegistry lifecycle helpers, and adds the AAS discovery
methods and typed resource clients:
Opc.Ua.Aas.Client.Registry.AasRegistryClient registry =
await Opc.Ua.Aas.Client.Registry.AasRegistryClient.ForServerAsync(
session,
telemetry,
ct);
ArrayOf<NodeId> shells = await registry.LookupShellsByAssetLinkAsync(
"serial",
"42",
ct);
Opc.Ua.Aas.Client.Registry.AasGetSubmodelDocumentResult submodel = await registry.GetSubmodelAsync(
"https://example.com/submodels/nameplate",
ct);
if (StatusCode.IsGood(submodel.StatusCode))
{
File.WriteAllBytes("nameplate.json", submodel.Document.ToArray());
}
The DI shortcut is AddAasV3RegistryClient:
services
.AddOpcUa()
.AddClient(options => { /* endpoint and application options */ })
.AddAasV3RegistryClient();
7. V3 updateable registry and environment export
AasMaterializationCoordinator implements the optional updateable-registry
profile of clause 6.5.9. It reads AasMaterializationDocument entries from an
IAasMaterializationDocumentStore, prepares shadow generations, atomically
switches them through an IAasEnvironmentProjectionHost, records per-document
outcomes and skips unchanged digests unless Force is set.
Important behaviours:
- Documents are canonical. Materialized nodes can be rebuilt from the stored documents and the specification. No extra per-node state is required.
- Switches are atomic. Shadow generations are not browsable and do not emit model-change events until committed.
- Failures retain the previous generation. Invalid desired versions stay in the registry with diagnostics while the previous active generation continues serving.
- Graceful retirement is the default. Existing monitored items survive an
update until retained work drains;
Immediateinvalidates affected monitored items withBadNodeIdUnknown. - Value write-back bumps versions. Writes through materialized nodes update the stored document and avoid redundant projection when the digest is unchanged.
AasEnvironmentExporter implements clause 6.5.10. It exports the whole
materialized environment as AAS JSON, XML or AASX and filters the result per the
calling session's read access. A filtered export does not publish a digest,
because the bytes depend on the caller.
8. V3 federation, packages, DPP and WoT
Federation
AAS federation follows clause 6.5.6. AasFederationIdentity derives proxy
identity from the remote AAS identifier attributes, never from the local endpoint.
AasResourceUrlFederationResolver and AasOpcUaFederationResolver apply a
fail-closed egress policy: host/scheme controls, DNS and connected-address
checks, redirect/time/size bounds, no ambient credentials and OPC UA peer
identity validation.
Packages
AasPackageIntegrity and IAasPackageStore implement the package integrity
rules in clause 6.5.4. Package versions publish immutable Digest and
DigestAlg; consumers verify the returned blob by default. OCI-backed packages
verify the manifest digest, require exactly one package layer descriptor, keep
mutable tags separate from immutable VersionIds and keep referrers out of the
package version collection.
Digital Product Passport vocabulary
The Dpp/ namespace provides a small Digital Product Passport vocabulary layer.
AasDppDisclosurePolicy maps DPP regulatory classes to the AAS Public and
Controlled disclosure tiers, while preserving the finer disclosure class and
authorization text for consumers.
WoT bridge
Opc.Ua.Aas.WoT implements the informative Annex F projection. AasWotBridge
reads a bundle of Thing Descriptions carrying the AAS and OPC UA WoT Binding
terms, resolves the type binding (@type, ua:HasTypeDefinition or both), and
projects the object graph described by Annex F. It does not replace the WoT
Connectivity registry; it is the AAS-specific bridge between the two companion
specifications.
9. Security and disclosure notes
Registry writes and file updates inherit the xRegistry security model. The AAS
GetSubmodel convenience method deliberately re-checks authorization on the
target resource; permission to call the method is never substituted for
permission to read the file. Denied responses return no bytes, no parse metadata
and no size or digest. Concealed targets and nonexistent targets follow the same
externally visible branch.
DisclosureTier and Authorization are metadata, not credentials. The
implemented DPP policy maps regulatory classes to public or controlled tiers,
and tests assert that AASAuthorizationOptionDataType carries configuration
only, not passwords, tokens, keys or secrets.
Federation resolves untrusted metadata and therefore fails closed. A resolver policy, DNS, redirect, credential, certificate, identity or resource-bound failure terminates resolution without returning or caching bytes from the rejected destination.
10. V3 conformance matrix
The following table maps the sixteen V3 draft clause 10 conformance units to tests in
tests/Opc.Ua.Aas.Tests. Each cited test was checked against the behaviour it
asserts; no unit is intentionally left unmapped.
A Server publishes the units it actually enables. Both NodeManagers implement
IConformanceContributor, and the server aggregates their contributions into
Server/ServerCapabilities/ConformanceUnits, so the published set follows what
was wired up rather than what the assemblies could in principle do:
AAS-OperationInvoke appears only when an operation handler other than
DefaultAasOperationHandler is configured, and AAS-Packages (with
AAS-PackageIntegrity, which clause 10 requires alongside it) only when a
package group is present. The unit names are held in AasConformanceUnits.
ServerProfileArray gains no AAS entry. Clause 10 names conformance units but
assigns no server profile URIs, and the IDTA profile identifier of Annex G is
qualified on also implementing the HTTP binding, which this stack does not — so
publishing it would claim conformance that is not met.
| Conformance unit | Demonstrating tests |
|---|---|
AAS-Metamodel |
Materialization.AasEnvironmentMaterializerTests.EverySubmodelElementTypeMaterializesWithItsAnnexBMembers; Client.AasClientTests.OpenMethodsResolveByIdentifierAndIdShortPath |
AAS-SubmodelElements |
Materialization.AasEnvironmentMaterializerTests.EverySubmodelElementTypeMaterializesWithItsAnnexBMembers; Serialization.AasSerializationTests.JsonRoundTripPreservesEverySubmodelElementType |
AAS-ValueFidelity |
Values.AasXsdTypeMapTests.EveryDeclaredXsdTypeIsAssignedADataType; Values.AasXsdTypeMapTests.NoDataTypeIsAssignedToTwoXsdTypes; Values.AasLexicalCanonicalizerTests.CanonicalizationIsIdempotent; Client.AasClientTests.ReadAndWriteValueUseDeclaredXsdType |
AAS-InstanceMaterialization |
Materialization.AasEnvironmentMaterializerTests.OrderedListUsesHasOrderedComponentAndUnorderedListUsesHasComponent; Materialization.AasEnvironmentMaterializerTests.ListMembersAreNamedByIndexAndCarrySequentialIndex; Materialization.AasEnvironmentMaterializerTests.MaterializingTheSameEnvironmentTwiceProducesByteIdenticalNodeSet |
AAS-LosslessRoundTrip |
Materialization.AasEnvironmentRoundTripTests.MaterializeThenSerializeProducesEquivalentEnvironment; Materialization.AasEnvironmentRoundTripTests.SerializeThenMaterializeProducesEquivalentNodeSet; Materialization.AasEnvironmentRoundTripTests.ConflatingAbsentWithEmptyIsReported; Materialization.AasEnvironmentRoundTripTests.RewritingValueIntoCanonicalLexicalFormIsNotReported |
AAS-Registry |
Registry.AasRegistryProjectionTests.ProjectionExposesEverySourceIdentityVerbatim; Registry.AasRegistryProjectionTests.ConcreteRegistryMethodsCarryDeclarationIds; Registry.AasRegistryServiceTests.UpsertResourceAdvancesGenerationAndPreservesExistingSnapshots |
AAS-RegistryIdentity |
Registry.AasRegistryServiceTests.IdentifierConstructionMatchesXRegistryAndIsInvariantAcrossVersions; Registry.AasRegistryProjectionTests.ProjectionExposesEverySourceIdentityVerbatim; Client.AasRegistryClientTests.TypedGroupAndResourceClientsExposeSourceIdentities |
AAS-RegistryVersioning |
Registry.AasRegistryServiceTests.VersionOrderingResolvesNewestVersionNotLaterThanMoment; Client.AasRegistryClientTests.VersionsResolveNewestVersionNotLaterThanMoment |
AAS-Discovery |
Registry.AasRegistryServiceTests.LookupShellsByAssetLinkFindsShellAndBoundsUnauthenticatedResults; Registry.AasRegistryGetSubmodelContractTests.AuthorizedCallerGetsDocumentFormatAndContentType; Client.AasRegistryClientTests.LookupShellsByAssetLinkReturnsHitAndMiss; Client.AasRegistryClientTests.GetSubmodelReturnsDocumentOnSuccess |
AAS-OperationInvoke (partial, see section 11) |
Server.AasRuntimeInvocationTests.InvokeWithCorrectArityReturnsOperationOutputsAsync; Server.AasRuntimeInvocationTests.InvokeWithArityMismatchReturnsBadInvalidArgumentAsync; Server.AasRuntimeInvocationTests.InvokeOperationFailureReturnsGoodCallStatusAndFalseSuccessAsync; Client.AasClientTests.InvokeMarshalsArguments |
AAS-Federation |
Federation.AasFederationTests.ProxyIdentityRetainsRemoteAttributesAndIgnoresLocalEndpoint; Federation.AasFederationTests.EgressPolicyRejectsRestrictedAddressAndReturnsNoBytes; Federation.AasFederationTests.ConnectedAddressRevalidationBlocksDnsRebinding; Federation.AasFederationTests.OpcUaPeerIdentityMismatchTerminatesWithoutRemoteRead |
AAS-DisclosureTiers |
Registry.AasRegistryServiceTests.DisclosureTierAndAuthorizationAdvertiseConfigurationOnly; Registry.AasRegistryServiceTests.ControlledDisclosureTierRequiresAuthenticationButPublicDoesNot; Registry.AasRegistryGetSubmodelContractTests.TargetRolePermissionsDenialReturnsUserAccessDenied; Dpp.AasDppDisclosurePolicyTests.RegulatoryClassesMapToDisclosureTiers |
AAS-UpdateableRegistry |
Updateable.AasUpdateableRegistryMaterializationTests.ShadowGenerationStaysInvisibleUntilAtomicSwitch; Updateable.AasUpdateableRegistryMaterializationTests.GracefulRetirementServesExistingMonitoredItemUntilDrain; Updateable.AasUpdateableRegistryMaterializationTests.ValidationFailureKeepsPreviousGenerationAndDivergesVersions; Updateable.AasUpdateableRegistryMaterializationTests.ValueWriteBackBumpsVersionWithoutRedundantMaterialization |
AAS-EnvironmentExport |
Updateable.AasUpdateableRegistryMaterializationTests.EnvironmentExportIsFilteredPerSessionWithNoDigest; Serialization.AasxPackageTests.JsonPackageRoundTripsEnvironmentAndSupplementaryFiles; Serialization.AasxPackageTests.XmlPackageReadsEnvironment |
AAS-Packages |
Packaging.AasPackageIntegrityTests.PublishedVersionExposesImmutableDigestAndDigestAlg; Packaging.AasPackageIntegrityTests.MovingTagToNewManifestRetainsDistinctVersions; Packaging.AasPackageIntegrityTests.ReferrerDoesNotMutatePackageVersionCollectionOrLifecycle; Client.AasRegistryClientTests.PackageDownloadVerifiesDigestByDefault |
AAS-PackageIntegrity |
Packaging.AasPackageIntegrityTests.DigestAlgorithmsAcceptOnlyExactAasSpellings; Packaging.AasPackageIntegrityTests.MismatchedBlobIsRejectedBeforeItIsReadable; Packaging.AasPackageIntegrityTests.ConsumerSideVerificationCatchesTamperedBlob; Packaging.AasPackageIntegrityTests.OciBindingPublishesManifestDigestWithPrefixAndDigestWithoutPrefix; Packaging.AasPackageIntegrityTests.OciManifestRequiresExactlyOnePackageLayerDescriptor; Client.AasRegistryClientTests.PackageDownloadRejectsTamperedBlob |
11. V3 limitations and migration notes
- Annex G / IDTA-01002 Part 2 HTTP APIs are not implemented by this branch.
- Calling a V3
Operationwith arguments is incomplete. A materialized Operation carries itsInvokeMethod, and a Client resolves it by either its own NodeId or theMethodDeclarationIdofAASOperationType.Invoke. The Method also carriesInputArgumentsandOutputArgumentsunder the standard BrowseNames. What does not yet work is the Call itself:NodeState.AddChilddoes not route throughMethodState.FindChild, so a Method imported from a NodeSet leaves the typedInputArgumentsproperty null, and the Server answersBadTooManyArguments. Closing this needs a change to how the shared NodeSet import claims typed children, which affects every consumer of runtime NodeSets and is therefore out of scope here.samples/Aasexercises the path and reports the status rather than working around it. This limitation is V3-specific; the V2OperationMethod has no declared arguments and is called with an empty input list. - The AAS registry server currently exposes direct construction through
AasRegistryServiceandAasRegistryNodeManagerFactory; the client has a DI helper (AddAasV3RegistryClient), while the metamodel server hasAddAasV3Server. - This branch adds new AAS packages and the OPC UA
DecimalDataType. No manual migration from 1.5.378 is required solely to consume the new AAS libraries; the general 2.0 migration guidance remains in Migration Guide.
12. OPC 30270 / AAS V2 half
The OPC 30270 half implements the published "OPC UA for Asset Administration
Shell" companion specification for the AAS V2.0.1 metamodel. The static model
is generated into Opc.Ua.Aas.V2, its namespace URI is
http://opcfoundation.org/UA/I4AAS/, and predefined nodes are loaded with the
generated AddOpcUaAasV2 extension. The server and client entry points are
AddAasV2Server and AddAasV2Client.
V2 is ingestion-only. Opc.Ua.Aas.V2.AasJsonReader,
Opc.Ua.Aas.V2.AasXmlReader and Opc.Ua.Aas.V2.AasxPackageReader parse AAS
V2.0.1 documents and AASX packages into the V2 object model; there is no
AddressSpace-to-document serializer and therefore no round-trip guarantee. The
materializer projects the document into an OPC UA NodeSet:
await using FileStream input = File.OpenRead("environment.json");
Opc.Ua.Aas.V2.AasDocumentReadResult read = await new Opc.Ua.Aas.V2.AasJsonReader()
.ReadAsync(input, ct);
if (!read.Succeeded)
{
Console.WriteLine(read.Error);
return;
}
Opc.Ua.Aas.AasMaterializationResult materialized = Opc.Ua.Aas.V2.AasEnvironmentMaterializer
.Materialize(read.Environment!);
AddAasV2Server registers the V2 environment NodeManager, the document-backed
value provider and the shared operation handler contract:
services
.AddOpcUa()
.AddServer(options =>
{
options.EndpointUrls.Add("opc.tcp://localhost:4840/AasV2Server");
})
.Services
.AddOpcUa()
.AddAasV2Server(options =>
{
options.ControlNamespaceUri = "urn:example:aas-v2:instances";
})
.AddEnvironmentProvider<MyAasV2EnvironmentProvider>()
.AddOperationHandler<MyAasOperationHandler>();
The runtime surface is Read + Write + Call, matching the rest of this stack. A
V2 write reaches IAasValueProvider.WriteValueAsync and stops there: there is
no V2 document writer that could persist the change back to JSON, XML or AASX.
Operation is an OptionalPlaceholder Method named Operation with no
declared arguments, unlike the V3 mandatory Invoke Method, so a V2 Call is
zero-argument.
File and Blob content on V2 is served through an embedded standard OPC UA
FileType object. Clients call Open, Read and Close on that child rather
than reading a V3-style value projection:
var client = new Opc.Ua.Aas.Client.V2.AasClient(session, aasNamespaceIndex, telemetry);
Opc.Ua.Aas.V2.AASFileTypeClient manual = client.OpenFile(
"https://example.com/submodels/nameplate",
"Manual");
ByteString content = await client.ReadFileContentAsync(manual.ObjectId, ct: ct);
OPC 30270 leaves instance NodeIds server-specific and defines no addressing
convention. The V2 materializer and client deliberately reuse the deterministic
AasNodeIdEncoding and AasIdShortPath rules used by the V3 draft, so one
addressing scheme serves both generations and neither client has to browse for
known identifiers:
var client = new Opc.Ua.Aas.Client.V2.AasClient(session, aasNamespaceIndex, telemetry);
Opc.Ua.Aas.V2.AASPropertyTypeClient temperature = client.OpenProperty(
"https://example.com/submodels/process",
"Temperature");
Opc.Ua.Aas.Client.V2.AasValueReadResult current = await client.ReadValueAsync(
temperature.ObjectId,
ct);
StatusCode status = await client.WriteValueAsync(
temperature.ObjectId,
Opc.Ua.Aas.V2.AASValueTypeDataType.Double,
new Variant(42.5),
ct);
V2 has no registry, no packages, no federation and no DPP. Those are V3 draft additions, and the V2 implementation intentionally stops at the OPC 30270 metamodel.
OPC 30270 conformance units
The V2 server publishes the seventeen conformance units from OPC 30270 Table 83
through Server/ServerCapabilities/ConformanceUnits and publishes no
ServerProfileArray entry, because Table 84, which would assign profile URIs,
is empty. Two internal specification contradictions are resolved in
src/Opc.Ua.Aas.Server/V2/AasV2ConformanceUnits.cs: Table 83 misspells the
multi-language unit as I4AAS MultiLangaugeProperty while Table 85 spells it
I4AAS MultiLanguageProperty, and Table 85 lists an I4AAS Security unit that
Table 83 never defines. The implementation publishes the corrected
I4AAS MultiLanguageProperty spelling and does not publish I4AAS Security.
Each mapping below was checked against the cited test in
tests/Opc.Ua.Aas.Tests/V2/. No OPC 30270 Table 83 unit is currently unmapped.
| OPC 30270 Table 83 unit | Demonstrating tests |
|---|---|
I4AAS AAS |
Model.AasV2ObjectModelTests.ConstructionRetainsV2TopLevelIdentifiablesAndReferences; Serialization.AasV2SerializationTests.JsonReaderParsesV2EnvironmentAndEverySubmodelElementType; Server.AasV2ServerTests.ConformanceUnitsPublishOpc30270FacetWithoutProfileUris |
I4AAS Asset |
Model.AasV2ObjectModelTests.ConstructionRetainsV2TopLevelIdentifiablesAndReferences; Model.AasV2ObjectModelTests.V2OnlyConceptsRoundTripThroughModel; Client.AasClientTests.OpenMethodsResolveByIdentifierAndIdShortPath |
I4AAS Submodel |
Model.AasV2ObjectModelTests.ConstructionRetainsV2TopLevelIdentifiablesAndReferences; Materialization.AasEnvironmentMaterializerTests.EverySubmodelElementTypeMaterializesWithItsMembers; Client.AasClientTests.OpenMethodsResolveByIdentifierAndIdShortPath |
I4AAS ConceptDescription |
Model.AasV2ObjectModelTests.ConceptDescriptionFlavoursRetainIdentifierKinds; Serialization.AasV2SerializationTests.XmlReaderParsesV2EnvironmentAndEverySubmodelElementType |
I4AAS View |
Model.AasV2ObjectModelTests.V2OnlyConceptsRoundTripThroughModel; Materialization.AasEnvironmentMaterializerTests.ShellMaterializesAssetViewsInterfacesAndAasReferences |
I4AAS RelationshipElement |
Serialization.AasV2SerializationTests.JsonReaderParsesV2EnvironmentAndEverySubmodelElementType; Serialization.AasV2SerializationTests.XmlReaderParsesV2EnvironmentAndEverySubmodelElementType; Materialization.AasEnvironmentMaterializerTests.EverySubmodelElementTypeMaterializesWithItsMembers |
I4AAS Property |
Model.AasV2ObjectModelTests.SubmodelElementMembersRetainNodeSetTypes; Client.AasClientTests.ReadAndWriteValueUseDeclaredAasValueTypeAsync; Materialization.AasEnvironmentMaterializerTests.EverySubmodelElementTypeMaterializesWithItsMembers |
I4AAS MultiLanguageProperty |
Model.AasV2ObjectModelTests.SubmodelElementMembersRetainNodeSetTypes; Serialization.AasV2SerializationTests.JsonReaderParsesV2EnvironmentAndEverySubmodelElementType; Materialization.AasEnvironmentMaterializerTests.EverySubmodelElementTypeMaterializesWithItsMembers |
I4AAS Range |
Serialization.AasV2SerializationTests.JsonReaderParsesV2EnvironmentAndEverySubmodelElementType; Serialization.AasV2SerializationTests.XmlReaderParsesV2EnvironmentAndEverySubmodelElementType; Materialization.AasEnvironmentMaterializerTests.EverySubmodelElementTypeMaterializesWithItsMembers |
I4AAS Blob |
Serialization.AasV2SerializationTests.JsonReaderParsesV2EnvironmentAndEverySubmodelElementType; Materialization.AasEnvironmentMaterializerTests.EverySubmodelElementTypeMaterializesWithItsMembers; Client.AasClientTests.FileAndBlobContentAreReadThroughEmbeddedFileTypeAsync |
I4AAS File |
Serialization.AasV2SerializationTests.AasxReaderReadsXmlJsonAndSupplementaryFiles; Server.AasV2ServerTests.EmbeddedFileOpenReadAndCloseServeBlobContentAsync; Client.AasClientTests.FileAndBlobContentAreReadThroughEmbeddedFileTypeAsync |
I4AAS ReferenceElement |
Serialization.AasV2SerializationTests.JsonReaderParsesV2EnvironmentAndEverySubmodelElementType; Serialization.AasV2SerializationTests.XmlReaderParsesV2EnvironmentAndEverySubmodelElementType; Materialization.AasEnvironmentMaterializerTests.EverySubmodelElementTypeMaterializesWithItsMembers |
I4AAS Capability |
Serialization.AasV2SerializationTests.JsonReaderParsesV2EnvironmentAndEverySubmodelElementType; Serialization.AasV2SerializationTests.XmlReaderParsesV2EnvironmentAndEverySubmodelElementType; Materialization.AasEnvironmentMaterializerTests.EverySubmodelElementTypeMaterializesWithItsMembers |
I4AAS SubmodelElementCollection |
Model.AasV2ObjectModelTests.V2OnlyConceptsRoundTripThroughModel; Materialization.AasEnvironmentMaterializerTests.OrderedCollectionUsesHasOrderedComponentAndUnorderedCollectionUsesHasComponent; AasV2ModelTests.OrderedCollectionRedeclaresTheSubmodelElementPlaceholder |
I4AAS Operation |
Materialization.AasEnvironmentMaterializerTests.MaterializedNodeSetImportsIntoAnAddressSpace; Server.AasV2ServerTests.OperationMethodInvokesHandlerAndRejectsArgumentsAsync; Client.AasClientTests.InvokeCallsTheEmbeddedOperationMethodAsync |
I4AAS Event |
Serialization.AasV2SerializationTests.JsonReaderParsesV2EnvironmentAndEverySubmodelElementType; Serialization.AasV2SerializationTests.XmlReaderParsesV2EnvironmentAndEverySubmodelElementType; Materialization.AasEnvironmentMaterializerTests.EverySubmodelElementTypeMaterializesWithItsMembers |
I4AAS Entity |
Model.AasV2ObjectModelTests.SubmodelElementMembersRetainNodeSetTypes; Serialization.AasV2SerializationTests.JsonReaderParsesV2EnvironmentAndEverySubmodelElementType; Materialization.AasEnvironmentMaterializerTests.EverySubmodelElementTypeMaterializesWithItsMembers |
13. References
- OPC 30270, "OPC UA for Asset Administration Shell", namespace
http://opcfoundation.org/UA/I4AAS/. - OPC UA for Asset Administration Shell V3 draft, release
3.00-draft3, namespacehttp://opcfoundation.org/UA/I4AAS/v3/. - xRegistry — abstract registry base model.
- OPC UA WoT Connectivity.
- Dependency Injection.
- Runtime NodeSets.