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 |
|---|---|---|---|
|
Stary kod, legacy | Integracja z XPath | Dużo kodu, skomplikowane API |
|
Dla serializacji obiektów | Łatwo serializować C# ↔ XML | Niska elastyczność |
|
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.
GO TO FULL VERSION