使用LINQ搜索XDocument而不知道命名空间

84
有没有一种方法可以在不知道命名空间的情况下搜索XDocument?我有一个记录所有SOAP请求并加密敏感数据的过程。我想根据名称查找任何元素。类似于,给我所有名称为CreditCard的元素。我不关心命名空间是什么。
我的问题似乎与LINQ和需要xml命名空间有关。
我有其他从XML中检索值的过程,但我知道这些其他过程的命名空间。
XDocument xDocument = XDocument.Load(@"C:\temp\Packet.xml");
XNamespace xNamespace = "http://CompanyName.AppName.Service.Contracts";

var elements = xDocument.Root
                        .DescendantsAndSelf()
                        .Elements()
                        .Where(d => d.Name == xNamespace + "CreditCardNumber");

我真的希望能够在不知道命名空间的情况下搜索XML,就像这样:

XDocument xDocument = XDocument.Load(@"C:\temp\Packet.xml");
var elements = xDocument.Root
                        .DescendantsAndSelf()
                        .Elements()
                        .Where(d => d.Name == "CreditCardNumber")

这样做行不通,因为在编译时我不知道命名空间。

有什么方法可以解决这个问题吗?

<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
<s:Body xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
    <Request xmlns="http://CompanyName.AppName.Service.ContractA">
        <Person>
            <CreditCardNumber>83838</CreditCardNumber>
            <FirstName>Tom</FirstName>
            <LastName>Jackson</LastName>
        </Person>
        <Person>
            <CreditCardNumber>789875</CreditCardNumber>
            <FirstName>Chris</FirstName>
            <LastName>Smith</LastName>
        </Person>
        ...

<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
<s:Body xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
    <Request xmlns="http://CompanyName.AppName.Service.ContractsB">
        <Transaction>
            <CreditCardNumber>83838</CreditCardNumber>
            <TransactionID>64588</FirstName>
        </Transaction>      
        ...
7个回答

96

正如Adam在评论中解释的那样,XName可以转换为字符串,但是当存在命名空间时,该字符串需要包含命名空间。这就是为什么将.Name与字符串进行比较失败或者为什么不能将“Person”作为参数传递给XLinq方法来过滤它们的名称。
XName由前缀(命名空间)和本地名称组成。如果您忽略命名空间,则要查询的是本地名称。
谢谢Adam :)

您无法将节点的名称作为.Descendants()方法的参数,但您可以用这种方式查询:

var doc= XElement.Parse(
@"<s:Envelope xmlns:s=""http://schemas.xmlsoap.org/soap/envelope/"">
<s:Body xmlns:xsi=""http://www.w3.org/2001/XMLSchema-instance"" xmlns:xsd=""http://www.w3.org/2001/XMLSchema"">
  <Request xmlns=""http://CompanyName.AppName.Service.ContractA"">
    <Person>
        <CreditCardNumber>83838</CreditCardNumber>
        <FirstName>Tom</FirstName>
        <LastName>Jackson</LastName>
    </Person>
    <Person>
        <CreditCardNumber>789875</CreditCardNumber>
        <FirstName>Chris</FirstName>
        <LastName>Smith</LastName>
    </Person>
   </Request>
   </s:Body>
</s:Envelope>");

编辑:复制/粘贴出了问题

var persons = from p in doc.Descendants()
              where p.Name.LocalName == "Person"
              select p;

foreach (var p in persons)
{
    Console.WriteLine(p);
}

对我来说这个方案可行...


6
名称是一个XName(扩展名称),并且XName刚好可以转换为字符串,因此将.Name与字符串进行比较时,会导致问题提出者的查询失败。XName由前缀和本地名称组成,在忽略命名空间时,您要查询的是本地名称。 - Adam Sills
那是我在Somerockstar的回答中放置的评论。为了澄清,我可以添加这个。你是对的。 - Stéphane
非常感谢您的快速帮助。希望这能帮助其他人。 - Mike Barlow - BarDev
希望如此,我第一次使用XLinq时也遇到了同样的问题 :) - Stéphane
1
@MikeBarlow-BarDev,确实如此;-) - Simon_Weaver

