CodeGym /Kursy /C# SELF /LINQ to XML: XDocument

LINQ to XML: XDocument, XElement

C# SELF
Poziom 48 , Lekcja 3
Dostępny

1. Wprowadzenie

Pojawienie się LINQ wywołało rewolucję w pracy z XML w .NET. Jeśli wcześniej do modyfikacji XML trzeba było pisać ciężkie konstrukcje z NodeList i XmlElement, teraz możemy wyrażać wszystkie operacje w stylu LINQ. To jak przejście na szybką autostradę po długim błądzeniu po drogach szutrowych.

LINQ to XML rozwiązuje trzy główne zadania:

  • Upraszcza składnię budowania i czytania XML.
  • Sprawia, że nawigacja po dokumencie przypomina pracę z kolekcjami.
  • Pozwala integrować XML z dowolnym zapytaniem LINQ.

Główne klasy: XDocument, XElement, XAttribute

Zanim wskoczysz w głębokie wody LINQ to XML, poznajmy trzy fundamenty:

  • XDocument — to korzeń całego dokumentu, analogiczny do grzbietu papierowego dokumentu, w którym wszystko jest zawarte.
  • XElement — dowolny pojedynczy węzeł (element) XML. To jak pudełko w magazynie, które może zawierać inne pudełka lub towary (elementy, atrybuty i tekst).
  • XAttribute — atrybut elementu, na przykład <User id="42" />.

Schemat wizualny


+-----------------+
|   XDocument     |---------------------+
+-----------------+                     |
            |                      (Root XElement)
            v                           |
      +-------------+              +-----------------+
      |   XElement  |<-------------| XElement (root) |
      +-------------+              +-----------------+
            |                              |
        (XAttribute)                (nested XElements)

2. Czytanie XML za pomocą LINQ to XML

Wyobraźmy sobie, że w naszej aplikacji edukacyjnej (np. książka adresowa) chcemy przeczytać kontakty z pliku XML. Niech struktura pliku będzie taka:

<Contacts>
  <Person id="1">
    <Name>Ivan Ivanov</Name>
    <Email>ivan@example.com</Email>
    <Phones>
      <Phone type="mobile">+12 999 123-45-67</Phone>
      <Phone type="home">+12 812 123-45-67</Phone>
    </Phones>
  </Person>
  <Person id="2">
    <Name>Maria Petrova</Name>
    <Email>maria@example.com</Email>
    <Phones>
      <Phone type="mobile">+12 921 123-45-67</Phone>
    </Phones>
  </Person>
</Contacts>

Wczytujemy XML do pamięci

Najpierw dodajemy dyrektywę do projektu:

using System.Xml.Linq;

Teraz czytamy plik lub string:

// Niech xmlString zawiera dokument XML, albo można użyć XDocument.Load("contacts.xml")
var doc = XDocument.Parse(xmlString);

// Pobieramy listę Person
var persons = doc.Root.Elements("Person");

foreach (var person in persons)
{
    string name = person.Element("Name")?.Value ?? "Bez imienia";
    string email = person.Element("Email")?.Value ?? "Bez e-mail";
    string id = person.Attribute("id")?.Value ?? "brak id";
    
    Console.WriteLine($"Imię: {name}, Email: {email}, id: {id}");
}

- doc.Root daje element root (Contacts).
- Elements("Person") zwraca wszystkie <Person>.
- Potem bierzemy potrzebne elementy/atrybuty przez .Element("...") lub .Attribute("...").
- Operator ?. chroni przed błędami, jeśli element/atrybut nie istnieje.

Anologia

Praca z XElement jest jak rozbieranie matrioszki: wyciągasz jedną, w środku są inne, i tak dalej. Tylko że teraz robisz to jednym zgrabnym zapytaniem, a nie skomplikowanym obchodzeniem.

3. Tworzymy zapytania LINQ po XML

Chcesz uzyskać listę wszystkich telefonów komórkowych wszystkich osób? Proszę bardzo, można to zrobić w stylu LINQ!

var mobilePhones = doc
    .Descendants("Phone") // szukamy wszystkich Phone na dowolnym poziomie
    .Where(p => (string)p.Attribute("type") == "mobile")
    .Select(p => p.Value);

foreach (string phone in mobilePhones)
{
    Console.WriteLine($"Mobilny: {phone}");
}

- Descendants("Phone") wyszukuje wszystkie tagi <Phone> w dokumencie.
- Attribute("type") — pobieramy atrybut.
- Rzutowanie (string) na atrybut daje automatycznie string lub null.

Fajne triki

Można szukać przez wiele poziomów bez martwienia się o zagnieżdżenie. Tego nie da się łatwo zrobić przy użyciu prostego XmlDocument bez kombinowania.

4. Tworzenie i modyfikacja XML "w locie"

XML wygodnie tworzyć w pamięci za pomocą konstruktorów XElement i XDocument. Oto przykład dodania nowej osoby do dokumentu (to część naszego "rozwijającego się aplikacyjnego katalogu"):

