Skip to content

Commit 973cc81

Browse files
authored
fix(di): accept FunctionalGroupType subtypes in BrowseFunctionalGroupsAsync (#4032)
### Summary This PR makes `DiDeviceClient.BrowseFunctionalGroupsAsync()` accept `FunctionalGroupType` subtypes instead of requiring an exact type-definition match and adds a regression test that verifies derived functional groups are included in the returned results. ### Problem In OPC UA DI, functional groups may be modeled as instances of `FunctionalGroupType` or of any subtype derived from it. A client helper that enumerates functional groups therefore needs to follow the OPC UA type hierarchy instead of requiring the object's `TypeDefinition` to be exactly `FunctionalGroupType`. Previously, `BrowseFunctionalGroupsAsync()` filtered browse results through a helper named `IsSubtypeOf()`, but that helper only compared `typeDefinition == expectedType`. As a result, any device that exposed functional groups through a legal subtype of `FunctionalGroupType` would have those groups silently filtered out even though they were valid DI functional-group objects. ### Changes - Replace the exact type-definition comparison in `BrowseFunctionalGroupsAsync()` with a real subtype check based on `Session.NodeCache.IsTypeOfAsync(...)`. - Preserve the existing filtering behaviour for non-functional-group objects. - Add a regression test that verifies a derived functional-group type is returned while an unrelated object is still excluded.
1 parent 8602d40 commit 973cc81

2 files changed

Lines changed: 82 additions & 4 deletions

File tree

src/Opc.Ua.Di.Client/DiDeviceClient.cs

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -292,7 +292,10 @@ public async IAsyncEnumerable<FunctionalGroupEntry> BrowseFunctionalGroupsAsync(
292292
{
293293
ct.ThrowIfCancellationRequested();
294294

295-
if (!IsSubtypeOf(reference.TypeDefinition, functionalGroupTypeId))
295+
if (!await IsSubtypeOfAsync(
296+
reference.TypeDefinition,
297+
functionalGroupTypeId,
298+
ct).ConfigureAwait(false))
296299
{
297300
continue;
298301
}
@@ -394,11 +397,15 @@ public async IAsyncEnumerable<FunctionalGroupEntry> BrowseFunctionalGroupsAsync(
394397
return default;
395398
}
396399

397-
private static bool IsSubtypeOf(
400+
private ValueTask<bool> IsSubtypeOfAsync(
398401
ExpandedNodeId typeDefinition,
399-
ExpandedNodeId expectedType)
402+
ExpandedNodeId expectedType,
403+
CancellationToken ct)
400404
{
401-
return typeDefinition == expectedType;
405+
return Session.NodeCache.IsTypeOfAsync(
406+
typeDefinition,
407+
expectedType,
408+
ct);
402409
}
403410

404411
private static string? ExtractStringFromWrappedValue(Variant wrapped)

tests/Opc.Ua.Di.Tests/DiDeviceClientTests.cs

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,77 @@ public async Task ForDeviceAsyncReturnsClientWhenReadSucceeds()
238238
Assert.That(client.DeviceNodeId, Is.EqualTo(new NodeId("dev-1", 2)));
239239
}
240240

241+
[Test]
242+
public async Task BrowseFunctionalGroupsAsyncReturnsSubtypeInstancesAsync()
243+
{
244+
Mock<ISession> sessionMock = CreateSessionMock();
245+
var nodeCacheMock = new Mock<INodeCache>(MockBehavior.Strict);
246+
sessionMock.SetupGet(s => s.NodeCache).Returns(nodeCacheMock.Object);
247+
248+
ExpandedNodeId functionalGroupSubtype = new("VendorFunctionalGroupType", 2);
249+
ExpandedNodeId unrelatedType = new("FolderType", 2);
250+
ReferenceDescription subtypeGroup = new()
251+
{
252+
NodeId = new ExpandedNodeId(new NodeId("group-1", 2)),
253+
DisplayName = new LocalizedText("VendorGroup"),
254+
TypeDefinition = functionalGroupSubtype,
255+
NodeClass = NodeClass.Object
256+
};
257+
ReferenceDescription folder = new()
258+
{
259+
NodeId = new ExpandedNodeId(new NodeId("folder-1", 2)),
260+
DisplayName = new LocalizedText("Folder"),
261+
TypeDefinition = unrelatedType,
262+
NodeClass = NodeClass.Object
263+
};
264+
265+
sessionMock
266+
.Setup(s => s.BrowseAsync(
267+
It.IsAny<RequestHeader?>(),
268+
It.IsAny<ViewDescription?>(),
269+
It.IsAny<uint>(),
270+
It.IsAny<ArrayOf<BrowseDescription>>(),
271+
It.IsAny<CancellationToken>()))
272+
.ReturnsAsync(new BrowseResponse
273+
{
274+
Results =
275+
[
276+
new BrowseResult
277+
{
278+
StatusCode = StatusCodes.Good,
279+
References = [subtypeGroup, folder]
280+
}
281+
]
282+
});
283+
284+
nodeCacheMock
285+
.Setup(c => c.IsTypeOfAsync(
286+
functionalGroupSubtype,
287+
Opc.Ua.Di.ObjectTypeIds.FunctionalGroupType,
288+
It.IsAny<CancellationToken>()))
289+
.Returns(new ValueTask<bool>(true));
290+
nodeCacheMock
291+
.Setup(c => c.IsTypeOfAsync(
292+
unrelatedType,
293+
Opc.Ua.Di.ObjectTypeIds.FunctionalGroupType,
294+
It.IsAny<CancellationToken>()))
295+
.Returns(new ValueTask<bool>(false));
296+
297+
var client = new DiDeviceClient(
298+
sessionMock.Object, new NodeId("dev-1", 2), NullTelemetry());
299+
300+
var groups = new System.Collections.Generic.List<FunctionalGroupEntry>();
301+
await foreach (FunctionalGroupEntry group in client.BrowseFunctionalGroupsAsync())
302+
{
303+
groups.Add(group);
304+
}
305+
306+
Assert.That(groups, Has.Count.EqualTo(1));
307+
Assert.That(groups[0].NodeId, Is.EqualTo(new NodeId("group-1", 2)));
308+
Assert.That(groups[0].DisplayName, Is.EqualTo("VendorGroup"));
309+
nodeCacheMock.VerifyAll();
310+
}
311+
241312
[Test]
242313
public void ForDeviceAsyncThrowsOnNullSession()
243314
{

0 commit comments

Comments
 (0)