Serializacja–typy Surrogate

Serialziacja jest dobrze znanym tematem. Jakiś czas temu pisałem, jak oddelegować serializację jednego obiektu do drugiego. Pokazałem to na przykładzie wzorca singleton – w tamtym przypadku chcieliśmy oddelegować serializację do IObjectReference, który zwracał po prostu zawsze tą samą instancję.

Dziś trochę inny scenariusz. Załóżmy, że w plikach, zawsze chcemy trzymać czas w UTC a nie w konkretnej strefie. Ponadto nie mamy dostępu ani do kodu źródłowego DateTime ani nie chcemy korzystać z DateTimeOffset. Rozwiązaniem będzie użycie ISerializationSurrogate – interfejsu odpowiedzialnego za podstawienie logiki serializacji dla konkretnego typu. Gdybyśmy mieli dostęp do kodu źródłowego moglibyśmy po prostu zaimplementować interfejs ISerializable. W naszym przypadku nie mamy takiego komfortu albo po prostu nie chcemy mieszać logiki biznesowej z mechanizmem serializacji.

Zacznijmy od szablonu, na którym będziemy testować nasze rozwiązanie:

private static void Main(string[] args)
{            
  var timestamp=new DateTime(2010,1,1,11,50,0);

  using (MemoryStream stream = new MemoryStream())
  {
      serializer.Serialize(stream, timestamp);
      stream.Position = 0;
      DateTime timestamp2 = (DateTime)serializer.Deserialize(stream);
      Console.WriteLine(timestamp2);
  }
}

Datetime nie przechowuje informacji o danej strefie czasowej – stanowi zwykły kontener na daty. Co prawda, jest tam pole oznaczające czy mamy do czynienia z UTC czy lokalną strefą ale nie definiuje to dokładnego przesunięcia. Napiszmy zatem obiekt zastępczy, który będzie zapisywał do bazy to co dokładnie chcemy:

public class TimestampSurrogate : ISerializationSurrogate
{
   public void GetObjectData(object obj, SerializationInfo info, StreamingContext context)
   {
       info.AddValue("Date", ((DateTime)obj).ToUniversalTime().ToString("u"));
   }

   public object SetObjectData(object obj, SerializationInfo info, StreamingContext context, ISurrogateSelector selector)
   {
       return DateTime.ParseExact(info.GetString("Date"), "u", null).ToLocalTime();
   }
}

Interfejs implementuje dwie metody w zależności czy obiekt jest właśnie zapisywany czy odczytywany. Konstrukcja powinna być już znana ponieważ przypomina np. tą przedstawioną w poście o serializacji singleton. Klasa SerializationInfo (parametr wejściowy) nie jest niczym nowym – służy do zapisu\odczytu danych ze strumienia. Metoda GetObjectData ma za zadanie zapisanie obiektu do strumienia. Za pomocą SerializationInfo.AddValue dodajemy pole “Date”, którego wartością jest czas w formacie  UTC. Parametr wejściowy obj to obiekt, dla którego TimestampSurrogate jest zastępczy. Zatem jak najbardziej możliwe jest w naszym przypadku zrzutowanie obj do DateTime – chcemy stworzyć obiekt surrogate dla DateTime.

Analogicznie sprawa wygląda z SetObjectData. SetObjectData wywoływany jest w momencie deserializacji i ma za zadanie wypełnienie przetwarzanego obiektu danymi w prawidłowym formacie. W naszym przypadku chcemy odczytać zapisaną datę w formacie UTC (info.GetString(“Date”)) a następnie skonwertować do strefy lokalnej.

Proszę zauważyć, że w SetObjectData nie wykorzystujemy parametru obj, którym jest dany obiekt. Przed wywołaniem SetObjectData jest utworzona instancja danego obiektu (DateTime).  Utworzona instancja ma jednak pewne specyficzne cechy. Wszystkie jej pola są ustawione na NULL albo 0. Konstruktor również nie jest wywołany. Zadaniem SetObjectData jest wypełnienie tych pól prawidłowymi wartościami. W naszym przypadku mamy do czynienia z Immutable Object więc nie ma to sensu – tworzymy i zwracamy całkowicie nowy obiekt.

Na końcu w jakiś sposób musimy powiedzieć serializatorowi, że chcemy używać danego surrogate. Służy do tego SurrogateSelctor, który definiuje mapowanie pomiędzy obiektami a ich surrogate:

private static void Main(string[] args)
{            
  var timestamp=new DateTime(2010,1,1,11,50,0);

  var serializer = new SoapFormatter();
  
  SurrogateSelector surrogateSelector = new SurrogateSelector();
  surrogateSelector.AddSurrogate(typeof(DateTime), serializer.Context, new TimestampSurrogate());
  
  serializer.SurrogateSelector = surrogateSelector;

  using (MemoryStream stream = new MemoryStream())
  {
      serializer.Serialize(stream, timestamp);
      stream.Position = 0;
      DateTime timestamp2 = (DateTime)serializer.Deserialize(stream);
      Console.WriteLine(timestamp2);
  }
}

Warto jeszcze raz podkreślić, że SurrogateSelector zachowuje się dokładnie jak mapowanie – możemy wielokrotnie wywoływać AddSurrogate aby określić relacje między obiektami a ich surrogate.  Dlaczego w AddSurrogate przekazujemy kontekst? Kontekst zawiera informacje o typie serializacji – czyli np. zawiera informacje o tym czy zapisujemy do pliku, pamięci czy sieci. Dzięki temu, możemy użyć różnego surrogate w zależności od danego “kontekstu” zapisu. Jedną z wartości Context.State może być: CrossProcess , CrossMachine, File, Persistence, Remoting , Other , Clone, CrossDomain. Więcej informacji można znaleźć tutaj.

Przykład:

ss.AddSurrogate(typeof(Employee),new StreamingContext(StreamingContextStates.Remoting),new EmployeeSerializationSurrogate());

Ponadto, oprócz kilku mapowań istnieje możliwość definicji nawet kilka ISurrogateSelector. Jeśli jest ktoś zainteresowany, to odsyłam do dokumentacji – tutaj chciałbym tylko zaznaczyć, że metody ChainSelector oraz GetNextSelector będą nas interesować:

public interface ISurrogateSelector 
{
    void ChainSelector(ISurrogateSelector selector);
    ISurrogateSelector GetNextSelector();
    ISerializationSurrogate GetSurrogate(Type type, StreamingContext context,out ISurrogateSelector selector);
}

One thought on “Serializacja–typy Surrogate”

  1. Wypasik info… najpierw człowiek zakłada że nie możliwe, a potem się okazuje że ktoś wcześniej o tym pomyślał.

Leave a Reply

Your email address will not be published.