LINQ to XML cz. 4, Transformacje i serializacja

Drugim ważnym zastosowaniem Linq to Xml jest serializacja dokumentów Xmlowych. Serializacja i deserializacja to procesy umożliwiające konwersje dokumentów Xmlowych odpowiednio z i do postaci obiektowej. Tak jak w przypadku transformacji Linq to Xml nie stara się konkurować z istniejącym na platformie .NET technologiami do serializacji (XmlSerializer, DataContractSerializer) ale jest bardzo poręczny w przypadkach gdy trzeba dokonać serializacji małych obiektów bezpośrednio w kodzie.

Zacznijmy od deserializacji - elementy "wylot" i "przylot" można przedstawić w sposób obiektowy za pomocą następującej klasy (warto zauważyć w jaki sposób zdefiniowane są pola - wykorzystują one wprowadzone w C# 3.0 tzw. "automatic properties". Dzięki nim nie trzeba definiować prywatnych zmiennych w klasie do przechowywania wartości pola - kompilator zrobi to za nas):

public class Przelot {
    public string KodLotniska { get; set; }
    public DateTime Data { get; set; }
    public string NazwaLotniska { get; set; }
}

Kod realizujący taką serializację jest dzięki Linq to Xml stosunkowo prosty. Z dokumentu Xmlowego wybieramy elementy "wylot" i  "przylot" i dla każdego takiego elementu tworzymy obiekt klasy Przelot. Oto przykład:

IEnumerable<Przelot> przeloty = 
from przelot in xDoc.Elements("wycieczka").Elements("segment").Elements()
select new Przelot() {
      Data = (DateTime)przelot.Attribute("data"),
       KodLotniska = (string)przelot.Attribute("kod-lotniska"),
       NazwaLotniska = (string)przelot
     };

W powyższym fragmencie jest zastosowany nowy sposób inicjalizacji obiektu po jego stworzeniu - mimo, że klasa Przelot nie posiada żadnego poza domyślnym konstruktora możemy w prosty sposób zainicjalizować pola stworzonego obiektu dzięki nowej składni wprowadzonej w C# 3.0. Ten nowy, poręczny sposób inicjalizacji obiektów nazywa się "object initializers".

Inną nowością w C# 3.0 są klasy anonimowe. Pozwalają one na stworzenie obiektów "ad hoc" bez konieczności definiowania klas. Za pomocą składni podobnej do "object initizalizers" tworzymy obiekt nie nazwany, w którym tworzymy właściwości dynamicznie przez przypisanie wartości danej właściwości. Aby odwołać się później do takiego obiektu deklarujemy zmienną za pomocą słowa kluczowego "var" (pozwalając w ten sposób kompilatorowi określić rzeczywisty typ zmiennej ponieważ sami go tak naprawdę nie znamy) i odwołujemy się do obiektu za pomocą nazw właściwości użytych podczas tworzenia obiektu. Poniższy fragment przedstawia zastosowanie klas anonimowych do deserializacji dokumentów Xmlowych za pomocą Linq to Xml:

var przeloty = 
from przelot in xDoc.Elements("wycieczka").Elements("segment").Elements()
select new {
      Date = (DateTime)przelot.Attribute("data"),
       AirportCode = (string)przelot.Attribute("kod-lotniska"),
       AirportName = (string)przelot
     };


foreach(var przelot in przeloty) {
Console.WriteLine(string.Format("{0} ({1}) {2}",
  przelot.AirportName, przelot.AirportCode, przelot.Date));
}

Jak widać z powyższych przykładów, że deserializacja dokumentów Xmlowych do obiektów za pomocą Linq to Xml nie jest skomplikawana. A jak jest z serializacją? Najlepiej chyba pokazać to na przykładzie. Załóżmy, że mamy tablicę zawierającą obiekty typu Przelot (definicja klasy Przelot taka jak w jednym z wcześniejszych przykładów). Zdefiniujmy ją w następujący sposób:

var przeloty = new[] {
new {
   Wylot = new Przelot() {
     KodLotniska = "SEA",
     Data = DateTime.Parse("2008-10-09T15:25Z"),
     NazwaLotniska = "Seattle-Tacoma International"
    },
    Przylot = new Przelot()  {
    KodLotniska = "LHR",
      Data = DateTime.Parse("2008-10-10T02:20Z"),
      NazwaLotniska = "London Heathrow"
    },
},
new {
Wylot = new Przelot() {
     KodLotniska = "LHR",
Data = DateTime.Parse("2008-10-10T05:35Z"),
      NazwaLotniska = "London Heathrow"
    },
    Przylot = new Przelot() {
     KodLotniska = "WRO",
      Data = DateTime.Parse("2008-10-10T10:05Z"),
      NazwaLotniska = "Wroclaw Strachowice"
    }
  }
};

Tablica zdefiniowana jest za pomocą "wodotrysków" pochodzących z C# 3.0. Mianowicie jest to tablica obiektów anonimowych, z których każdy zawiera informacje o wylocie i przylocie będącymi instancjami klasy Przelot. Instancje klasy Przelot inicjalizowane są za pomocą funkcjonalności "object initializers". Zadanie polega na zamianie powyższej tablicy na dokument Xmlowy taki jak ten używany w przykładach w naszym cyklu artykułów. Voila - oto kod:

XDocument xDoc = new XDocument(
new XDeclaration("1.0", string.Empty, string.Empty),
  new XElement("wycieczka",
   from przelot in przeloty
select new XElement("segment",
new XElement("wylot",
                new XAttribute("kod-lotniska", przelot.Wylot.KodLotniska),
                 new XAttribute("data", przelot.Wylot.Data),
                 new XText(przelot.Wylot.NazwaLotniska)),
               new XElement("przylot",
                new XAttribute("kod-lotniska", przelot.Przylot.KodLotniska),
                 new XAttribute("data", przelot.Przylot.Data),
                 new XText(przelot.Przylot.NazwaLotniska)))));

Kod może wydawać się odrobinę znajomy... Szczególnie jak się spojrzy na kod z pierwszego odcinka cyklu oraz na przykładowy kod pokazujący w jaki sposób dokonać transformacji dokumentu Xmlowego za pomocą Linq to Xml. Podobieństwo do kodu z pierwszego odcinku cyklu nie powinno dziwić - w końcu budujemy praktycznie ten sam dokument z tą tylko różnicą, że zamiast wstawiać wszystkie wartości ręcznie chcemy je pobrać z tablicy. Musimy zatem iterować po tej tablicy do czego używamy technologii Linq co z kolei przypomina kod z tego artykułu. Uważni jednak zauważą jedną dość istotną różnicę. Mianowicie przykładowa transformacja przedstawiona na początku artykułu iterowała po elementach z przestrzeni nazw System.Xml.Linq. Natomiast w tym przypadku iterujemy po "normalnej" tablicy obiektów. Jak to jest możliwe? Otóż w tamym przykładzie używaliśmy Linq to Xml podczas gdy w tym przykładzie została użyta technologia Linq to... Objects. Tak oto Linq spotkał Linqa...