1. Wprowadzenie
Już wiesz, że .NET potrafi serializować i deserializować obiekty do XML domyślnie, opierając się na publicznych polach i właściwościach. Ale kiedy chcesz zrobić ładny XML, kompatybilny z cudzymi programami/standardami, albo gdy nie chcesz umieszczać w XML jakichś wewnętrznych detali, potrzebna jest drobna konfiguracja.
W C# do tego służą atrybuty serializacji — specjalne „etykiety”, którymi oznaczamy klasy, właściwości lub pola. Dzięki tym atrybutom możemy sterować dosłownie wszystkim: które pola trafią do XML, które będą ignorowane, jak element będzie się nazywał, czy stanie się atrybutem, czy ma mieć złożoną strukturę, a nawet jaki będzie porządek elementów!
Wyobraź sobie, że domyślna serializacja to jak automatyczne wypełnianie formularza: imię-nazwisko-środowisko w swoje pola, reszta — jak popadnie. A z atrybutami jesteś jak super-urzędnik: sam decydujesz, którą linię gdzie zapisać, co pominąć, co wyróżnić czerwonym, a co schować do sekcji tajnej.
Mapowanie obiektu na XML
| Klasa/Właściwość | Atrybut | Rezultat w XML |
|---|---|---|
|
Klasa UserList | |
|
|
|
|
|
id="..." (atrybut) |
|
|
|
|
|
|
|
[XmlIgnore] (zob. RegisteredAtString) | - |
|
|
|
|
|
|
2. Przegląd podstawowych atrybutów serializacji XML
Przejdźmy po najpopularniejszych atrybutach, którymi użytkownicy .NET ozdabiali swoje klasy przez wiele pokoleń!
Atrybut [XmlElement] — Nazwa elementu
Zmieniaj nazwę elementu w XML lub oznacz właściwość/pole jako element XML.
public class Person
{
[XmlElement("FullName")]
public string Name { get; set; }
}
XML:
<Person>
<FullName>John Doe</FullName>
</Person>
Jeśli nie podasz atrybutu — domyślnie nazwa elementu będzie taka sama jak nazwa właściwości/pola.
Atrybut [XmlAttribute] — Serializacja do atrybutu XML
Pozwala serializować właściwość/pole jako atrybut XML, a nie jako element.
public class Person
{
[XmlAttribute("id")]
public int Id { get; set; }
}
XML:
<Person id="123"></Person>
W realnych XML-API często używa się atrybutów dla identyfikatorów, dat, flag itp.
Atrybut [XmlIgnore] — Pomijanie pola/właściwości
Marzenie każdego paranoika: całkowicie wykluczyć właściwość z procesu serializacji i deserializacji! Wszystko, co oznaczone tym atrybutem, po prostu nie trafi do finalnego XML.
public class Person
{
[XmlIgnore]
public string InternalNote { get; set; }
}
XML: (właściwości InternalNote tu nie ma i nie będzie)
Atrybuty [XmlArray] i [XmlArrayItem] — kontrola nad kolekcjami
Specjalne atrybuty dla tablic i kolekcji. [XmlArray] ustala nazwę zewnętrznego wrappera-tablicy, a [XmlArrayItem] — nazwę pojedynczego elementu.
public class Person
{
[XmlArray("Phones")]
[XmlArrayItem("Phone")]
public string[] PhoneNumbers { get; set; }
}
XML:
<Person>
<Phones>
<Phone>+12951234567</Phone>
<Phone>+12876543210</Phone>
</Phones>
</Person>
Atrybut [XmlRoot] — Nazwa elementu root
Oznaczasz samą klasę, żeby zmienić nazwę korzeniowego elementu w XML.
[XmlRoot("User")]
public class Person
{
public string Name { get; set; }
}
XML:
<User>
<Name>Anna</Name>
</User>
Atrybut [XmlText] — Serializacja jako zawartość tekstowa elementu
Czasem chcesz (albo wymaga specyfikacja), żeby wartość właściwości była po prostu tekstem wewnątrz elementu, a nie jego pod-elementem czy atrybutem.
public class Note
{
[XmlText]
public string Content { get; set; }
}
XML:
<Note>Zadzwoń do babci!</Note>
Atrybuty [XmlNamespaceDeclarations], [XmlElement(Type = ...)]
Do bardziej zaawansowanych scenariuszy (np. serializacja z namespace'ami, wsparcie dziedziczenia itp.) są też bardziej specyficzne atrybuty. Koniecznie zerknij na oficjalną dokumentację po XML-serializacji dla pełnej listy.
3. Praktyka: poprawiamy serializację na realnym przykładzie
Zróbmy krok po kroku nasze edukacyjne appkę do „XML-serializacji premium”, żeby zadowolić nawet najbardziej wymagającego XML-inspektora.
Klasa bazowa bez atrybutów
Załóżmy, że mamy taką klasę użytkownika:
public class User
{
public int Id { get; set; }
public string Name { get; set; }
public string[] Emails { get; set; }
public DateTime RegisteredAt { get; set; }
public string Password { get; set; }
}
Domyślna serializacja da mniej więcej taki XML (i datę zapisze w swoim formacie):
<User>
<Id>42</Id>
<Name>John Doe</Name>
<Emails>
<string>user@mail.com</string>
<string>admin@site.org</string>
</Emails>
<RegisteredAt>2024-07-01T00:00:00</RegisteredAt>
<Password>qwerty</Password>
</User>
Co jest nie tak:
- Hasło lepiej w ogóle nie serializować.
- Chcemy datę jako atrybut i w czytelnym formacie.
- Chcemy Emails w ładnej formie.
- Chcemy korzeń <Person>.
Dodajemy ustawienia przez atrybuty
[XmlRoot("Person")]
public class User
{
[XmlAttribute("id")]
public int Id { get; set; }
[XmlElement("FullName")]
public string Name { get; set; }
[XmlArray("EMails")]
[XmlArrayItem("Email")]
public string[] Emails { get; set; }
[XmlAttribute("registered")]
public string RegisteredAtString
{
get => RegisteredAt.ToString("yyyy-MM-dd");
set => RegisteredAt = DateTime.Parse(value);
}
[XmlIgnore]
public string Password { get; set; }
[XmlIgnore]
public DateTime RegisteredAt { get; set; }
}
Wyjaśnienie niuansów:
- Używamy [XmlAttribute("registered")] żeby serializować datę jako atrybut.
- Aby serializować nie sam DateTime, a jego reprezentację tekstową, dodajemy osobne pole RegisteredAtString, a rzeczywiste pole ukrywamy przez [XmlIgnore].
- Hasło ukrywamy za pomocą [XmlIgnore].
- Tablica emaili jest skonfigurowana tak, że zewnętrzny tag to <EMails>, a każdy email to <Email>.
Rezultat:
<Person id="42" registered="2024-07-01">
<FullName>John Doe</FullName>
<EMails>
<Email>user@mail.com</Email>
<Email>admin@site.org</Email>
</EMails>
</Person>
4. Praca z zagnieżdżonymi obiektami
XML pokazuje swoją moc najbardziej, gdy mamy zagnieżdżone obiekty (np. użytkownik i jego adresy).
public class Address
{
[XmlAttribute]
public string City { get; set; }
[XmlText]
public string Details { get; set; }
}
public class User
{
// ...inne właściwości, patrz wyżej...
public Address[] Addresses { get; set; }
}
Dodajmy konfigurację dla tablicy adresów:
[XmlArray("UserAddresses")]
[XmlArrayItem("Address")]
public Address[] Addresses { get; set; }
Wówczas XML będzie wyglądać tak:
<Person id="42" registered="2024-07-01">
<FullName>John Doe</FullName>
<EMails>
<Email>user@mail.com</Email>
</EMails>
<UserAddresses>
<Address City="Neon city">Zamkova St 1</Address>
<Address City="North Cave">Krajnaja St 12</Address>
</UserAddresses>
</Person>
Widać, że dla każdego <Address> city to atrybut, a sam adres jest tekstem wewnątrz elementu.
5. Częste błędy i niuanse
Jeśli serializujesz złożony obiekt i nie widzisz zmian w XML, których się spodziewałeś — najprawdopodobniej zapomniałeś poprawnie ustawić atrybut. Czasem właściwości/pola zostają bez oznaczeń. Wtedy .NET po prostu wybiera domyślne zachowanie.
Kolejna pułapka — nazwa kolekcji. Jeśli kolekcja (np. tablica stringów) nie jest oznaczona ani [XmlArray], ani [XmlArrayItem], to elementy zostaną zapisane z tagiem string (jak w początkowym przykładzie powyżej). Aby tego uniknąć, zawsze jawnie ustawiaj nazwy tagów przez atrybuty.
Uważaj na typy właściwości „getter-only” (tylko do odczytu) — takie właściwości domyślnie się nie deserializują. Staraj się mieć publiczny set.
Niektóre typy (np. słowniki albo interfejsy) nie są serializowane przez standardowy XmlSerializer, albo wymagają osobnego traktowania.
GO TO FULL VERSION