Files
Marc Schier 51bcd4272a GDS Part 12 Full Compliance (#3795)
* GDS: Authorization & SecureChannel hardening (Part 12 §6.5, §7.2, §7.6)

* Fix OnGetCertificates self-admin authorization: pass applicationId to
  HasAuthorization so ApplicationSelfAdmin can read its own certificates per
  OPC 10000-12 §7.6.10 (was rejecting self-admin pull-management callers).
* Fix OnUnregisterApplicationAsync self-admin authorization: also pass
  applicationId (was silently failing the self-admin branch).
* OnUpdateApplication: accept ApplicationSelfAdmin and ApplicationAdmin in
  addition to DiscoveryAdmin per §6.5.7. Add self-admin role-permission shim.
* OnGetCertificateStatus: accept CertificateAuthorityAdmin / SelfAdmin /
  ApplicationAdmin per §7.6.12 (was using AuthenticatedUserOrSelfAdmin which
  is more permissive than spec).

* Introduce ApplicationAdmin privilege (Part 12 §7.2):
  - Add GdsRole.ApplicationAdmin static privilege.
  - Extend GdsRoleBasedIdentity with AdministeredApplicationIds and a new
    constructor accepting the list. WithAdditionalRoles preserves it.
  - AuthorizationHelper.HasAuthorization recognises the privilege: alone
    when no applicationId is supplied (e.g. RegisterApplication) and
    membership-tested against AdministeredApplicationIds otherwise.
  - HasTrustListAccess honours the ApplicationAdmin privilege for the
    administered applications' trust lists.
  - New role lists: DiscoveryAdminOrAppAdmin (RegisterApplication),
    DiscoveryAdminOrSelfAdminOrAppAdmin (Update/UnregisterApplication),
    CertificateAuthorityAdminOrSelfAdminOrAppAdmin (cert management).

* SecureChannel result-code alignment (Part 12 method tables):
  - HasAuthenticatedSecureChannel throws BadSecurityModeInsufficient (was
    BadUserAccessDenied).
  - Adds requireEncryption flag so callers can demand SignAndEncrypt
    (used by future StartNewKeyPairRequest/FinishRequest tightening).
  - Sign and SignAndEncrypt are now both accepted as 'authenticated'.

* Tests:
  - Register the GDS namespace in AuthorizationHelperTests SetUp so
    GDS-namespace role NodeIds do not collide on NodeId.Null with
    privilege roles.
  - Update HasAuthenticatedSecureChannelThrowsForNonSecureChannel to
    expect BadSecurityModeInsufficient.
  - Add HasAuthenticatedSecureChannelDoesNotThrowForSign and
    HasAuthenticatedSecureChannelWithRequireEncryptionThrowsForSign.
  - Add ApplicationAdmin coverage: SucceedsForAdministeredApp,
    ThrowsForUnmanagedApp, SucceedsForRegisterApplication,
    ThrowsWithEmptyAdministeredList.
  - Add NewRoleListsAreCorrectlyPopulated regression test.

The framework already enforces SigningRequired/EncryptionRequired via
DefaultAccessRestrictions in OpcUaGdsModel.xml, returning
BadSecurityModeInsufficient at request dispatch time
(MasterNodeManager.ValidateAccessRestrictions). The model declares the
correct restriction on every admin method, so explicit handler-level
checks would be redundant. Verified manually against §6.5.6, §6.5.7,
§6.5.8, §7.6.4-7.6.10 and §7.6.12.

23 Authorization unit tests pass. 181 unit + 420 integration GDS tests
pass on net10.0; build clean on net48 and net10.0.

* GDS: Audit event correctness (Part 12 7.6, 7.9)

* Fix audit-event ordering: emit CertificateRequestedAuditEvent AFTER the
  method outcome is known. Previously the event was raised before
  authorization with Status=true, so any failed call appeared in audit as
  a successful certificate request and any authorization-denied call
  generated a misleading 'success' audit trail.
  - OnStartNewKeyPairRequest: refactor to try/catch/finally, emit audit
    once with auditException reflecting either the captured throw, the
    bad ServiceResult, or null on success.
  - OnStartSigningRequestAsync: add the audit emission (previously
    missing entirely - 7.9.3 requires it) using the same pattern.
* Redact privateKeyPassword from audit InputArguments. The previous
  ApplicationsNodeManager.cs:1206-1223 included the raw password string
  as a Variant in the audit event payload, leaking the secret into any
  downstream audit sink. The password is now replaced with
  AuditEvents.RedactedPrivateKeyPassword (placeholder string).
* OnFinishRequestAsync: build the audit InputArguments from only the
  actual method inputs (applicationId, requestId) per 7.6.6 method
  signature. Previously the implementation appended the returned
  Certificate / PrivateKey / IssuerCertificates to InputArguments, which
  leaked the returned PrivateKey into audit payloads.
* Raise CertificateRevokedAuditEventType in OnRevokeCertificate on
  success and failure per 7.6.9. The event type was defined in the
  model (OpcUaGdsModel.xml:543) but never emitted. Refactored
  OnRevokeCertificate with try/catch/finally; sync-over-async (.Result)
  is preserved here and addressed separately in Track C (c-revoke-async).

* AuditEvents helper:
  - New ReportCertificateRevokedAuditEvent extension method, mirroring
    the existing CertificateRequested/Delivered helpers, with an
    Exception parameter that drives Status (true on success, false on
    failure).
  - New RedactedPrivateKeyPassword and RedactedPrivateKey constants
    documented as the canonical placeholders. Class-level remarks
    instruct callers to redact sensitive arguments before invoking the
    helpers.

* Tests:
  - AuditEventsTests: lock the placeholder values
    (RedactedPrivateKeyPassword and RedactedPrivateKey) so they remain
    stable for downstream audit-sink filters.
  - Existing 181 unit + 420 integration GDS tests pass on net10.0; build
    clean on net48 and net10.0.

b-keycred-audit and b-token-audit remain pending pending Track E and F
respectively (KeyCredentialService and AuthorizationService handlers).

* GDS: Directory / Pull Management correctness gaps (Part 12 6.5, 7.6)

* OnRevokeCertificate converted to async OnRevokeCertificateAsync; uses
  await instead of sync-over-async .Result, accepts the method-call
  cancellation token, and is now wired through OnCallAsync to integrate
  with the async dispatch path. RevokeCertificateAsync helper takes an
  optional CancellationToken so callers (OnUnregisterApplicationAsync,
  OnRevokeCertificateAsync) can forward it.

* OnCheckRevocationStatusAsync now computes the ValidityTime per
  OPC 10000-12 7.6.11. The previous implementation always returned
  DateTime.MinValue. The new implementation enumerates the CRLs in the
  trusted-issuer store and uses the earliest NextUpdate as ValidityTime
  when the chain validates. On Bad results (chain validation failed)
  ValidityTime remains DateTime.MinValue so callers re-check
  immediately.

* OnFinishRequestAsync returns the issuer chain via a new
  BuildIssuerCertificateChain helper. The previous implementation
  returned only the immediate signing CA, which is incomplete for
  multi-tier CAs per OPC 10000-12 7.6.6. The helper uses
  ICertificateGroup.Certificates (in-memory) as the X509Chain
  ExtraStore and falls back to the immediate issuing CA if chain
  building does not yield a chain. The helper is intentionally
  synchronous to avoid contending with concurrent SigningRequestAsync /
  NewKeyPairRequestAsync writes against the CertificateGroup's
  AuthoritiesStore (which caused parameterised PushTest cases to fail
  in parallel runs).

* ValidateApplication (ApplicationsDatabaseBase) now enforces the
  reverse-connect rules in OPC 10000-12 6.5.5:
  - Server / DiscoveryServer DiscoveryUrls must NOT start with the
    "rcp+" prefix.
  - Client DiscoveryUrls (when non-empty) must all start with "rcp+"
    AND ServerCapabilities must include "RCP".
  - ClientAndServer must register at least one non-rcp+ Server
    DiscoveryUrl; reverse-connect URLs are still permitted alongside
    and require the RCP ServerCapability.
  Removed the previous blanket rejection of any DiscoveryUrls on
  ApplicationType.Client (which contradicted the spec's reverse-connect
  registration pattern).

* Tests:
  - RegisterApplicationClientWithRcpDiscoveryUrlsAndRcpCapabilityDoesNotThrow
  - RegisterApplicationClientWithRcpDiscoveryUrlsAndMissingCapabilityThrows
  - RegisterApplicationServerWithRcpDiscoveryUrlThrows
  - RegisterApplicationClientAndServerWithMixedUrlsDoesNotThrow
  - RegisterApplicationClientAndServerWithOnlyRcpDiscoveryUrlsThrows
  All 188 unit + 420 integration GDS tests pass on net10.0; build clean
  on net48 and net10.0.

c-redundancy intentionally requires no enforcement: both transparent
(single application with multiple DiscoveryUrls) and non-transparent
(separate applications with NTRS capability) patterns per OPC 10000-12
6.5.6 are admitted by the validator; transparent redundancy is a
registrant convention rather than a server-enforceable invariant.


* Annex D capabilities: Self-advertise the LDS-ME capability identifier
  whenever the MulticastExtension is configured. Previously LdsServer
  hard-coded ["LDS"] regardless of whether multicast was running, so
  FindServersOnNetwork peers could not distinguish LDS from LDS-ME
  instances. Refactored the capability computation into a public static
  ComputeServerCapabilities(existing, hasMulticast) helper:
    - empty input + no multicast -> ["LDS"]
    - empty input + multicast -> ["LDS", "LDS-ME"]
    - custom seed list with no "LDS" -> "LDS" is appended
    - existing "LDS-ME" is not duplicated
  OnServerStarting now wires up the multicast layer first so the
  capability set reflects the configured topology.

* Annex C / rcp+ reverse-connect preservation: MulticastDiscovery now
  carries the OPC 10000-12 6.5.5 rcp+ DiscoveryUrl prefix end-to-end
  through mDNS announce / discover.
    - TryBuildProfile strips the rcp+ prefix before parsing the
      underlying transport URL (Uri parsing on "rcp+opc.tcp://..." is
      brittle across runtimes) and emits a dedicated TXT-record key
      ReverseConnectTxtKey ("rc") so receivers can reconstruct it
      verbatim.
    - OnServiceInstanceDiscovered honours the "rc" key when present
      and rebuilds the DiscoveryUrl with the rcp+ scheme.
  Exposes MulticastDiscovery.OpcUaServiceType,
  MulticastDiscovery.ReverseConnectScheme and
  MulticastDiscovery.ReverseConnectTxtKey as public constants so
  client code and tests can reference the same values used during
  registration and discovery.

* RegisterServer2 fallback (4.2.2): verified that LdsServer overrides
  both RegisterServerAsync and RegisterServer2Async, so the LDS
  accepts both legacy RegisterServer registrations and the modern
  RegisterServer2 + MdnsDiscoveryConfiguration path; no code change
  required.

* Tests: new LdsServerStaticTests fixture exercising the capability
  computation (six matrix cases) and locking in the Annex C / rcp+
  constants (one regression test). 195 unit + 420 integration GDS
  tests pass on net10.0; build clean on net48 and net10.0.

* Added Opc.Ua.Lds.Server as a ProjectReference of Opc.Ua.Gds.Tests so
  the new tests can use the public static surface of LdsServer and
  MulticastDiscovery.

* GDS: Push management hardening (Part 12 7.10)

* d-push-client-multitype: Make ServerPushConfigurationClient.ApplicationCertificateType
  settable instead of hard-coded to RsaSha256ApplicationCertificateType.
  Callers managing ECC, Brainpool or HTTPS certificates can now override
  the default before invoking certificate-management methods (or pass an
  explicit certificate type to the methods themselves). Removed the
  TODO comment that flagged this limitation.

* d-trustlist-ext: Set Writable / UserWritable to true for the
  DefaultApplicationGroup / DefaultHttpsGroup / DefaultUserTokenGroup
  TrustList instances in the GDS. Per OPC 10000-12 7.8.2.1 a TrustList
  that supports CloseAndUpdate / AddCertificate / RemoveCertificate is
  writeable; role-based access controls on the individual methods still
  enforce who may actually mutate the trust list. The previous values
  (false) were misleading to clients browsing the trust-list nodes.

* d-server-cred-audit: Redact the private key from
  CertificateUpdateRequestedAuditEventType InputArguments per OPC 10000-12
  7.10.3. ConfigurationNodeManager.UpdateCertificateAsync was passing
  the raw private-key ByteString into the audit payload, leaking the
  secret to any downstream audit sink. The audit payload now uses
  Opc.Ua.Server.AuditEvents.RedactedPrivateKey (an empty ByteString)
  as the placeholder; the certificate, issuer chain and key format
  remain in the payload so administrators can correlate the request.

  Also extended Server.ReportCertificateUpdateRequestedAuditEvent with
  an optional Exception parameter so callers can raise the event with
  Status=false on failure (parity with the GDS-side helpers).

* Build clean on net48 and net10.0. 195 unit + 420 integration GDS
  tests pass on net10.0.

Remaining Track D items (d-create-selfsigned, d-confirm-update,
d-managed-apps, d-alarms, d-push-tests) are larger features that
depend on Track H model additions and are not in scope for this
commit; their todos remain pending.

* GDS: Lock in ServerPushConfigurationClient.ApplicationCertificateType behaviour

Adds ServerPushConfigurationClientStaticTests covering the
Track D quick-win (d-push-client-multitype): the property defaults
to RsaSha256ApplicationCertificateType for backwards compatibility
and is now caller-overridable (e.g. for ECC management).

* GDS: Implement CreateSelfSignedCertificate (Part 12 7.10.6)

* Server-side: ConfigurationNodeManager wires the generated
  CreateSelfSignedCertificateMethodState.OnCallAsync handler when the
  optional method node exists on the ServerConfiguration instance.
  The handler:
  - Requires SecurityAdmin access (HasApplicationSecureAdminAccess)
  - Validates CertificateGroupId / CertificateTypeId via the existing
    VerifyGroupAndTypeId helper
  - Creates a self-signed certificate using the CertificateFactory
    pipeline (RSA with caller-specified key size, or ECC from the
    curve mapped by the certificateTypeId)
  - Merges DnsNames and IpAddresses into the SAN extension
  - Stores the resulting raw data on the existing
    CertificateIdentifier so the resolver can materialise it
  - Returns the DER-encoded public certificate to the caller
  The method is Optional per the spec; servers that load a nodeset
  without it simply skip the wire-up (null guard).

* Client-side: IServerPushConfigurationClient extended with
  CreateSelfSignedCertificateAsync; ServerPushConfigurationClient
  delegates to the generated ServerConfigurationTypeClient proxy
  with the standard elevate/revert permission pattern.

* net48 compat: uses new ValueTask<T>(result) instead of
  ValueTask.FromResult which is unavailable on .NET Framework 4.8.
2026-05-23 08:07:28 +02:00

15 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 ✔
§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.cs — ConfigureAliasNameStore shows how to seed and register a store.