Files
Marc Schier 397db603db Add OPC UA for Asset Administration Shell: the published OPC 30270 and v3 draft (#4244)
# 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
2026-08-18 08:11:47 -07:00

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 in Opc.Ua.Aas.V2.
  • OPC UA for Asset Administration Shell V3 draft (release 3.00-draft3) maps the AAS V3 draft metamodel into namespace http://opcfoundation.org/UA/I4AAS/v3/ and lives in Opc.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, and AASOperationType.Invoke is served through an injectable IAasOperationHandler.
  • Registry half — a catalogue of shell, submodel, concept-description, package and environment documents is served as folders of FileType resources 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, Format and ContentType, while preserving Bad_UserAccessDenied and Bad_NotFound as 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; Immediate invalidates affected monitored items with BadNodeIdUnknown.
  • 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 Operation with arguments is incomplete. A materialized Operation carries its Invoke Method, and a Client resolves it by either its own NodeId or the MethodDeclarationId of AASOperationType.Invoke. The Method also carries InputArguments and OutputArguments under the standard BrowseNames. What does not yet work is the Call itself: NodeState.AddChild does not route through MethodState.FindChild, so a Method imported from a NodeSet leaves the typed InputArguments property null, and the Server answers BadTooManyArguments. 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/Aas exercises the path and reports the status rather than working around it. This limitation is V3-specific; the V2 Operation Method has no declared arguments and is called with an empty input list.
  • The AAS registry server currently exposes direct construction through AasRegistryService and AasRegistryNodeManagerFactory; the client has a DI helper (AddAasV3RegistryClient), while the metamodel server has AddAasV3Server.
  • This branch adds new AAS packages and the OPC UA Decimal DataType. 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