我正在寻找一种使用linq在XML树中查找第一个结果级别的方法。我拥有的XML如下所示:
<column>
<row>
<object/>
<column column-id="1" column-name="abc">
<row>
<column>
<row>
<column column-id="2" column-name="abc"/>
</row>
</column>
</row>
</column>
</row>
<row>
<column column-id="3" column-name="abc">
<row>
<column/>
</row>
</column>
</row>
</column>
现在我希望得到列名为columns
的所有第一级abc
。所以结果应该是:
<column column-id="1" column-name="abc">...</column>
<column column-id="3" column-name="abc">...</column>
我已经尝试过以下代码:
layout.Descendants("column")
.Where(x => x.Attribute("column-name").Value.Equals("abc") && !x.Ancestors("column").Any());
当搜索的XElement layout
未命名为"column"
且未嵌套在名为"column"
的任何容器元素内时,此方法正常。但我的XElement
实际上属于一个文档,其根元素名为"column"
,因此x.Ancestors("column").Any()
表达式错误地过滤掉所有匹配项。即通过初始化layout
,可以使用上面的XML字符串重现该问题,如下所示:
var layout = XElement.Parse(xmlString);
我想保留变量中的关系,因为我必须稍后进行更改。
是否有办法限制祖先选择器?
答案 0 :(得分:1)
假设您事先并不知道要查询的元素的精确深度,您要做的是下降指定元素下面的元素层次结构,并返回最顶层匹配给定条件的元素,在这种情况下具有名称"column"
。
作为一种快速而肮脏的方法,您只能使用TakeWhile()
检查候选匹配列的祖先是layout
的后代还是
var matches = layout
.Descendants("column")
.Where(x => (string)x.Attribute("column-name") == "abc" && !x.Ancestors().TakeWhile(a => a != layout).Any(a => a.Name == "column"));
更高性能的通用解决方案是在XElement
上引入一个扩展方法,该方法枚举给定元素的所有后代,返回与给定谓词匹配的最顶层元素。这通常是有用的,例如如果想要查询将要接近深层XML层次结构顶层的后代,因为它可以避免不必要地下降到匹配的节点中:
public static partial class XElementExtensions
{
/// <summary>
/// Enumerates through all descendants of the given element, returning the topmost elements that match the given predicate
/// </summary>
/// <param name="root"></param>
/// <param name="filter"></param>
/// <returns></returns>
public static IEnumerable<XElement> DescendantsUntil(this XElement root, Func<XElement, bool> predicate, bool includeSelf = false)
{
if (predicate == null)
throw new ArgumentNullException();
return GetDescendantsUntil(root, predicate, includeSelf);
}
static IEnumerable<XElement> GetDescendantsUntil(XElement root, Func<XElement, bool> predicate, bool includeSelf)
{
if (root == null)
yield break;
if (includeSelf && predicate(root))
{
yield return root;
yield break;
}
var current = root.FirstChild<XElement>();
while (current != null)
{
var isMatch = predicate(current);
if (isMatch)
yield return current;
// If not a match, get the first child of the current element.
XElement next = (isMatch ? null : current.FirstChild<XElement>());
if (next == null)
// If no first child, get the next sibling of the current element.
next = current.NextSibling<XElement>();
// If no more siblings, crawl up the list of parents until hitting the root, getting the next sibling of the lowest parent that has more siblings.
if (next == null)
{
for (var parent = current.Parent as XElement; parent != null && parent != root && next == null; parent = parent.Parent as XElement)
{
next = parent.NextSibling<XElement>();
}
}
current = next;
}
}
public static TNode FirstChild<TNode>(this XNode node) where TNode : XNode
{
var container = node as XContainer;
if (container == null)
return null;
return container.FirstNode.NextSibling<TNode>(true);
}
public static TNode NextSibling<TNode>(this XNode node) where TNode : XNode
{
return node.NextSibling<TNode>(false);
}
public static TNode NextSibling<TNode>(this XNode node, bool includeSelf) where TNode : XNode
{
if (node == null)
return null;
for (node = (includeSelf ? node : node.NextNode); node != null; node = node.NextNode)
{
var nextTNode = node as TNode;
if (nextTNode != null)
return nextTNode;
}
return null;
}
}
然后使用它:
var matches = layout
.DescendantsUntil(x => x.Name == "column")
.Where(x => (string)x.Attribute("column-name") == "abc");
扩展方法应该具有合理的性能,因为它避免了递归和复杂的嵌套linq查询。
示例.Net fiddle显示两个选项。
答案 1 :(得分:0)
说明问题的另一种方法是,您希望谓词包含与文档根目录的距离。
这是一个执行此操作的函数:
static int DistanceToRoot(XElement elem, XElement root)
{
var dist = 0;
var curr = elem;
while(curr != root)
{
dist++;
curr = curr.Parent;
}
return dist;
}
你就这样使用它(基于你的例子,我们想要的距离 2 ):
var columns = from column in xml.Descendants("column")
where
DistanceToRoot(column, xml.Root) == 2 &&
column.Attribute("column-name").Value == "abc"
select column;
foreach(var abc in xyzs)
{
Console.WriteLine(abc);
Console.Write("Distance is: ");
Console.WriteLine(DistanceToRoot(abc, xml.Root));
Console.ReadLine();
}
结果是:
<column column-id="1" column-name="abc">
<row>
<column>
<row>
<column column-id="2" column-name="abc" />
</row>
</column>
</row>
</column>
Distance is: 2
<column column-id="3" column-name="abc">
<row>
<column />
</row>
</column>
Distance is: 2
<强> Rextester Demo 强>