91
您可以从根元素获取命名空间:
XDocument xDocument = XDocument.Load(@"C:\temp\Packet.xml");
var ns = xDocument.Root.Name.Namespace;

现在你可以使用加号操作符轻松获取所有所需的元素:

root.Elements(ns + "CreditCardNumber")

这似乎是更好的答案,因为它仍然允许您使用大多数 LINQ 操作。 - Ehtesh Choudhury
6
只有当所有元素都不在不同的命名空间中时,才能接受此答案。如果只是询问根文档的命名空间,那么很容易知道该命名空间,但是询问特定名称的任何元素,则无论该元素本身位于哪个命名空间中都更棘手。这就是为什么我认为使用XElement.Name.LocalName(通常通过linq)的答案更为通用。 - Caleb Holt
1
这个答案不够通用。 - ceztko

15

我想我找到了我要的东西。你可以看到在下面的代码中,我执行了评估Element.Name.LocalName == "CreditCardNumber"。在我的测试中,这似乎是有效的。我不确定它是否是最佳实践,但我打算使用它。

XDocument xDocument = XDocument.Load(@"C:\temp\Packet.xml");
var elements = xDocument.Root.DescendantsAndSelf().Elements().Where(d => d.Name.LocalName == "CreditCardNumber");

现在我有一些元素,可以对其值进行加密。

如果有更好的解决方案,请提供。谢谢。


如果你不知道命名空间或者不关心它,那么这是一个完美的解决方案。谢谢! - SeriousM

3

有一些关于扩展方法的回答已经被删除了,不确定原因。这是我自己的版本,适合我的需求。

public static class XElementExtensions
{
    public static XElement ElementByLocalName(this XElement element, string localName)
    {
        return element.Descendants().FirstOrDefault(e => e.Name.LocalName == localName && !e.IsEmpty);
    }
}
IsEmpty 是用来过滤带有 x:nil="true" 的节点。
可能还有其他细微之处,使用时请谨慎。

太好了!谢谢Simon。我几乎可以说这应该是唯一正确的答案...如果你只做一次,那么你将会做100次,而其他所有答案都相对笨拙,与el.ElementByLocalName("foo")相比。 - Tim Cooper

2

如果您的XML文档总是在同一个节点中定义命名空间(如给出的两个示例中的Request节点),则可以通过查询并查看结果具有哪个命名空间来确定它:

XDocument xDoc = XDocument.Load("filename.xml");
//Initial query to get namespace:
var reqNodes = from el in xDoc.Root.Descendants()
               where el.Name.LocalName == "Request"
               select el;
foreach(var reqNode in reqNodes)
{
    XNamespace xns = reqNode.Name.Namespace;
    //Queries making use of namespace:
    var person = from el in reqNode.Elements(xns + "Person")
                 select el;
}

0

我正在遭受一种严重的“我知道那是解决方案,但我对那个解决方案感到失望”的情况... 我最近写了一个类似下面的查询(我很快会替换它,但它具有教育价值):

var result = xdoc.Descendants("{urn:schemas-microsoft-com:rowset}data")
.FirstOrDefault()?
.Descendants("{#RowsetSchema}row");

如果我从XML中删除命名空间,我可以像这样编写相同的查询:
var result = xdoc.Descendants("data")
.FirstOrDefault()?
.Descendants("row");

我计划编写自己的扩展方法,使我可以保持命名空间不变,并像这样搜索节点:
var result = xdoc.Descendants("rs:data") 
.FirstOrDefault()?
.Descendants("z:row"); 
//'rs:' {refers to urn:schemas-microsoft-com:rowset}
//'z:' {refers to xmlns:z=#RowsetSchema}

我的评论在代码下面,指出了我希望通过扩展方法库来隐藏解决方案的丑陋之处。再次强调,我知道之前发布的解决方案,但我希望API本身能够更加流畅地处理这个问题。(你看到我做了什么吗?)


-7

只需使用Descendents方法:

XDocument doc = XDocument.Load(filename);
String[] creditCards = (from creditCardNode in doc.Root.Descendents("CreditCardNumber")
                        select creditCardNode.Value).ToArray<string>();

3
由于后代参数要求一个XName,而这里的XName带有命名空间前缀,所以这种方式行不通。 - Stéphane

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接