从XML文档中删除节点的神秘故障

时间:2013-07-08 13:24:14

标签: c# .net xml linq

如果有人能够解释这一点,我会感到惊讶,但知道其他人是否可以重现我正在经历的奇怪感觉会很有趣......

我们有一个基于InfoPath的东西可以处理很多表单。表单数据应符合XSD,但InfoPath会以所谓的“my-fields”的形式不断添加自己的元数据。我们想删除my-fields,然后我写了这个简单的方法:

string StripMyFields(string xml)
{
    var doc = new XmlDocument();
    doc.LoadXml(xml);

    var matches = doc.SelectNodes("//node()").Cast<XmlNode>().Where(n => n.NamespaceURI.StartsWith("http://schemas.microsoft.com/office/infopath/"));
    Dbug("Found {0} nodes to remove.", matches.Count());
    foreach (var m in matches)
        m.ParentNode.RemoveChild(m);

    return doc.OuterXml;
}

现在出现了非常奇怪的东西!当我运行此代码时,它的行为与我期望的一样,删除InfoPath名称空间中的任何节点。但是,如果我注释掉对Dbug的调用,代码就会完成,但是一个“my-field”仍然在XML中。

我甚至评论了方便的Dbug方法的内容,它的行为方式仍然相同:

void Dbug(string s, params object[] args)
{
    //if (args.Length > 0)
    //    s = string.Format(s, args);
    //Debug.WriteLine(s);
}

输入XML:

<?xml version="1.0" encoding="UTF-8"?>
<skjema xmlns:my="http://schemas.microsoft.com/office/infopath/2003/myXSD/2008-03-03T22:25:25" xml:lang="en-us">
    <Field-1643 orid="1643">data.</Field-1643>
    <my:myFields>
        <my:field1>Al</my:field1>
        <my:group1>
            <my:group2>
                <my:field2 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">2009-01-01</my:field2>
                <Field-1611 orid="1611">More data.</Field-1611>
                <my:field3>true</my:field3>
            </my:group2>
            <my:group2>
                <my:field2>2009-01-31</my:field2>
                <my:field3>false</my:field3>
            </my:group2>
        </my:group1>
    </my:myFields>
    <Field-1612 orid="1612">Even more data.</Field-1612>
    <my:field3>Blah blah</my:field3>
</skjema>

除非我调用Dbug,否则不会删除“my:field3”元素(在底部,文本“Blah blah”)。

显然宇宙不应该是这样的,但我很想知道其他人是否能够重现。

我在Win8 Enterprise 6.2.9200上使用VS2012 Premium(11.0.50727.1 RTMREL)和FW 4.5.50709。

4 个答案:

答案 0 :(得分:3)

首先要做的事情。 LINQ使用称为deferred execution的概念。这意味着在实际实现查询之前不会获取任何结果(例如通过枚举)。

为什么节点删除问题会有问题?让我们看看代码中会发生什么:

  1. SelectNodes创建XPathNodeIteratorXPathNavigator使用XmlNodeList将数据提供给SelectNodes
  2. 返回的XPathNodeIterator
  3. Cast根据提供的XPath表达式遍历xml文档树
  4. WhereXPathNodeIterator只是决定DBug返回的节点是否应参与最终结果
  5. 我们在Cast方法调用之前到达。暂时,假设它不在那里。此时,没有什么实际上已经发生了。我们只有非物质化的 LINQ查询。

    当我们开始迭代时,事情会发生变化。所有迭代器(WhereWhereIterator都有自己的迭代器)开始滚动。 CastIteratorXPathNodeIterator询问项目,然后请求Field-1643最终返回第一个节点(Where)。不幸的是,这个没有通过my:myFields测试,因此我们要求下一个测试。运气好my:field1,这是一场比赛 - 我们将其删除。

    我们快速前往my:field1(再次, WhereIterator CastIterator XPathNodeIterator ),这也已删除。在这里停留片刻。删除my:field1将其与父项分离,从而将其(null)兄弟节点设置为XPathNodeIterator(删除节点之前/之后没有其他节点)。

    目前的状况如何? my:field1知道它的当前元素是XPathNodeIterator节点,它刚被删除。已删除,如与父分离,但迭代器仍保留引用。听起来不错,让我们问下一个节点。 Current的作用是什么?检查其NextSibling项目,并询问null(因为它没有孩子可以先行走) - 这是<Root> <James>Bond</James> <Jason>Bourne</Jason> <Jimmy>Keen</Jimmy> <Tom /> <Bob /> </Root> ,因为我们刚刚执行了分离。这意味着迭代结束了。完成工作。

    因此,通过在迭代期间更改集合结构,您只从文档中删除了两个节点(实际上只有一个节点,因为第二个删除的节点是已删除的节点的子节点)。

    使用更简单的XML可以观察到相同的行为:

    J

    假设我们想要摆脱以var doc = new XmlDocument(); doc.LoadXml(xml); var matches = doc .SelectNodes("//node()") .Cast<XmlNode>() .Where(n => n.Name.StartsWith("J")); foreach (var node in matches) { node.ParentNode.RemoveChild(node); } Console.WriteLine(doc.InnerXml); 开头的节点,导致文档只包含诚实的人名:

    DBug

    不幸的是, Jason Jimmy 仍然存在。 James '下一个兄弟(由迭代器返回的那个)本来是 Jason ,但是只要我们从树中分离 James 没有兄弟姐妹和迭代结束。

    现在,为什么它适用于CountToList调用实现了查询。迭代器已经运行,我们可以访问开始循环时所需的所有节点。在Where之后调用{{1}}或者在调试期间检查结果时会发生同样的事情(VS甚至通知您检查结果将枚举集合)。

答案 1 :(得分:0)

我认为这取决于schrodinger的cat问题,在你查看或采取行动之前,实际上不会编译查询结果。这意味着,在调用Count()(或获取结果的任何其他函数)或在调试器中查看它之前,结果不存在。作为测试,试试这样:

if (matches.Any())
    foreach (var m in matches)
        m.ParentNode.RemoveChild(m);

答案 2 :(得分:0)

非常奇怪,只有当您在调试时实际查看结果时才会删除最后一个节点。顺便说一句,将结果转换为List然后循环遍历它也可以。

List<XmlNode> matches = doc.SelectNodes("//node()").Cast<XmlNode>().Where(n =>   n.NamespaceURI.StartsWith("http://schemas.microsoft.com/office/infopath/")).ToList();
        foreach (var m in matches)
        {
            m.ParentNode.RemoveChild(m);
        }

答案 3 :(得分:0)

jimmy_keen的解决方案对我有用。我只有一个简单的

//d is an XmlDocument
XmlNodeList t = d.SelectNodes(xpath);
foreach (XmlNode x in t)
{
    x.ParentNode.RemoveChild(x);
}
d.Save(outputpath);

这将只删除3个节点,而在调试模式下单步执行将删除1000多个节点。

在foreach解决问题之前添加一个Count:

var count = t.Count;