Repository navigation
ArgumentOutOfRangeException when topic key starts overlaps with Root (e.g., RO) #154
Description
When importing or otherwise assigning a Parent to a newly constructed Topic, SetParent() can incorrectly throw ArgumentOutOfRangeException ("A descendant cannot be its own parent") for topics whose Key is a case-insensitive prefix of the target parent's UniqueKey, even when the topic isn't actually an ancestor or descendant of that parent. This happens because a new topic's GetUniqueKey() returns only its own Key until Parent is assigned, so the guard in SetParent() ends up comparing that key against the full parent path using StartsWith(). Any topic keyed R, Ro, Roo, or Root (case-insensitive) anywhere in a site whose root topic is named Root will therefore fail to be created.
Background
The TopicFactory.Create() factory method invokes the Topic constructor, which sets Key before assigning Parent. At the moment Parent is set, SetParent() runs:
if (parent.GetUniqueKey().StartsWith(GetUniqueKey(), StringComparison.OrdinalIgnoreCase)) {
throw new ArgumentOutOfRangeException(nameof(parent), "A descendant cannot be its own parent.");
}Since the new topic isn't yet assigned to a parent, GetUniqueKey() returns e.g., "RO". The check becomes "Root:Configuration:Metadata:Country:LookupList".StartsWith("RO", OrdinalIgnoreCase), which evaluates true—not because RO is an actual ancestor, but because Root case-insensitively starts with Ro.
Implementation Notes
The guard should only treat parent as a descendant of the current topic when the prefix matches a full path segment boundary, not any substring. The character following the matched prefix must thus be either the end of the string or the : delimiter.
Affected Files
OnTopic/Topic.cs(SetParent())
Proposed Fix
var uniqueKey = GetUniqueKey();
var parentKey = parent.GetUniqueKey();
if (
parentKey.StartsWith(uniqueKey, StringComparison.OrdinalIgnoreCase) &&
parentKey.Length > uniqueKey.Length &&
parentKey[uniqueKey.Length] == ':'
) {
throw new ArgumentOutOfRangeException(nameof(parent), "A descendant cannot be its own parent.");
}Tasks
- Update the ancestor-guard check in
SetParent()to require a:delimiter (or end of string) after the matched prefix - Add a regression test covering a topic keyed
Ro(or similar) being created anywhere under a topic namedRoot - Add a regression test covering the general case, where a short topic key is a case-insensitive prefix of an unrelated ancestor path segment
Activity