Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions src/Opc.Ua.XRegistry.Client/GenericXRegistryClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,29 @@ public GenericXRegistryClient(
{
}

/// <summary>
/// Initializes a generic registry client rooted at an explicit registry Object.
/// <para>
/// A domain registry declares its own root rather than reusing the provisional well-known
/// identifier, and a caller typically discovers it by Browse. This overload drives such a
/// registry through the base-model proxies without deriving a domain client.
/// </para>
/// </summary>
/// <param name="session">The connected session whose server hosts the registry.</param>
/// <param name="registryNamespaceUri">The registry companion namespace URI.</param>
/// <param name="registryNodeId">
/// The registry root Object. Pass a null NodeId to use the well-known root.
/// </param>
/// <param name="telemetry">Telemetry context used by the generated proxies.</param>
public GenericXRegistryClient(
ISession session,
string registryNamespaceUri,
NodeId registryNodeId,
ITelemetryContext telemetry)
: base(session, registryNamespaceUri, registryNodeId, telemetry)
{
}

/// <summary>
/// Initializes a generic client bound to the abstract xRegistry base namespace.
/// </summary>
Expand Down
49 changes: 42 additions & 7 deletions src/Opc.Ua.XRegistry.Client/XRegistryClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,8 @@ namespace Opc.Ua.XRegistry.Client
/// </summary>
/// <remarks>
/// <para>
/// This type is the sanctioned extension point. A domain registry — the PubSub Schema Registry,
/// a WoT registry — derives from it and adds domain-specific naming and defaults;
/// This type is the sanctioned extension point. A domain registry derives from it and adds
/// domain-specific naming and defaults;
/// <see cref="GenericXRegistryClient"/> is the plain, sealed implementation for callers that
/// only need the base model.
/// </para>
Expand All @@ -58,7 +58,7 @@ public abstract class XRegistryClient
{
/// <summary>
/// Initializes a registry client bound to a connected <paramref name="session"/> and the
/// registry's companion namespace.
/// registry's companion namespace, using the well-known registry root Object.
/// </summary>
/// <param name="session">The connected session whose server hosts the registry.</param>
/// <param name="registryNamespaceUri">The registry companion namespace URI.</param>
Expand All @@ -71,6 +71,37 @@ protected XRegistryClient(
ISession session,
string registryNamespaceUri,
ITelemetryContext telemetry)
: this(session, registryNamespaceUri, default, telemetry)
{
}

/// <summary>
/// Initializes a registry client bound to a connected <paramref name="session"/>, the
/// registry's companion namespace, and an explicit registry root Object.
/// <para>
/// A domain registry does not necessarily publish its root at
/// <see cref="XRegistryWellKnown.RegistryObject"/> — that identifier is provisional, and a
/// domain model can declare its own root, which a client typically discovers by Browse.
/// Passing the resolved NodeId here makes the root a construction-time input that cannot
/// subsequently drift.
/// </para>
/// </summary>
/// <param name="session">The connected session whose server hosts the registry.</param>
/// <param name="registryNamespaceUri">The registry companion namespace URI.</param>
/// <param name="registryNodeId">
/// The registry root Object. Pass a null NodeId to use the well-known root
/// <see cref="XRegistryWellKnown.RegistryObject"/> in the resolved registry namespace.
/// </param>
/// <param name="telemetry">Telemetry context used by the generated proxies.</param>
/// <exception cref="ArgumentNullException"><paramref name="session"/> or
/// <paramref name="telemetry"/> is <c>null</c>.</exception>
/// <exception cref="ArgumentException"><paramref name="registryNamespaceUri"/> is null/empty.</exception>
/// <exception cref="ServiceResultException">The server does not expose the registry namespace.</exception>
protected XRegistryClient(
ISession session,
string registryNamespaceUri,
NodeId registryNodeId,
ITelemetryContext telemetry)
{
Session = session ?? throw new ArgumentNullException(nameof(session));
Telemetry = telemetry ?? throw new ArgumentNullException(nameof(telemetry));
Expand All @@ -90,6 +121,9 @@ protected XRegistryClient(

RegistryNamespaceUri = registryNamespaceUri;
NamespaceIndex = (ushort)index;
RegistryNodeId = registryNodeId.IsNull
? new NodeId(XRegistryWellKnown.RegistryObject, NamespaceIndex)
: registryNodeId;
Comment thread
marcschier marked this conversation as resolved.
}

/// <summary>
Expand All @@ -108,11 +142,12 @@ protected XRegistryClient(
public ushort NamespaceIndex { get; }

/// <summary>
/// Gets the NodeId of the registry root Object, which a server publishes at a well-known
/// identifier in its registry namespace. This is the starting point for the group lifecycle,
/// so a caller does not have to Browse for it.
/// Gets the NodeId of the registry root Object. By default this is the well-known
/// identifier a server publishes in its registry namespace, so a caller does not have to
/// Browse for it; a domain registry whose root lives elsewhere supplies it at construction.
/// This is the starting point for the group lifecycle.
/// </summary>
public NodeId RegistryNodeId => new(XRegistryWellKnown.RegistryObject, NamespaceIndex);
public NodeId RegistryNodeId { get; }

/// <summary>
/// Gets the telemetry context handed to the generated proxies.
Expand Down
56 changes: 54 additions & 2 deletions tests/Opc.Ua.XRegistry.Tests/XRegistryClientTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,58 @@ public void RegistryNodeIdAddressesTheWellKnownRoot()
});
}

[Test]
public void RegistryNodeIdUsesAnExplicitRootWhenSupplied()
{
var domainRoot = new NodeId(64100u, 1);

var client = new GenericXRegistryClient(
CreateSession().Object,
TestNamespaceUri,
domainRoot,
CreateTelemetry());

Assert.That(client.RegistryNodeId, Is.EqualTo(domainRoot),
"A domain registry declares its own root, so an explicitly supplied NodeId must " +
"win over the provisional well-known identifier.");
}

[Test]
public void RegistryNodeIdFallsBackToTheWellKnownRootForANullExplicitRoot()
{
var client = new GenericXRegistryClient(
CreateSession().Object,
TestNamespaceUri,
default,
CreateTelemetry());

Assert.That(client.RegistryNodeId,
Is.EqualTo(new NodeId(XRegistryWellKnown.RegistryObject, 1)),
"A null NodeId selects the well-known root, so the explicit-root overload stays " +
"equivalent to the namespace-only constructor.");
}

[Test]
public async Task AnExplicitRootDrivesTheGroupLifecycleAsync()
{
Mock<ISession> session = CreateSession();
var calls = new List<CallMethodRequest>();
SetupCall(session, calls);
var domainRoot = new NodeId(64100u, 1);

var client = new GenericXRegistryClient(
session.Object,
TestNamespaceUri,
domainRoot,
CreateTelemetry());
await client.CreateGroupAsync(client.RegistryNodeId, "things")
.ConfigureAwait(false);

Assert.That(calls, Has.Count.EqualTo(1));
Assert.That(calls[0].ObjectId, Is.EqualTo(domainRoot),
"The lifecycle Methods must be invoked on the explicit root, not the well-known one.");
Comment thread
marcschier marked this conversation as resolved.
}

[Test]
public async Task RegistryNodeIdDrivesTheGroupLifecycleAsync()
{
Expand Down Expand Up @@ -799,8 +851,8 @@ private static int CountCalls(List<CallMethodRequest> calls, ExpandedNodeId meth
}

/// <summary>
/// Stands in for a domain registry client (Schema Registry, WoT registry): it extends the
/// abstract base and adds domain naming without re-implementing the lifecycle.
/// Stands in for a domain registry client: it extends the abstract base and adds domain
/// naming without re-implementing the lifecycle.
/// </summary>
private sealed class TestDomainRegistryClient : XRegistryClient
{
Expand Down
Loading