Skip to content

Latest commit

 

History

History
358 lines (288 loc) · 15.1 KB

File metadata and controls

358 lines (288 loc) · 15.1 KB

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
§6.3.1 AliasNameCategoryType
§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
§9.3 Well-known TagVariables (i=23479) ✔ wired
§9.4 Well-known Topics (i=23488) ✔ wired
Annex D PubSub replication (LastChange notifications) ✔ transport-agnostic — see below
Annex D PubSub replication not implemented

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/.
  • 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.

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.

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). Optional methods (FindAliasVerbose/Add/Delete) on the standard nodes would require NodeSet extension and are not wired by the binder. To expose those, use a standalone AliasNameNodeManager with your own category nodes.
  • AliasNameCapabilities.AddAliasesToCategory / DeleteAliasesFromCategory — defaults to SecurityAdmin-only via AliasNameNodeManagerOptions.RequireSecurityAdminForMutations. The check requires both the role grant AND a SignAndEncrypt channel; opt out via the option for development scenarios only.
  • 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.
  • Applications/Quickstarts.Servers/ReferenceServer/ReferenceServer.csConfigureAliasNameStore shows how to seed and register a store.