// Tworzymy nowy Person
var newPerson = new XElement("Person",
    new XAttribute("id", "3"),
    new XElement("Name", "Sergej Nowikov"),
    new XElement("Email", "sergey@example.com"),
    new XElement("Phones",
        new XElement("Phone", new XAttribute("type", "work"), "+12 812 987-65-43")
    )
);

// Dodajemy do root
doc.Root.Add(newPerson);

// Zapisujemy z powrotem do pliku (lub stringa)
doc.Save("contacts.xml");

Komentarze:

- Można zagnieżdżać elementy przez "matrioszkowe" konstruktory: pierwszy argument to tag, pozostałe to zawartość (mogą być elementy, atrybuty, tekst).
- Atrybuty zwykle podaje się przed bezpośrednią zawartością (tzn. <Phone type="work">...).

5. Interakcja z kolekcjami C# — magia LINQ

Często trzeba zserializować zwykłą kolekcję obiektów (np. List<Person>) do XML lub odwrotnie. Robi się to bardzo prosto, praktycznie jedną linijką LINQ:

var people = new List<Person>
{
    new Person { Id = 1, Name = "Ivan Ivanov", Email = "ivan@example.com" },
    // i tak dalej
};

var doc = new XDocument(
    new XElement("Contacts",
        people.Select(p =>
            new XElement("Person",
                new XAttribute("id", p.Id),
                new XElement("Name", p.Name),
                new XElement("Email", p.Email)
            )
        )
    )
);
Klasa Person wygląda tak (dla pełnego obrazu):
public class Person
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Email { get; set; }
}

Teraz masz proste, przejrzyste i kontrolowane przekształcenie kolekcji C# do XML!

6. Przydatne metody i techniki XElement i XDocument

Nawigacja

  • .Elements("TagName") — wszystkie bezpośrednie elementy (bezpośredni potomkowie).
  • .Descendants("TagName") — wszystkie zagnieżdżone elementy gdziekolwiek wewnątrz bieżącego elementu.
  • .Parent — element rodzicielski.
  • .Ancestors() — wszyscy przodkowie elementu (przydatne przy znalezieniu ścieżki).
  • .FirstNode, .LastNode, .NextNode, .PreviousNode — nawigacja po sąsiadach.

Wyszukiwanie z warunkami

var personsWithEmail = doc.Root
    .Elements("Person")
    .Where(p => !string.IsNullOrEmpty((string)p.Element("Email")));

Dodawanie i usuwanie

// Dodajemy telefon do istniejącego użytkownika
var maria = doc.Root.Elements("Person")
    .FirstOrDefault(p => (string)p.Element("Name") == "Maria Petrova");

maria?.Element("Phones")?.Add(
    new XElement("Phone", new XAttribute("type", "work"), "+12 812 111-22-33")
);

// Usunąć użytkownika po id
doc.Root.Elements("Person")
    .Where(p => (string)p.Attribute("id") == "3")
    .Remove();

Konwersja z powrotem do stringa/pliku

string xmlText = doc.ToString(); // z wcięciami
doc.Save("contacts.xml");

7. Przydatne niuanse

Porównanie LINQ to XML z innymi podejściami (.NET)

Podejście Kiedy używać Zalety Wady
XmlDocument
Stary kod, legacy Integracja z XPath Dużo kodu, skomplikowane API
XmlSerializer
Dla serializacji obiektów Łatwo serializować C# ↔ XML Niska elastyczność
LINQ to XML
Wszystko inne: parsowanie, edycja Linowy, zwięzły, potężny, LINQ Trochę więcej pamięci

Zastosowanie w realnych projektach i na rozmowach kwalifikacyjnych

  • Parsowanie plików konfiguracyjnych, gdzie nie ma ścisłego mapowania do klas C#.
  • Import/eksport danych (np. eksport/import danych użytkowników).
  • Migracje, obróbka "brudnych" lub złożonych struktur XML.
  • Szybkie transformacje (np. przekształcenie jednego formatu XML w inny).

"Czemu warto to znać?" — na rozmowie kwalifikacyjnej mogą poprosić o przekształcenie lub sparsowanie jakiegoś złożonego XML, a znając LINQ to XML zrobisz to prościej i ładniej niż używając starych API.

8. Typowe błędy przy pracy z LINQ to XML

Błąd nr 1: próba odwołania się do nieistniejącego elementu bez sprawdzenia.
Jeśli element może być nieobecny, używaj operatora ?. lub wcześniejszej kontroli na null. W przeciwnym razie dostaniesz NullReferenceException i nieoczekiwane crashowanie w runtime.

Błąd nr 2: pomylenie atrybutów z elementami.
Atrybuty i elementy to różne rzeczy w XML. Czasem dane są rozdzielone między nie. Na przykład:

<User login="admin"><Status>active</Status></User>

Aby uzyskać wartość atrybutu login, użyj .Attribute("login"), a żeby dostać zawartość taga <Status>.Element("Status").

Błąd nr 3: niezrozumienie działania metody .Remove().
Metoda .Remove() natychmiast zmienia strukturę dokumentu XML w pamięci. Nie zwraca nic, i "cofnąć" usunięcia się nie da. Nowicjusze czasem oczekują, że zwróci usunięte elementy lub w jakiś sposób zasygnalizuje zmianę — tak nie jest.

Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION