* 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.
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:
- Register the store directly with the server-wide
IAliasNameStoreRegistryso the standard well-knownAliases/TagVariables/Topicsnodes start dispatching through it, or - Wrap the store in an
AliasNameNodeManagerto expose application- defined categories under a custom namespace (with fullAddAliasesToCategory/DeleteAliasesFromCategorysupport).
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 tohttp://opcfoundation.org/UA/AliasName/.LinkToStandardAliasesObject(defaulttrue) — addsOrganizesexternal references from the well-knownAliases (i=23470)object to the manager's root categories so they show up in the standard browse tree.RequireSecurityAdminForMutations(defaulttrue) — rejectsAddAliasesToCategory/DeleteAliasesFromCategorycalls from unauthenticated users or sessions without theWellKnownRole_SecurityAdminrole on aSignAndEncryptchannel.RegisterWithServerRegistry(defaulttrue) — also registers the store withIAliasNameStoreRegistryso 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(...)— throwsNotSupportedExceptionwhen the category does not expose the optional method.AddAliasesToCategoryAsync(IEnumerable<AliasNameAddRequest>, ct)DeleteAliasesFromCategoryAsync(IEnumerable<AliasNameDeleteRequest>, ct)EnumerateSubCategoriesAsync(ct)—IAsyncEnumerableof childAliasNameSubCategoryInfo.ReadLastChangeAsync(ct)— returns theVersionTime(ornullwhen the category does not exposeLastChange).
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 asExpandedNodeId[](NodeSeti=18), notNodeId[]. The source-generatedAliasNameDataTypeis correct; the historical Quickstart sample usedNodeId[]and has been removed.- Standard well-known nodes — the OPC UA NodeSet instantiates only
FindAliasonAliases/TagVariables/Topics(plusLastChangeonAliases). 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 standaloneAliasNameNodeManagerwith your own category nodes. AliasNameCapabilities.AddAliasesToCategory/DeleteAliasesFromCategory— defaults toSecurityAdmin-only viaAliasNameNodeManagerOptions.RequireSecurityAdminForMutations. The check requires both the role grant AND aSignAndEncryptchannel; opt out via the option for development scenarios only.ReferenceTypeFiltersemantics — null/empty andReferenceTypeIds.Referencesmatch every alias regardless of reference type. Otherwise matches are limited to aliases whose reference type is, or is a subtype of, the filter (usingServer.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 localNodeIdinto the spec-requiredPortableNodeId(NamespaceUri + Identifier with namespace index stripped).AliasUpdateDataSetFactory.Create(dataSetClassId)— builds the fixed-by-specDataSetMetaDataTypedescribing theAliasUpdateDataSet (ApplicationUri : string,Categories : AliasCategoryUpdateDataType[]).AliasNamePublisher— subscribes to the registry, builds and emitsAliasUpdateDataTypemessages via theAliasUpdateProducedevent.
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 incomingAliasUpdateDataTypemessages as theAliasUpdateReceivedevent. OptionalExpectedApplicationUrifilter drops messages from other publishers.AliasNamePubSubRefreshStrategy : IAliasNameRefreshStrategy— bridges the reader into the resolver. Matches incoming entries by the resolver's categoryNamespaceUri+ identifier; firesInvalidateon 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—ConfigureAliasNameStoreshows how to seed and register a store.