Wyszukaj XDocument za pomocą LINQ bez znajomości przestrzeni nazw

81

Czy istnieje sposób na przeszukiwanie XDocument bez znajomości przestrzeni nazw? Mam proces, który rejestruje wszystkie żądania SOAP i szyfruje poufne dane. Chcę znaleźć wszystkie elementy na podstawie nazwy. Coś w stylu, podaj mi wszystkie elementy, na których nazwa to CreditCard. Nie obchodzi mnie, czym jest przestrzeń nazw.

Wydaje się, że mój problem dotyczy LINQ i wymaga przestrzeni nazw xml.

Mam inne procesy, które pobierają wartości z XML, ale znam przestrzeń nazw dla tych innych procesów.

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");

Naprawdę chcę mieć możliwość wyszukiwania xml bez wiedzy o przestrzeniach nazw, coś takiego:

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

To nie zadziała, ponieważ nie znam wcześniej przestrzeni nazw w czasie kompilacji.

Jak można to zrobić?

<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>      
        ...
Mike Barlow - BarDev
źródło
Sprawdź tę odpowiedź z innego pytania: stackoverflow.com/questions/934486/ ...
MonkeyWrench

Odpowiedzi:

90

Jak Adam precyzuje w komentarzu, XName można zamienić na ciąg, ale ten ciąg wymaga przestrzeni nazw, jeśli taka istnieje. Dlatego porównanie .Name z łańcuchem nie powiedzie się lub dlaczego nie można przekazać „Person” jako parametru do metody XLinq, aby przefiltrować ich nazwę.
XName składa się z prefiksu (przestrzeni nazw) i nazwy lokalnej. Nazwa lokalna jest tym, o co chcesz zapytać, jeśli ignorujesz przestrzenie nazw.
Dziękuję Adam :)

Nie możesz umieścić nazwy węzła jako parametru metody .Descendants (), ale możesz zapytać w ten sposób:

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>");

EDYCJA: zła kopia / przeszłość z mojego testu :)

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

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

To działa dla mnie ...

Stéphane
źródło
5
Może pomóc wyjaśnienie, dlaczego twoja odpowiedź jest taka, jaka jest: Name to XName, a XName po prostu można zamienić na ciąg, więc porównanie .Name z ciągiem kończy się niepowodzeniem z zapytaniem osoby zadającej pytanie. XName składa się z prefiksu i nazwy lokalnej, a nazwa lokalna jest tym, o co chcesz zapytać, jeśli ignorujesz przestrzenie nazw.
Adam Sills,
to było w komentarzu, który zamieściłem w odpowiedzi Somerockstar. Mogę to dodać dla jasności, masz rację
Stéphane
Wielkie dzięki za szybką pomoc. Mam nadzieję, że pomoże to komuś innemu.
Mike Barlow - BarDev
Mam nadzieję, że utknąłem na tym samym problemie po raz pierwszy używając XLinq :)
Stéphane
1
@ MikeBarlow-BarDev tak się stało ;-)
Simon_Weaver
88

Możesz wziąć przestrzeń nazw z elementu głównego:

XDocument xDocument = XDocument.Load(@"C:\temp\Packet.xml");
var ns = xDocument.Root.Name.Namespace;

Teraz możesz łatwo uzyskać wszystkie pożądane elementy za pomocą operatora plus:

root.Elements(ns + "CreditCardNumber")
Sascha
źródło
Wydaje się, że jest to lepsza odpowiedź, ponieważ nadal umożliwia korzystanie z większości LINQoperacji.
Ehtesh Choudhury
6
Ta odpowiedź jest akceptowalna tylko wtedy, gdy żaden z elementów nie znajduje się w innej przestrzeni nazw niż dokument główny. Tak, łatwo jest poznać przestrzeń nazw, jeśli po prostu zapytasz o nią dokument główny, ale trudniej jest zapytać o elementy o danej nazwie, niezależnie od przestrzeni nazw, w której sam element się znajduje. Dlatego uważam, że odpowiedź polega na użyciu XElement. Name.LocalName (zwykle przez linq) są bardziej uogólnione.
Caleb Holt
Ta odpowiedź nie jest wystarczająco ogólna.
ceztko
14

Myślę, że znalazłem to, czego szukałem. Możesz zobaczyć w poniższym kodzie, że wykonuję ocenę Element.Name.LocalName == "CreditCardNumber". To wydawało się działać w moich testach. Nie jestem pewien, czy to najlepsza praktyka, ale zamierzam z niej skorzystać.

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

Teraz mam elementy, w których mogę zaszyfrować wartości.

Jeśli ktoś ma lepsze rozwiązanie, podaj je. Dzięki.

Mike Barlow - BarDev
źródło
To idealne rozwiązanie, jeśli nie znasz przestrzeni nazw ani nie przejmujesz się nią. Dziękuję Ci!
SeriousM
2

Jeśli twoje dokumenty XML zawsze definiują przestrzeń nazw w tym samym węźle ( Requestwęźle w dwóch podanych przykładach), możesz to określić, wykonując zapytanie i sprawdzając, jaką przestrzeń nazw ma wynik:

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;
}
Clivest
źródło
2

Istnieje kilka odpowiedzi z metodami rozszerzającymi, które zostały usunięte. Nie pewny dlaczego. Oto moja wersja, która działa na moje potrzeby.

public static class XElementExtensions
{
    public static XElement ElementByLocalName(this XElement element, string localName)
    {
        return element.Descendants().FirstOrDefault(e => e.Name.LocalName == localName && !e.IsEmpty);
    }
}

Służy IsEmptydo filtrowania węzłów za pomocąx:nil="true"

Mogą występować dodatkowe subtelności - więc używaj ich ostrożnie.

Simon_Weaver
źródło
Śliczny! Dzięki Simon. Chciałbym niemal powiedzieć, że to powinna być tylko poprawna odpowiedź .... jeśli robisz to raz potem będziesz robił to 100 razy i wszystkie inne odpowiedzi są dość niezgrabne w porównaniu do: el.ElementByLocalName ( „foo”) .
Tim Cooper
-7

Po prostu użyj metody Descendents:

XDocument doc = XDocument.Load(filename);
String[] creditCards = (from creditCardNode in doc.Root.Descendents("CreditCardNumber")
                        select creditCardNode.Value).ToArray<string>();
Evan Chiu
źródło
3
to nie zadziała, ponieważ parametr descendant pyta o XName, a XName jest w tym miejscu poprzedzony przestrzenią nazw.
Stéphane