Files
romanett 651beaee1e Fully support AliasNames in ConsoleReferenceServer (#4012) (#4321)
# Description

The CTT AliasName conformance units skip or fail against the
ConsoleReferenceServer. `AliasName Base` skips with *"No instance of
AliasNameType has been found in the address space"*; `Category Tags`,
`Category Topics` and `Hierarchy` fail. Part 17 support landed in #3779,
but aliases live only inside the `IAliasNameStore` — nothing is
browsable, no `AliasNameCategoryType` is nested under another, and the
Topics aliases point at the Server object rather than at a
`PublishedDataSetType` instance.

Part 17 §6.2 clients — the CTT among them — discover aliases by
browsing, not only by calling `FindAlias`. This PR materializes the
store contents as real nodes and fills the three gaps.

### SDK — `Opc.Ua.Server`

* **`DiagnosticsNodeManager.MaterializeRegisteredAliasNameNodesAsync`**
(new, opt-in) walks every registered `IAliasNameStore` and creates one
`AliasNameType` instance per alias — BrowseName carrying the alias name,
`AliasFor` references to its targets, and the inverse `HasAlias`
reference on each local target (via `externalReferences`, since targets
usually live in another node manager) — plus an `AliasNameCategoryType`
instance for every store category the standard NodeSet does not already
ship. Categories whose NodeId belongs to another node manager are
skipped, so an `AliasNameNodeManager` that also registers its store with
the server-wide registry does not get its NodeIds claimed twice.

Opt-in: servers that only need the well-known `FindAlias` to answer from
their store pay nothing. The nodes are a snapshot taken at address-space
creation — later mutations change what `FindAlias` returns but do not
add or remove nodes.

* **Optional Part 17 methods on the well-known categories.** The same
pass instantiates whatever a category declares through
`AliasNameCapabilities`: `FindAliasVerbose` (§6.3.3),
`AddAliasesToCategory` (§6.3.4), `DeleteAliasesFromCategory` (§6.3.5)
and `LastChange` (§6.3.1). Mutations require a `SecurityAdmin` caller on
a `SignAndEncrypt` channel; the gate previously private to
`AliasNameNodeManager` moved to `AliasNameMethodDispatcher` so both
paths share one implementation.

* **`LastChange`** is now tracked per category rather than only for the
standard `Aliases` node, and the duplicated seeding logic collapsed into
one helper.

### Reference server

* A nested **`Devices`** category under `TagVariables` holds
`Pump1_Status` and `Heater_Power`. `FindAlias` on `TagVariables` still
returns all six names through subtree recursion, while `FindAlias` on
`Devices` returns exactly two — which is what the Hierarchy unit checks.
* `ReferenceServerConfigurationNodeManager` creates three
**`PublishedDataSet` instances** under `PublishedDataSets` (`i=17371`)
and the Topics aliases now target them, satisfying the *"is a Dataset"*
check. These are address-space instances only — no PubSub runtime is
added, and the CTT only inspects the target's type definition.

## Note for reviewers — the reserved NodeIds

The optional children are created at the NodeIds the OPC Foundation
reserves for them (`Aliases.FindAliasVerbose` = `i=24054`,
`TagVariables.LastChange` = `i=32854`, …). Those identifiers are
allocated in `StandardTypes.csv` but are **not** declared in the
ModelDesign and **not** present in the published NodeSet, because the
standard address space does not instantiate optional children. The
source generator therefore emits no parent-to-instance mapping and no
`MethodIds` constants for them, so
`DiagnosticsNodeManager.ReservedChildIds` supplies the nine method ids
and the two missing `LastChange` ids explicitly, each traceable to its
registry row. The 18 argument-property ids do have generated
`VariableIds` constants and are referenced through those.

I deliberately did **not** "fix" this by declaring the children in
`tools/Opc.Ua.SourceGeneration.Core/Design/StandardTypes.xml`: that file
is a verbatim copy of upstream
`UA-ModelCompiler/Design.v105/StandardTypes.xml` (the only differences
today are upstream being newer), so a local edit is lost on the next
sync and the NodeIds would silently revert to factory-minted ones.
`docs/AliasNames.md` records this.

Worth raising upstream separately: `StandardTypes.csv` allocates
`Aliases_FindAliasVerbose` and siblings while the ModelDesign never
declares those children, so the ModelCompiler cannot emit ids it has
itself reserved. If that is ever reconciled, `ReservedChildIds` becomes
redundant and deletes cleanly.

## Related Issues

- Fixes #4012

## 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.
- [ ] I ran **all** tests locally using the **UA.slnx** solution against
at least .net **framework** and .net **10**, and all passed. — **not
done: verified on net8.0 only** (Server 4710, InformationModel 1099,
Core 4443, plus the client and features alias suites; AOT build clean).
Needs a run against net framework and net10 before this leaves draft.
- [ ] I fixed **all** failing and flaky tests in the CI pipelines and
**all** CodeQL warnings.
- [ ] I have addressed **all** PR feedback received.

### Tests

`tests/Opc.Ua.InformationModel.Tests/Aliasname*Tests.cs` mirror the four
CTT conformance units and no longer `Assert.Ignore` where the CTT
reported failures — the alias suite is 37 tests with no skips. New
`AliasnameOptionalMethodsTests` covers the optional methods, their
reserved NodeIds, the anonymous-caller denial, an add/find/delete round
trip and `LastChange` advancing on mutation.

One pre-existing test changed premise:
`EnumerateSubCategoriesAsyncOnLeafReturnsEmptyAsync` asserted that
`TagVariables` has no sub-categories, which this PR deliberately makes
false. It now targets `Topics` (still a leaf) and a new test guards the
nesting.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: romanett <7413710+romanett@users.noreply.github.com>
Co-authored-by: marcschier <11168470+marcschier@users.noreply.github.com>
2026-08-29 18:05:42 +00:00

22 KiB

OPC UA Part 17 — Alias Names

OPC UA Part 17 defines a small but valuable address-space pattern: a hierarchy of human-readable alias names that point at one or more nodes via a non-hierarchical reference type. Clients can search the hierarchy by wildcard pattern and resolve a name to its targets without needing to know the target NodeId in advance — useful for tag-naming schemes (PI / SCADA / DCS), pub/sub topic registries, MES integration, and any scenario where humans pick names but machines need ids.

This stack ships full Part 17 support in Opc.Ua.Server (server side) and Opc.Ua.Client (client side). The implementation covers:

Spec section Type / Method Status
§6.2 AliasNameType (browsable instance nodes) ✔ opt-in — see below
§6.3.1 AliasNameCategoryType (incl. nesting) ✔ (nested instances opt-in — see below)
§6.3.1 LastChange (VersionTime) ✔
§6.3.2 FindAlias ✔
§6.3.3 FindAliasVerbose ✔
§6.3.4 AddAliasesToCategory ✔
§6.3.5 DeleteAliasesFromCategory ✔
§7.2 AliasNameDataType ✔
§7.3 AliasNameVerboseDataType ✔
§8.2 AliasFor reference type ✔
§9.2 Well-known Aliases (i=23470) ✔ wired; optional methods opt-in — see below
§9.3 Well-known TagVariables (i=23479) ✔ wired; optional methods opt-in — see below
§9.4 Well-known Topics (i=23488) ✔ wired; optional methods opt-in — see below
Annex D PubSub replication (LastChange notifications) ✔ transport-agnostic — see below

Server side — Opc.Ua.Server.AliasNames

The server library exposes a pluggable backend (IAliasNameStore) plus a default in-memory implementation. Apps assemble their alias inventory inside a store, then either:

  1. Register the store directly with the server-wide IAliasNameStoreRegistry so the standard well-known Aliases/TagVariables/Topics nodes start dispatching through it, or
  2. Wrap the store in an AliasNameNodeManager to expose application- defined categories under a custom namespace (with full AddAliasesToCategory / DeleteAliasesFromCategory support).

Both approaches can be combined.

Quick start — serving standard categories

using Opc.Ua;
using Opc.Ua.Server;
using Opc.Ua.Server.AliasNames;

// inside CreateMasterNodeManager(IServerInternal server, ...):
var tagVariables = new AliasNameCategoryDescriptor(
    ObjectIds.TagVariables,
    QualifiedName.From(BrowseNames.TagVariables),
    AliasNameCapabilities.FindAliasVerbose);

var store = new InMemoryAliasNameStore([tagVariables]);
store.Seed(ObjectIds.TagVariables, "TIC101_Setpoint",
    new ExpandedNodeId("Scalar_Static_Double", refServerNs),
    serverUri: null,
    referenceTypeId: ReferenceTypeIds.AliasFor);
// ... seed more entries ...

((IAliasNameStoreRegistryProvider)server)
    .AliasNameStoreRegistry.Register(store);

When a client calls Aliases.FindAlias (i=23476), TagVariables.FindAlias (i=23485) or Topics.FindAlias (i=23494), DiagnosticsNodeManager's late binder routes the call through the registry to the matching store.

Quick start — application-defined categories

AliasNameNodeManager is a CustomNodeManager2 that owns a namespace and creates its own category tree from the store's RootCategories. Add it to your server's node-manager list:

var myRoot = new AliasNameCategoryDescriptor(
    new NodeId("My/Category", myNamespaceIndex),
    new QualifiedName("My/Category", myNamespaceIndex),
    AliasNameCapabilities.All);            // expose every optional method
var store = new InMemoryAliasNameStore([myRoot]);
nodeManagers.Add(new AliasNameNodeManager(server, configuration, store));

Options:

  • NamespaceUri — controls the namespace under which the manager registers its category instances. Defaults to http://opcfoundation.org/UA/AliasName/. Every category descriptor's NodeId must lie in this namespace; descriptors pointing anywhere else are skipped with a warning rather than claiming another manager's ids.
  • MaterializeAliasNodes (default true) — creates one browsable AliasNameType node per alias, with AliasFor references to its targets, exactly as the standard-node materialization below does. Disable to expose only the category tree.
  • LinkToStandardAliasesObject (default true) — adds Organizes external references from the well-known Aliases (i=23470) object to the manager's root categories so they show up in the standard browse tree.
  • RequireSecurityAdminForMutations (default true) — rejects AddAliasesToCategory / DeleteAliasesFromCategory calls from unauthenticated users or sessions without the WellKnownRole_SecurityAdmin role on a SignAndEncrypt channel.
  • RegisterWithServerRegistry (default true) — also registers the store with IAliasNameStoreRegistry so the well-known standard nodes see it.

Browsable alias nodes

Registering a store makes FindAlias answer from it, but the aliases themselves stay inside the store — nothing in the address space shows them. Part 17 §6.2 clients (the OPC Foundation CTT among them) also browse for aliases, so a server under conformance test needs the alias hierarchy materialized as real nodes.

Both materialization paths — AliasNameNodeManager for application-defined namespaces and DiagnosticsNodeManager.MaterializeRegisteredAliasNameNodesAsync for the standard well-known nodes — run the same shared walker (AliasNameNodeMaterializer), so they produce structurally identical Part 17 trees and every fix lands in both at once.

DiagnosticsNodeManager.MaterializeRegisteredAliasNameNodesAsync does that for every registered store: one AliasNameType instance per alias — BrowseName carrying the alias name, AliasFor references to its targets, and the inverse HasAlias reference added on each local target — plus an AliasNameCategoryType instance for every store category the standard NodeSet does not already ship (nested categories included; a root category from the store is organized under the standard Aliases object so it stays browse-discoverable). Remote targets get their server URI registered in the server's ServerUris table and referenced with the matching ServerIndex. Server-defined BrowseNames that a descriptor left in namespace 0 — reserved for OPC-Foundation-defined names — are re-homed into the diagnostics namespace; Part 17 clients compare alias names ignoring the namespace, so this is transparent to them. The pass is idempotent, and only creates category nodes whose NodeId lies in the diagnostics namespace — a descriptor pointing anywhere else is skipped with a warning rather than claiming another manager's (or the standard NodeSet's) ids. Call it from an overridden CreateAddressSpaceAsync, after base has loaded the standard categories:

public override async ValueTask CreateAddressSpaceAsync(
    IDictionary<NodeId, IList<IReference>> externalReferences,
    CancellationToken cancellationToken = default)
{
    await base.CreateAddressSpaceAsync(externalReferences, cancellationToken)
        .ConfigureAwait(false);

    await MaterializeRegisteredAliasNameNodesAsync(
        externalReferences, cancellationToken).ConfigureAwait(false);
}

The same pass also instantiates the optional Part 17 methods — FindAliasVerbose, AddAliasesToCategory, DeleteAliasesFromCategory and LastChange — on every category whose descriptor declares them through AliasNameCapabilities, including the well-known ones the standard NodeSet ships without them:

var tagVariables = new AliasNameCategoryDescriptor(
    ObjectIds.TagVariables,
    QualifiedName.From(BrowseNames.TagVariables),
    AliasNameCapabilities.All);       // verbose + add + delete + LastChange

Mutation calls are gated on a SecurityAdmin caller over a SignAndEncrypt channel and return BadUserAccessDenied otherwise.

It is opt-in: servers that only need FindAlias to answer from their store pay nothing. The created nodes are a snapshot taken at address-space creation — aliases added or removed later through AddAliasesToCategory / DeleteAliasesFromCategory change what FindAlias returns and advance LastChange, but do not add or remove AliasNameType nodes.

The browse view carries AliasFor associations only: an entry a store holds under an unrelated reference type is not a Part 17 §6.2 alias association, so it is served by FindAlias (with a matching filter) but not materialized. Entries stored under an AliasFor subtype are materialized, but always as the base AliasFor reference — the verbose record does not carry the concrete reference type.

Two things the caller controls:

  • Give category descriptors a BrowseName in a namespace the server owns. The store stamps that namespace onto every alias QualifiedName it reports, and the materializer re-homes ns=0 BrowseNames out of the reserved OPC Foundation namespace — with a server-owned descriptor namespace the query results and the browsable nodes agree, so a returned alias name resolves via TranslateBrowsePathsToNodeIds. The reference server uses the diagnostics namespace for all its descriptors.
  • Part 17 §9 constrains Topics aliases to PublishedDataSetType targets. Neither the store nor AddAliasesToCategory enforces this — a server that exposes the mutation methods on Topics should only grant them to operators who preserve the constraint.

Quickstarts.ReferenceServer uses this in ReferenceServerConfigurationNodeManager, which also creates the PublishedDataSet instances and seeds the Topics aliases that point at them — in the same place, so an alias can never target a dataset that was not created. The nested Devices sub-category under TagVariables is declared with the store seeding in ReferenceServer.ConfigureAliasNameStore.

Custom backend

Implement IAliasNameStore to back the alias inventory with your own storage (DB, file, MES, …). The interface is small:

public interface IAliasNameStore
{
    IReadOnlyList<AliasNameCategoryDescriptor> RootCategories { get; }
    event EventHandler<AliasStoreChangedEventArgs>? Changed;

    uint? GetLastChange(NodeId categoryId);
    bool OwnsCategory(NodeId categoryId);

    ValueTask<IReadOnlyList<AliasNameDataType>> FindAliasAsync(...);
    ValueTask<IReadOnlyList<AliasNameVerboseDataType>> FindAliasVerboseAsync(...);
    ValueTask<StatusCode[]> AddAliasesAsync(...);
    ValueTask<StatusCode[]> DeleteAliasesAsync(...);
}

The reference InMemoryAliasNameStore is thread-safe (SemaphoreSlim), supports nested categories and emits Changed events that bubble up to the address-space LastChange property. A mutation on a nested category bumps — and notifies for — every ancestor category as well, per Part 17 §6.3.1/§9.2: a category's LastChange reflects the most recent change anywhere in its subtree, and the root Aliases value reflects any change at all.

Client side — Opc.Ua.Client.AliasNames

The client library provides a high-level AliasNameClient plus a caching AliasNameResolver.

using Opc.Ua.Client.AliasNames;

// Standard categories have hardcoded method NodeIds so the first call
// is one round-trip — no extra TranslateBrowsePaths probe needed.
AliasNameClient client = AliasNameClient.OpenStandardTagVariables(session);

IReadOnlyList<AliasNameDataType> result =
    await client.FindAliasAsync("TIC%", referenceTypeFilter: null, ct);

AliasNameClient exposes the full Part 17 method surface:

  • FindAliasAsync(pattern, referenceTypeFilter, ct)
  • FindAliasVerboseAsync(...) — throws NotSupportedException when the category does not expose the optional method.
  • AddAliasesToCategoryAsync(IEnumerable<AliasNameAddRequest>, ct)
  • DeleteAliasesFromCategoryAsync(IEnumerable<AliasNameDeleteRequest>, ct)
  • EnumerateSubCategoriesAsync(ct) — IAsyncEnumerable of child AliasNameSubCategoryInfo.
  • ReadLastChangeAsync(ct) — returns the VersionTime (or null when the category does not expose LastChange).

Per-call errors map to typed exceptions:

Status code Exception
BadUserAccessDenied UnauthorizedAccessException
BadNotSupported NotSupportedException
BadNotImplemented NotSupportedException
other BadXxx ServiceResultException

AliasNameResolver — cached alias→NodeId

await using var resolver = new AliasNameResolver(
    AliasNameClient.OpenStandardTagVariables(session));

IReadOnlyList<ExpandedNodeId> targets =
    await resolver.ResolveAsync("TIC101_Setpoint", ct);

string aliasName = await resolver.ResolveAliasNameAsync(targets[0], ct);

Default refresh mode is Manual — callers invoke RefreshAsync (or rely on lazy-load via ResolveAsync). Opt in to automatic cache invalidation via one of:

AliasNameResolverRefreshMode Strategy When to use
Manual (default) ManualAliasNameRefreshStrategy Caller drives refresh explicitly. Safe everywhere.
AutoOnLastChangePolling PollingAliasNameRefreshStrategy Server lacks Subscriptions or you want a fixed Read cadence.
AutoOnLastChangeMonitoredItem MonitoredItemAliasNameRefreshStrategy Server supports Subscriptions. Push-based — no Read per interval.

Custom strategies (e.g. the Annex D PubSub bridge in Opc.Ua.Client.AliasNames.PubSub — see Annex D below) plug in via:

new AliasNameResolverOptions
{
    RefreshStrategy = new MyCustomStrategy()  // takes precedence over RefreshMode
}

The IAliasNameRefreshStrategy contract is tiny:

public interface IAliasNameRefreshStrategy : IAsyncDisposable
{
    ValueTask StartAsync(AliasNameClient client, Action onInvalidate, CancellationToken ct);
}

Implementations watch for stale-cache triggers and invoke onInvalidate whenever they detect a change. MonitoredItemAliasNameRefreshStrategyOptions controls the underlying Subscription: it can be left owned (default — created + deleted by the strategy) or set via SharedSubscription to plug the monitored item into an externally managed subscription.

Disposing the resolver (await using / DisposeAsync) tears down the strategy: timer for polling, MonitoredItem + Subscription for the monitored-item variant. Disposal is idempotent and never throws.

Spec deviations / wrinkles

  • AliasNameDataType.ReferencedNodes — the wire format defines this as ExpandedNodeId[] (NodeSet i=18), not NodeId[]. The source-generated AliasNameDataType is correct; the historical Quickstart sample used NodeId[] and has been removed.

  • Standard well-known nodes — the OPC UA NodeSet instantiates only FindAlias on Aliases/TagVariables/Topics (plus LastChange on Aliases), so the always-on binder wires just those. The optional methods are added by MaterializeRegisteredAliasNameNodesAsync through the generated AddFindAliasVerbose / AddAddAliasesToCategory / AddDeleteAliasesFromCategory optional- child helpers, for each category whose descriptor declares the matching AliasNameCapabilities. Each child is created at the NodeId the OPC Foundation reserves for it (Aliases.FindAliasVerbose = i=24054, TagVariables.LastChange = i=32854, and so on).

    Those identifiers are allocated in the standard identifier registry (StandardTypes.csv) but are deliberately not declared in the ModelDesign and not present in the published NodeSet — the standard address space does not instantiate optional children. A reserved id is a number set aside for the node if a server chooses to expose it, not a node the server must have. Consequently the source generator emits no parent-to-instance mapping and no MethodIds constant for them, and DiagnosticsNodeManager.ReservedChildIds supplies the nine method ids and the two missing LastChange ids explicitly, each traceable to its registry row. The argument properties do have generated VariableIds constants and are referenced through those.

    Do not "fix" this by declaring the children in StandardTypes.xml: that file is a verbatim copy of the OPC Foundation's ModelDesign and is re-synced from upstream, so a local edit is lost on the next sync and the NodeIds silently revert to factory-minted ones.

  • AliasNameCapabilities.AddAliasesToCategory / DeleteAliasesFromCategory — on an AliasNameNodeManager this defaults to SecurityAdmin-only via AliasNameNodeManagerOptions.RequireSecurityAdminForMutations; on the standard well-known nodes the same check is always applied and cannot be opted out of. It requires both the role grant AND a SignAndEncrypt channel.

  • ReferenceTypeFilter semantics — null/empty and ReferenceTypeIds.References match every alias regardless of reference type. Otherwise matches are limited to aliases whose reference type is, or is a subtype of, the filter (using Server.TypeTree.IsTypeOf).

Annex D — PubSub LastChange notifications

Part 17 Annex D defines a lightweight PubSub schema for alias-change notifications between servers. The schema carries only each category's current LastChange value (a VersionTime/uint) — subscribers learn that a publisher's category changed, then refetch alias contents via FindAlias/FindAliasVerbose if needed.

The data types (already emitted by the source generator):

NodeId Type Fields
i=24052 AliasCategoryUpdateDataType Category : PortableNodeId, LastChange : VersionTime
i=24053 AliasUpdateDataType ApplicationUri : string, Categories : AliasCategoryUpdateDataType[]

Server side — Opc.Ua.Server.AliasNames.PubSub

The server library exposes a transport-agnostic publisher that emits fully-built AliasUpdateDataType messages whenever an IAliasNameStoreRegistry-tracked store changes:

using Opc.Ua.Server.AliasNames;
using Opc.Ua.Server.AliasNames.PubSub;

// inside server startup (after the alias store is registered):
var resolver = new ServerPortableNodeIdResolver(server);
var publisher = new AliasNamePublisher(
    registry: ((IAliasNameStoreRegistryProvider)server).AliasNameStoreRegistry,
    portableResolver: resolver,
    applicationUri: configuration.ApplicationUri);

publisher.AliasUpdateProduced += (_, e) =>
{
    // Hand `e.Update` to your transport — e.g. publish a DataSetMessage
    // through Opc.Ua.PubSub.UaPubSubApplication with the DataSet
    // built by AliasUpdateDataSetFactory.Create(...).
};

Helpers shipped:

  • IPortableNodeIdResolver + ServerPortableNodeIdResolver — converts a local NodeId into the spec-required PortableNodeId (NamespaceUri + Identifier with namespace index stripped).
  • AliasUpdateDataSetFactory.Create(dataSetClassId) — builds the fixed-by-spec DataSetMetaDataType describing the AliasUpdate DataSet (ApplicationUri : string, Categories : AliasCategoryUpdateDataType[]).
  • AliasNamePublisher — subscribes to the registry, builds and emits AliasUpdateDataType messages via the AliasUpdateProduced event.

The library deliberately stays transport-agnostic: it raises the fully-built AliasUpdateDataType and lets the application wire it into Opc.Ua.PubSub.UaPubSubApplication (UDP / JSON / MQTT) or any other transport that can carry the AliasUpdateDataType payload.

Client side — Opc.Ua.Client.AliasNames.PubSub

The client library mirrors the publisher:

using Opc.Ua.Client.AliasNames;
using Opc.Ua.Client.AliasNames.PubSub;

var reader = new AliasNamePubSubReader(
    new AliasNamePubSubReaderOptions
    {
        ExpectedApplicationUri = "urn:opcfoundation:publisher",
    });

// Hand incoming AliasUpdateDataType messages off to the reader from
// your transport (e.g. UaPubSubApplication.DataReceived, an MQTT
// subscriber callback, ...):
reader.Submit(receivedAliasUpdate);

// Wire reader into the resolver so the cache invalidates on every
// LastChange bump observed via PubSub.
await using var resolver = new AliasNameResolver(
    AliasNameClient.OpenStandardAliases(session),
    new AliasNameResolverOptions
    {
        RefreshStrategy = new AliasNamePubSubRefreshStrategy(reader),
    });

Helpers shipped:

  • AliasNamePubSubReader — surfaces incoming AliasUpdateDataType messages as the AliasUpdateReceived event. Optional ExpectedApplicationUri filter drops messages from other publishers.
  • AliasNamePubSubRefreshStrategy : IAliasNameRefreshStrategy — bridges the reader into the resolver. Matches incoming entries by the resolver's category NamespaceUri + identifier; fires Invalidate on any value-difference (wrap-safe — the comparison is inequality, not strict greater-than).

The PubSub bridge plugs into the same IAliasNameRefreshStrategy extension point as the polling and monitored-item strategies — apps can mix-and-match, e.g. fall back to polling on a particular category while letting PubSub drive the rest.

See also

  • OPC UA Part 17 specification: https://reference.opcfoundation.org/v105/Core/docs/Part17/
  • tools/Opc.Ua.SourceGeneration.Core/Design/StandardTypes.xml — Part 17 type definitions consumed by the source generator.
  • tests/Opc.Ua.Server.Tests/AliasNames/ — server-side unit tests.
  • tests/Opc.Ua.Client.Tests/AliasNames/ — mocked-session and live integration tests.
  • samples/Quickstarts.Servers/ReferenceServer/ReferenceServer.cs — ConfigureAliasNameStore shows how to seed and register a store.