Eigentlich verwende ich die XML-Serialisierung für die Windows Phone Entwicklung nicht. Bedingt durch den anstehenden Performancetest der SterlingDB habe ich aber eine kleine Basis hierfür geschrieben, um eben diesen optimal durchführen zu können.

Da zum Einen die von mir erhaltenen Performancezahlen (diese werde ich in ein paar Tagen natürlich teilen) sehr stark von den auf der Sterling-Projektseite abweichen, möchte ich meine Basis mit euch teilen und hoffe auf Feedback dazu, um die erhaltenen Zahlen eventueller noch aussagekräftiger zu gestalten.

Damit ich einigermaßen realitätsnah operiere, wurden folgende Voraussetzungen geschaffen:

  • Jedes Repository behandelt seine Daten für sich selbst und persistiert diese mittels XML-Serialisierung in eine eigene Datei.
  • Es soll möglich sein, Trigger zu definieren, welche Manipulationen für bestimmte Aktionen vorsehen.
  • Daten werden nicht sofort persistiert (also serialisiert), sondern erst auf "Wunsch".

Eine zentrale Frage für mich ist hierbei, ob es vom Entwickler erwartet wird, dass ein "gespeichertes" Element auch tatsächlich sofort persistiert wird, oder dies eigens angeworfen wird. Die hier vorliegende Implementierung persistiert die Daten nur auf Wunsch und verhält sich quasi wie eine Transaction. Ein sofortiges Persistieren würde sich definitiv auf die Performance auswirken.

Nun, bewegen wir uns auf die Eingeweide der Implementierung zu.

Trigger

Mit Hilfe des Triggers soll es möglich sein, zu klar definierten Zeitpunkten der Speicherung eingreifen zu können. Als Beispiel kann das Setzen eines eindeutigen Schlüssels, oder aber auch das Setzen eines Erstellungs-, oder Änderungsdatums herhalten. Hierzu wurde eine Schnittstelle definiert:

public interface IXmlTrigger
{
void BeforeSave(Type type, object item);
void AfterSave(Type type, object item);
void BeforeDelete(Type type, object item);
}

Für die weitere Vereinfachung wird auch eine generische Basisklasse zur Verfügung gestellt:

public abstract class BaseXmlTrigger<T, TKey> : IXmlTrigger
{
public abstract void BeforeSave(T item);
public abstract void AfterSave(T item);
public abstract void BeforeDelete(TKey item);

public void BeforeSave(System.Type type, object item)
{
BeforeSave((T)item);
}

public void AfterSave(System.Type type, object item)
{
AfterSave((T)item);
}

public void BeforeDelete(System.Type type, object key)
{
BeforeDelete((TKey)key);
}
}

Auf dieser Basis kann ein konkreter Trigger implementiert werden. Diese Implementierung geht davon aus, dass ein Typ Book vorhanden ist und eine Eigenschaft Id definiert, welche den eindeutigen Schlüssel enthalten soll.

public class BookXmlTrigger : BaseXmlTrigger<Book, long>
{
private long nextId = 0;

public void SetNext(long id)
{
nextId = id;
}

public override void BeforeSave(Book item)
{
if (item.Id < 1)
item.Id = nextId++;
}

public override void AfterSave(Book item)
{

}

public override void BeforeDelete(long key)
{

}
}

Die konkrete Implementierung definiert, dass dieser Trigger für den Typ Book mit einem eindeutigen Schlüssel vom Typ long zuständig ist. Der nächste zu vergebene Identifier kann per Methode SetNext übergeben werden. Innerhalb der Methode BeforeSave wird nun der Identifier hochgezählt.

Nun bedarf es der Repository-Basis, welche sowohl die Persistierung, als auch das Einbinden von Triggern ermöglicht.

Repository

Nachfolgend findet sich die Basis des Repositories zur Persistierung von Daten mittels XML-Serialisierung. Im groben sei gesagt, dass per RegisterTrigger unterschiedliche Trigger für registriert werden können, welche in den Methoden Save und Delete zu tragen kommen (da nur hierfür mögliche Eingriffspunkte vorgesehen wurden).

Zusätzlich ist eine abstrakte Eigenschaft und eine abstrakte Methode definiert. FileName wird verwendet, um den Dateinamen durch die konkrete Implementierung des Repositories für den Serialisierungs-Outputs zu setzen. RegisterKey ist zu überschreiben, um zu definieren, wie der eindeutige Schlüssel ermittelt werden kann.

Die weiteren Methoden kümmern sich um das Laden, Speichern und Löschen von Daten. Via Commit werden alle vorgenommenen Änderungen tatsächlich persistiert.

