mirror of
https://github.com/OPCFoundation/UA-.NETStandard.git
synced 2026-10-29 15:11:53 +08:00
Include subtypes of AliasNameCategoryType in AliasNameClient.EnumerateSubCategoriesAsync (#4611)
# Description `AliasNameClient.EnumerateSubCategoriesAsync` kept a browsed child only when its TypeDefinition was exactly `AliasNameCategoryType`. OPC 10000-17 §6.3.1 declares `<SubAliasNameCategories>` as an OptionalPlaceholder of `AliasNameCategoryType` and does not rule out subtypes, so sub-categories modelled with a subtype were silently dropped. The exact type is still accepted without a round trip; any other type definition is checked against the type hierarchy with `Session.NodeCache.IsTypeOfAsync` (the pattern `RoleManagementClient` uses for `RoleType`). The node cache keeps the type references, so each distinct type is resolved once. The check runs on the first browse page and on every BrowseNext page. `docs/AliasNames.md` notes that subtypes are included. Tests: the mocked session harness gets a node cache that answers `IsTypeOfAsync` from a subtype map; a new test covers the exact type, a direct subtype, a subtype of a subtype, a FolderType child and an untyped child. It fails on master (`Missing: < "Vendor", "Derived" >`) and passes with the fix. `Opc.Ua.Client.Tests` 2733/2733 on net10.0 and net8.0; `Opc.Ua.Features.Tests` AliasNameIntegrationTests (real server) 7/7. Fork CI (full matrix incl. macOS) and CodeQL: green — [CI run 36769850776](https://github.com/biancode/UA-.NETStandard/actions/runs/36769850776) (full matrix incl. macOS), [CodeQL run 36769855329](https://github.com/biancode/UA-.NETStandard/actions/runs/36769855329). ## Related Issues - Fixes #4610 ## Checklist - [x] I have signed the [CLA](https://opcfoundation.org/license/cla/ContributorLicenseAgreementv1.0.pdf) and read the [CONTRIBUTING](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/CONTRIBUTING.md) doc. - [x] I have added tests that prove my fix is effective or that my feature works and increased code coverage. - [x] I have added all necessary documentation. - [x] I have verified that my changes do not introduce (new) build or analyzer warnings. - [ ] I ran **all** tests locally using the **UA.slnx** solution against at least .net **framework** and .net **10**, and all passed. (Affected suites on net10.0 and net8.0 locally on macOS; .NET Framework is covered by the fork CI's Windows net48 legs.) - [ ] I fixed **all** failing and flaky tests in the CI pipelines and **all** CodeQL warnings. - [ ] I have addressed **all** PR feedback received.
This commit is contained in:
+2
-1
@@ -363,7 +363,8 @@ IReadOnlyList<AliasNameDataType> result =
|
||||
* `AddAliasesToCategoryAsync(IEnumerable<AliasNameAddRequest>, ct)`
|
||||
* `DeleteAliasesFromCategoryAsync(IEnumerable<AliasNameDeleteRequest>, ct)`
|
||||
* `EnumerateSubCategoriesAsync(ct)` — `IAsyncEnumerable` of child
|
||||
`AliasNameSubCategoryInfo`.
|
||||
`AliasNameSubCategoryInfo`, including children whose type is a
|
||||
subtype of `AliasNameCategoryType`.
|
||||
* `ReadLastChangeAsync(ct)` — returns the `VersionTime` (or `null` when
|
||||
the category does not expose `LastChange`).
|
||||
|
||||
|
||||
@@ -408,7 +408,8 @@ namespace Opc.Ua.Client.AliasNames
|
||||
/// <summary>
|
||||
/// Asynchronously enumerates the sub-categories of this category
|
||||
/// (Part 17 §6.3.1 <c>SubAliasNameCategories</c>) via a forward
|
||||
/// browse on <see cref="ReferenceTypeIds.Organizes"/>.
|
||||
/// browse on <see cref="ReferenceTypeIds.Organizes"/>. Children of
|
||||
/// <c>AliasNameCategoryType</c> or of a subtype are returned.
|
||||
/// </summary>
|
||||
/// <exception cref="ServiceResultException">The browse operation
|
||||
/// returned an error status code.</exception>
|
||||
@@ -452,7 +453,7 @@ namespace Opc.Ua.Client.AliasNames
|
||||
{
|
||||
foreach (ReferenceDescription r in SnapshotReferences(br.References))
|
||||
{
|
||||
if (!r.TypeDefinition.Equals(ObjectTypeIds.AliasNameCategoryType))
|
||||
if (!await IsAliasNameCategoryAsync(r.TypeDefinition, ct).ConfigureAwait(false))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
@@ -479,7 +480,7 @@ namespace Opc.Ua.Client.AliasNames
|
||||
|
||||
foreach (ReferenceDescription r in SnapshotReferences(nextReferences))
|
||||
{
|
||||
if (!r.TypeDefinition.Equals(ObjectTypeIds.AliasNameCategoryType))
|
||||
if (!await IsAliasNameCategoryAsync(r.TypeDefinition, ct).ConfigureAwait(false))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
@@ -523,6 +524,30 @@ namespace Opc.Ua.Client.AliasNames
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Whether a browsed child is a sub-category: an instance of
|
||||
/// <c>AliasNameCategoryType</c> or of one of its subtypes. Part 17
|
||||
/// §6.3.1 declares <c><SubAliasNameCategories></c> as an
|
||||
/// <c>OptionalPlaceholder</c> of that type and does not rule out
|
||||
/// subtypes, so the type hierarchy is checked through the node cache.
|
||||
/// </summary>
|
||||
private async ValueTask<bool> IsAliasNameCategoryAsync(
|
||||
ExpandedNodeId typeDefinition,
|
||||
CancellationToken ct)
|
||||
{
|
||||
if (typeDefinition.IsNull)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (typeDefinition.Equals(ObjectTypeIds.AliasNameCategoryType))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
return await Session.NodeCache
|
||||
.IsTypeOfAsync(typeDefinition, ObjectTypeIds.AliasNameCategoryType, ct)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task<NodeId> ResolveChildAsync(
|
||||
string childBrowseName,
|
||||
CancellationToken ct)
|
||||
|
||||
@@ -290,5 +290,54 @@ namespace Opc.Ua.Client.Tests.AliasNames
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task EnumerateSubCategoriesAsyncIncludesSubtypesOfAliasNameCategoryTypeAsync()
|
||||
{
|
||||
var harness = AliasNameSessionHarness.Create();
|
||||
var vendorCategoryType = new ExpandedNodeId(new NodeId("VendorCategoryType", 2));
|
||||
var derivedCategoryType = new ExpandedNodeId(new NodeId("DerivedCategoryType", 2));
|
||||
harness.Supertypes[vendorCategoryType] = ObjectTypeIds.AliasNameCategoryType;
|
||||
harness.Supertypes[derivedCategoryType] = vendorCategoryType;
|
||||
|
||||
harness.BrowseHandler = _ => new BrowseResult
|
||||
{
|
||||
StatusCode = StatusCodes.Good,
|
||||
ContinuationPoint = ByteString.Empty,
|
||||
References = new[]
|
||||
{
|
||||
CreateReference("Standard", ObjectTypeIds.AliasNameCategoryType),
|
||||
CreateReference("Vendor", vendorCategoryType),
|
||||
CreateReference("Derived", derivedCategoryType),
|
||||
CreateReference("Folder", ObjectTypeIds.FolderType),
|
||||
CreateReference("Untyped", ExpandedNodeId.Null)
|
||||
}.ToArrayOf()
|
||||
};
|
||||
|
||||
var client = AliasNameClient.OpenStandardAliases(harness.Session);
|
||||
var names = new List<string>();
|
||||
|
||||
await foreach (AliasNameSubCategoryInfo info in
|
||||
client.EnumerateSubCategoriesAsync().ConfigureAwait(false))
|
||||
{
|
||||
names.Add(info.BrowseName.Name);
|
||||
}
|
||||
|
||||
string[] expectedNames = ["Standard", "Vendor", "Derived"];
|
||||
Assert.That(names, Is.EqualTo(expectedNames));
|
||||
|
||||
static ReferenceDescription CreateReference(
|
||||
string name,
|
||||
ExpandedNodeId typeDefinition)
|
||||
{
|
||||
return new ReferenceDescription
|
||||
{
|
||||
NodeId = new ExpandedNodeId(new NodeId(name, 2)),
|
||||
BrowseName = new QualifiedName(name, 2),
|
||||
DisplayName = new LocalizedText(name),
|
||||
TypeDefinition = typeDefinition
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,6 +56,11 @@ namespace Opc.Ua.Client.Tests.AliasNames
|
||||
public Func<BrowseDescription, BrowseResult> BrowseHandler { get; set; }
|
||||
public Func<ByteString, BrowseResult> BrowseNextHandler { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Subtype to supertype pairs the node cache answers type checks from.
|
||||
/// </summary>
|
||||
public Dictionary<ExpandedNodeId, ExpandedNodeId> Supertypes { get; } = [];
|
||||
|
||||
public Func<BrowsePath, BrowsePathResult> BrowsePathHandler { get; set; }
|
||||
|
||||
private AliasNameSessionHarness(
|
||||
@@ -77,6 +82,30 @@ namespace Opc.Ua.Client.Tests.AliasNames
|
||||
|
||||
var harness = new AliasNameSessionHarness(sessionMock, messageContext);
|
||||
|
||||
var nodeCache = new Mock<INodeCache>(MockBehavior.Loose);
|
||||
nodeCache
|
||||
.Setup(c => c.IsTypeOfAsync(
|
||||
It.IsAny<ExpandedNodeId>(),
|
||||
It.IsAny<ExpandedNodeId>(),
|
||||
It.IsAny<CancellationToken>()))
|
||||
.Returns<ExpandedNodeId, ExpandedNodeId, CancellationToken>(
|
||||
(subType, superType, _) =>
|
||||
{
|
||||
ExpandedNodeId current = subType;
|
||||
while (!current.IsNull)
|
||||
{
|
||||
if (current == superType)
|
||||
{
|
||||
return new ValueTask<bool>(true);
|
||||
}
|
||||
current = harness.Supertypes.TryGetValue(current, out ExpandedNodeId next)
|
||||
? next
|
||||
: ExpandedNodeId.Null;
|
||||
}
|
||||
return new ValueTask<bool>(false);
|
||||
});
|
||||
sessionMock.SetupGet(s => s.NodeCache).Returns(nodeCache.Object);
|
||||
|
||||
sessionMock
|
||||
.Setup(s => s.TranslateBrowsePathsToNodeIdsAsync(
|
||||
It.IsAny<RequestHeader>(),
|
||||
|
||||
Reference in New Issue
Block a user