News, examples, tips, ideas and plans.
Thoughts around ORM, .NET and SQL databases.

Friday, January 14, 2011

How to handle non-persistent collection

Today we've faced with an interesting scenario from Jensen: an Entity has a non-persistent collection which should be serialized to and deserialized from a persistent field on demand. Here is how the problem was initially described on the support website (go to the end of the page):

Object A should contain a list of samples (object T). I could do something like
A a = GetA();
a.Load(); (convert the byte[] to a List)
and something like
a.Store(); (convert the List to a byte[])
m_transactionScope.Commit();
But I don't like calling a Load() and Store() function each time, it would be easier if this could be automated. When A is loaded from the database it should Load(), when saved to the database it should Store().

After several experiments I've found a solution.

1. Add a set of properties to the desired class.
public class MyEntity : Entity {
    ...
    [Field] // Is used as an indicator that collection has been changed
    private bool IsChanged { get; set; }

    [Field] // Backing field for serialized collection value
    private byte[] CollectionValue { get; set; }

    // The collection itself. Note that it is not persistent.
    public ObservableCollection<int> Collection { get; set; }
    ...

2. Add proper methods for serialization & deserialization:
    private static byte[] Serialize(ObservableCollection<int> collection)
    {
      var ms = new MemoryStream();
      var bf = new BinaryFormatter();
      bf.Serialize(ms, collection);
      return ms.ToArray();
    }

    private static ObservableCollection<int> Deserialize(byte[] bytes)
    {
      var bf = new BinaryFormatter();
      var ms = new MemoryStream(bytes);
      return (ObservableCollection<int>) bf.Deserialize(ms);
    }

3. Add Entity initializer & event handlers:
    protected override void OnInitialize()
    {
      base.OnInitialize();
      // Initializing the collection
      if (CollectionValue != null && CollectionValue.Length > 0)
        Collection = Deserialize(CollectionValue);
      else
        Collection = new ObservableCollection<int>();

      // Subscribing to events
      Collection.CollectionChanged += Collection_CollectionChanged;
      Session.Events.Persisting += Session_Persisting;
    }

    void Session_Persisting(object sender, EventArgs e)
    {
      if (!IsChanged)
        return;

      // Serializing the collection right before persist, if it is changed
      CollectionValue = Serialize(Collection);
      IsChanged = false;
    }

    void Collection_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
    {
      IsChanged = true;
    }

So, what is going on here?
An instance of MyEntity is subscribed to a CollectionChanged event of MyEntity.Collection and updates MyEntity.IsChanged property when CollectionChanged event is fired. As the property is persistent, the MyEntity instance is added into a queue for persisting. Right before the persist procedure takes place, the instance of MyEntity receives a notification from its Session. During handling of this event we check whether the collection has been changed or not. If so, we update MyEntity.CollectionValue property and changes are successfully persisted to a database.

Note, that I've decided to use ObservableCollection type instead of List because the solution requires a notifications from the collection.

Hope this helps.

4 comments:

  1. Thanks, Peter. Glad to hear that =)

    ReplyDelete
  2. [Field] // Is used as an indicator that collection has been changed
    private bool IsChanged

    what purpuse of [Field] attribute here?

    ReplyDelete
  3. By changing the value of the field we are adding the Entity to a queue for persist, so this is sort of guarantee that the queue will contain at least one element and Session.Persisting event will be fired.

    For instance, imagine a scenario when you load an instance of MyEntity and change the content of a non-persistent collection only. After that you call transactionScope.Complete(). In this case, Session.Persisting event won't be fired because none of persistent fields were modified.

    ReplyDelete