/// <summary>
/// Base class for repositories based on XML serialization
/// </summary>
/// <typeparam name="T">Type of the entity</typeparam>
/// <typeparam name="TKey">Type of the key</typeparam>
public abstract class BaseXmlRepository<T, TKey>
{
private List<IXmlTrigger> triggers = new List<IXmlTrigger>();
private List<T> items = new List<T>();
private Func<T, TKey> FetchKey { get; set; }

/// <summary>
/// Gets the name of the file that is used to store data handled by this repository
/// </summary>
/// <value>
/// The name of the file.
/// </value>
protected abstract string FileName { get; }
/// <summary>
/// Registers the key for the entity. Use <see cref="DefineKey"/> to register a function to fetch the key from an entity.
/// </summary>
protected abstract void RegisterKey();

/// <summary>
/// Registers the trigger.
/// </summary>
/// <param name="triggerToRegister">The trigger to register.</param>
public void RegisterTrigger(IXmlTrigger triggerToRegister)
{
if (!triggers.Contains(triggerToRegister))
triggers.Add(triggerToRegister);
}

public void DefineKey(Func<T, TKey> keyDefinition)
{
FetchKey = keyDefinition;
}

/// <summary>
/// Loads all entries. Entries are only deserialized when loading for the first time.
/// </summary>
/// <returns></returns>
public List<T> Load()
{
if (items == null || items.Count == 0)
LoadInternal();
return items;
}

private void LoadInternal()
{
var store = IsolatedStorageFile.GetUserStoreForApplication();
using (IsolatedStorageFileStream stream = store.OpenFile(FileName, System.IO.FileMode.OpenOrCreate))
{
XmlSerializer serializer = new XmlSerializer(typeof(List<T>));
items = (List<T>)serializer.Deserialize(stream);
}
}

/// <summary>
/// Saves a specific item. If the item was presisted before, an update will be performed. Call <see cref="Commit"/> to write all data to the store.
/// </summary>
/// <param name="item">The item.</param>
public void Save(T item)
{
HandleBeforeSave(item);

if (!items.Contains(item))
items.Add(item);

HandleAfterSave(item);
}

private void HandleAfterSave(T item)
{
foreach (IXmlTrigger trigger in triggers)
trigger.AfterSave(item.GetType(), item);
}

private void HandleBeforeSave(T item)
{
foreach (IXmlTrigger trigger in triggers)
trigger.BeforeSave(item.GetType(), item);
}

private TKey FetchKeyFromInstance(T instance)
{
return FetchKey(instance);
}

/// <summary>
/// Deletes an item having the given key.
/// </summary>
/// <param name="key">The key to identify a specific item</param>
public void Delete(TKey key)
{
HandleBeforeDelete(key);

var itemToDelete = items.Where(item => FetchKeyFromInstance(item).Equals(key)).FirstOrDefault();

if (itemToDelete != null)
items.Remove(itemToDelete);
}

private void HandleBeforeDelete(TKey key)
{
foreach (IXmlTrigger trigger in triggers)
trigger.BeforeDelete(typeof(T), key);
}

/// <summary>
/// Performs a complete write action
/// </summary>
public void Commit()
{
var store = IsolatedStorageFile.GetUserStoreForApplication();
using (IsolatedStorageFileStream stream = store.OpenFile(FileName, System.IO.FileMode.OpenOrCreate))
{
XmlSerializer serializer = new XmlSerializer(typeof(List<T>));
serializer.Serialize(stream, items);
}
}
}

In diesem Fall ist das Repository so aufgebaut, dass immer alle Daten in den Speicher geladen werden. Wann der tatsächliche Schreibvorgang geschieht, bleibt dem Entwickler überlassen.

Konkretes Repository

Natürlich fehlt nun noch das konkrete Repository, dieses ist nachfolgend zu finden:

public class BookXmlRepository : BaseXmlRepository<Book, long>
{
protected override string FileName
{
get { return "BookRepository.xml"; }
}


protected override void RegisterKey()
{
DefineKey(item => item.Id);
}
}

Einzig der Dateiname muss definiert werden und wie der eindeutige Schlüssel bezogen werden kann. Alle weiteren Funktionalitäten werden durch die Basis abgebildet.

Anmerkungen

Der Nachteil dieser Lösung ist sicherlich, dass alle Daten solange im Speicher sind, solange eine Instanz des Repositories existiert. Auf der Gegenseite steht natürlich, dass sämtliche Manipulationen sehr schnell durchgeführt werden und der Zeitpunkt der Speicherung durch den Entwickler (oder beispielsweise durch das Tombstoning) bestimmt werden kann.

Feedback

Ich freue mich auf jegliches, konstruktives Feedback zu dieser Lösung. Ich möchte mich nicht auf Spezialfälle einlassen, trotzdem eine brauchbare Basis liefern. Mit dem Resultat werde ich einen Performancevergleich zur Sterling DB aufstellen (geplant sind hier auch eine Binär- bzw. JSON-Serialisierung, allerdings nach demselben Schema aufgebaut).

Über den Autor

Norbert Eder

Ich bin ein leidenschaftlicher Softwareentwickler und Fotograf. Mein Wissen und meine Gedanken teile ich nicht nur hier im Blog, sondern auch in Fachartikeln und Büchern.