devLux

Self-Tracking Entities Part 1 - Fundamentals

Many .NET developers enjoy change tracking either since the days with the Data Sets or later with the Entity Framework. This document describes - in four posts - how to easily implement self-tracking entities that fit well into an MVVM application for WPF. As the solution relies on proven concepts, the basic ideas may also be applied to other technologies as well.

We focus especially at client-side development as this technology fits best there. The document describes the difference between change tracking and self-tracking entities by also providing implementations for both.

The entire article is split into five parts. The first post talks about the patterns behind and describes the change tracking implementation. The second post is dedicated to self-tracking entities. The third post depicts how to extend the self-tracking entities by validation concerns. The forth posts shows how to implement a composite entity and how to enhance validation by using the fluent validation API. And the fifth post demonstrates how to adapt self-tracking entities to an MVVM application:

Each post starts by describing the principals behind the respective solution. Then the solution’s expected behavior is documented with unit tests. The posts are then completed by presenting a fully functional implementation.

This post talks about the patterns behind self-tracking entities and describes a change tracking implementation.

Introduction

In the following paragraphs we clarify the terminology, especially the terms change tracking and self-tracking entities.

We talk about change tracking when are able to retrieve information about an object’s modifications, either directly on the affected object or through an other component. This applies to single objects as well to collections of objects. The later track the changes by keeping a list of operations that afterwards are executed - in a single transaction - on a data source.

Without self-tracking entities the entities are Plain Old C# Objects (POCOs) and do not track the changes internally. The developer has to store the modifications in an own component. This component usually consists of four sets, one for the newly added (inserted), one for the changed (updated), one for the removed (deleted), and one for the unchanged entities. The developer has to keep those sets in sync, and e.g. would call Update(author) for updating an existing author.

Collections allowing to execute a set of operations in a single transaction are known as Unit-of-Work. We will introduce the pattern in more detail later.

Self-tracking entities are an extension to this definition where both the Unit of Work and the entities are enhanced to handle certain changes without requiring an interaction from the developer. The entities manage their status autonomous and they are observable. In case of a change they notify their observers. The observers - e.g. the Unit of Work - then have the possibility to trigger further actions, i.e. to update their entity sets accordingly. But self-tracking entities do not come without any cost as this logic brings additional complexity.

The following two unit tests depict the difference between change tracking and self-tracking entities (in this order).

[Test]
public void Updating_Repository_Sets_HasChanges_On_Repository_To_True()
{
  var author = repository.Find(a => a.Name == "John Doe");

  Assert.IsFalse(repository.HasChanges);

  author.Name = "John William Doe";

  Assert.IsFalse(repository.HasChanges);

  repository.Update(author);

  Assert.IsTrue(repository.HasChanges);
}
[Test]
public void Modifying_Entity_Sets_HasChanges_On_Repository_And_Entity_To_True()
{
  var author = repository.Find(a => a.Name == "John Doe");

  Assert.IsFalse(repository.HasChanges);
  Assert.IsFalse(author.HasChanges);

  author.Name = "John William Doe";

  Assert.IsTrue(repository.HasChanges);
  Assert.IsTrue(author.HasChanges);
}

Before modelling our component, we start consulting the most widely used patterns to see if we can borrow some proven concepts.

Repository Pattern

We need a component that provides data access in a uniform way, independent from the data source. Such a component is described by the Repository Pattern.

As per Martin Fowler’s definition of the Repository Pattern, [The repository] mediates between the domain and data mapping layers using a collection-like interface for accessing domain objects.. In other words, the repository provides means for accessing, updating, adding, and deleting entities through the same object.

The pattern does not provide an exact interface definition. But the following one should fit our needs.

public interface IRepository<TEntity>
{
  IEnumerable<TEntity> GetAll();
  bool ContainsItem(TEntity entity);
  void AddItem(TEntity entity);
  void RemoveItem(TEntity entity);
  void ClearItems();
  TEntity Find(Func<TEntity, bool> expression);
  IQueryable<TEntity> FindAll(Func<TEntity, bool> expression);
}

Unit of Work Pattern

Next we define a component that handles change tracking. The Unit-of-Work Pattern describes exactly such a component, including its interface.

public interface IUnitOfWork<TEntity>
{
  void Insert(TEntity entity);
  void Update(TEntity entity);
  void Delete(TEntity entity);
  void RejectChanges();
  void SaveChanges();
}

The Unit-of-Work maintains a list of objects affected by a business transaction and coordinates the writing out of changes and the resolution of concurrency problems.. One solution for implementing this definition is to keep an entity set for each kind of operation.

For our needs we slightly enhance the above interface with the following methods, properties, and events, to make it resemble the change tracking API provided by the good old Data Sets.

public interface IUnitOfWork<TEntity>
{
  void Insert(TEntity entity);
  void Update(TEntity entity);
  void Delete(TEntity entity);

  event EventHandler HasChangesChanged;
  bool HasChanges { get; }
  void AcceptChanges();
  void RejectChanges();
  IChangeTracker<TEntity> ChangeTracker { get; }

  event EventHandler<SaveCompletedEventArgs> SaveChangesCompleted;
  void SaveChangesAsync();
  void SaveChanges();
}

The Unit of Work Repository

As we are not going use a Repository without the functionality of a Unit of Work, we create a new interface IUnitOfWorkRepository that implements both the IUnitOfWork and IRepository interfaces. From now on both terms will refer to this component.

Have you noticed that with AddItem (part of IRepository) and Insert (part of IUnitOfWork) we have two methods that on the first sight provide a similar functionality? As AddItem is not part of the IUnitOfWork interface it will not affect any information related to change tracking, whereas Insert would change the HasChanges flag of the repository to TRUE. As you can see from the interface definition it will even raise a HasChangesChanged event, in case the repository was not yet dirty (the event will only be raised once).

In summary the AddItem method is used to fill the repository with entities from a data source. When you have to insert, update, or delete items afterwards then you will use the corresponding methods from the IUnitOfWork interface.

Unit Tests

In the spirit of Test Driven Development (TDD) we describe the unit tests before starting the implementation. The unit tests reflect the expected behavior in detail and assert a high level of quality, especially if combined with Continous Integration (CI).

[Test]
public void Initializing_A_Repository_Keeps_HasChanges_False()
{
  FakeEntity e1 = new FakeEntity("Hans", "Hans Müller", 1937);
  FakeEntity e2 = new FakeEntity("Toni", "Toni Müller", 1947);
  FakeEntity e3 = new FakeEntity("Markus", "Markus Müller", 1967);
  FakeEntity e4 = new FakeEntity("Sepp", "Sepp Müller", 1977);

  Repository.AddItem(e1);
  Repository.AddItem(e2);
  Repository.AddItem(e3);
  Repository.AddItem(e4);

  bool hasChanges = Repository.HasChanges;

  Assert.IsFalse(hasChanges);
}

[Test]
[ExpectedException(typeof(InvalidOperationException))]
public void Adding_Same_Entity_Twice_Throws_InvalidOperationException()
{
  FakeEntity e1 = new FakeEntity("Hans", "Hans Müller", 1937);

  Repository.AddItem(e1);
  Repository.AddItem(e1);
}

[Test]
[ExpectedException(typeof(InvalidOperationException))]
public void Updating_An_Not_Yet_Added_Entity_Throws_InvalidOperationException()
{
  FakeEntity e1 = new FakeEntity("Hans", "Hans Müller", 1937);

  Repository.Update(e1);
}

[Test]
[ExpectedException(typeof(InvalidOperationException))]
public void Deleting_An_Not_Yet_Added_Entity_Throws_InvalidOperationException()
{
  FakeEntity e1 = new FakeEntity("Hans", "Hans Müller", 1937);

  Repository.Delete(e1);
}

[Test]
public void Updating_Inserted_Items_Keeps_Them_In_Inserted_Collection()
{
  FakeEntity e1 = new FakeEntity("Hans", "Hans Müller", 1937);
  FakeEntity e2 = new FakeEntity("Toni", "Toni Müller", 1947);
  FakeEntity e3 = new FakeEntity("Markus", "Markus Müller", 1967);
  FakeEntity e4 = new FakeEntity("Sepp", "Sepp Müller", 1977);

  Repository.Insert(e1);
  Repository.Insert(e2);
  Repository.Insert(e3);
  Repository.Insert(e4);

  e1.Name = "Hansli";
  Repository.Update(e1);

  Assert.IsTrue(Repository.ChangeTracker.Inserted.Contains(e1));
  Assert.IsTrue(Repository.ChangeTracker.Inserted.Contains(e2));
  Assert.IsTrue(Repository.ChangeTracker.Inserted.Contains(e3));
  Assert.IsTrue(Repository.ChangeTracker.Inserted.Contains(e4));
  Assert.IsTrue(Repository.ChangeTracker.Inserted.Count() == 4);

  Assert.IsFalse(Repository.ChangeTracker.Changed.Contains(e1));
  Assert.IsTrue(Repository.ChangeTracker.Changed.Count() == 0);
}

[Test]
public void Updating_Deleted_Items_Moves_Them_To_Changed_Collection()
{
  FakeEntity e1 = new FakeEntity("Hans", "Hans Müller", 1937);
  FakeEntity e2 = new FakeEntity("Toni", "Toni Müller", 1947);
  FakeEntity e3 = new FakeEntity("Markus", "Markus Müller", 1967);
  FakeEntity e4 = new FakeEntity("Sepp", "Sepp Müller", 1977);

  Repository.AddItem(e1);
  Repository.AddItem(e2);
  Repository.AddItem(e3);
  Repository.AddItem(e4);

  Repository.Delete(e1);

  Assert.IsTrue(Repository.ChangeTracker.Deleted.Contains(e1));

  Repository.AddItem(e1);

  e1.Name = "Hansli";
  Repository.Update(e1);

  Assert.IsTrue(Repository.ChangeTracker.Changed.Contains(e1));
  Assert.IsTrue(Repository.ChangeTracker.Changed.Count() == 1);
  Assert.IsFalse(Repository.ChangeTracker.Deleted.Contains(e1));
  Assert.IsTrue(Repository.ChangeTracker.Deleted.Count() == 0);
}

[Test]
public void Updating_Item_Sets_HasChanges_To_True_And_Marks_Item_As_Changed()
{
  FakeEntity e1 = new FakeEntity("Hans", "Hans Müller", 1937);
  FakeEntity e2 = new FakeEntity("Toni", "Toni Müller", 1947);
  FakeEntity e3 = new FakeEntity("Markus", "Markus Müller", 1967);
  FakeEntity e4 = new FakeEntity("Sepp", "Sepp Müller", 1977);

  Repository.AddItem(e1);
  Repository.AddItem(e2);
  Repository.AddItem(e3);
  Repository.AddItem(e4);

  bool eventHandlerCalled = false;
  EventHandler handler = (sender, e) =>
  {
    eventHandlerCalled = true;
  };
  Repository.HasChangesChanged += handler;

  e1.Name = "Hansli";
  Repository.Update(e1);

  Repository.HasChangesChanged -= handler;

  bool hasChanges = Repository.HasChanges;

  Assert.IsTrue(eventHandlerCalled);

  Assert.IsTrue(hasChanges);
  Assert.IsTrue(Repository.ChangeTracker.Changed.Contains(e1));
  Assert.IsTrue(Repository.ChangeTracker.Changed.Count() == 1);
}

[Test]
public void Deleting_Item_Sets_HasChanges_To_True_And_Marks_Item_As_Deleted()
{
  FakeEntity e1 = new FakeEntity("Hans", "Hans Müller", 1937);
  FakeEntity e2 = new FakeEntity("Toni", "Toni Müller", 1947);
  FakeEntity e3 = new FakeEntity("Markus", "Markus Müller", 1967);
  FakeEntity e4 = new FakeEntity("Sepp", "Sepp Müller", 1977);

  Repository.AddItem(e1);
  Repository.AddItem(e2);
  Repository.AddItem(e3);
  Repository.AddItem(e4);

  bool eventHandlerCalled = false;
  EventHandler handler = (sender, e) =>
  {
    eventHandlerCalled = true;
  };
  Repository.HasChangesChanged += handler;

  Repository.Delete(e1);

  Repository.HasChangesChanged -= handler;

  bool hasChanges = Repository.HasChanges;

  Assert.IsTrue(eventHandlerCalled);

  Assert.IsTrue(hasChanges);
  Assert.IsTrue(Repository.ChangeTracker.Deleted.Contains(e1));
  Assert.IsTrue(Repository.ChangeTracker.Deleted.Count() == 1);
}

[Test]
public void Rejecting_Changes_On_A_Changed_Repository_Sets_HasChanges_To_False_And_Clears_ChangeTracker()
{
  FakeEntity e1 = new FakeEntity("Hans", "Hans Müller", 1937);
  FakeEntity e2 = new FakeEntity("Toni", "Toni Müller", 1947);
  FakeEntity e3 = new FakeEntity("Markus", "Markus Müller", 1967);

  Repository.AddItem(e1);
  Repository.AddItem(e2);
  Repository.AddItem(e3);

  e1.Name = "Hansli";
  Repository.Update(e1);

  e2.Name = "Tönchen";
  Repository.Update(e2);
  Repository.Delete(e2);

  Repository.Delete(e3);

  FakeEntity e4 = new FakeEntity("Sepp", "Sepp Müller", 1977);
  Repository.Insert(e4);

  Repository.RejectChanges();

  bool hasChanges = Repository.HasChanges;

  Assert.IsFalse(hasChanges);

  Assert.IsTrue(Repository.GetAll().Count() == 3);

  Assert.IsTrue(Repository.ContainsItem(e1));
  Assert.IsTrue(Repository.ContainsItem(e2));
  Assert.IsTrue(Repository.ContainsItem(e3));

  Assert.IsFalse(Repository.ContainsItem(e4));

  Assert.IsTrue(Repository.ChangeTracker.Inserted.Count() == 0);
  Assert.IsTrue(Repository.ChangeTracker.Changed.Count() == 0);
  Assert.IsTrue(Repository.ChangeTracker.Deleted.Count() == 0);
}

[Test]
public void Accepting_Changes_On_A_Changed_Repository_Sets_HasChanges_To_False_And_Clears_ChangeTracker()
{
  FakeEntity e1 = new FakeEntity("Hans", "Hans Müller", 1937);
  FakeEntity e2 = new FakeEntity("Toni", "Toni Müller", 1947);
  FakeEntity e3 = new FakeEntity("Markus", "Markus Müller", 1967);
  
  Repository.AddItem(e1);
  Repository.AddItem(e2);
  Repository.AddItem(e3);

  e1.Name = "Hansli";
  Repository.Update(e1);

  e2.Name = "Tönchen";
  Repository.Update(e2);
  Repository.Delete(e2);
  e2.Name = "Test";

  Repository.Delete(e3);

  FakeEntity e4 = new FakeEntity("Sepp", "Sepp Müller", 1977);
  Repository.Insert(e4);

  Repository.AcceptChanges();

  bool hasChanges = Repository.HasChanges;

  Assert.IsFalse(hasChanges);

  Assert.IsTrue(Repository.GetAll().Count() == 2);

  Assert.IsFalse(Repository.ContainsItem(e2));
  Assert.IsFalse(Repository.ContainsItem(e3));

  Assert.IsTrue(Repository.ContainsItem(e1));
  Assert.IsTrue(Repository.ContainsItem(e4));

  Assert.IsTrue(Repository.ChangeTracker.Inserted.Count() == 0);
  Assert.IsTrue(Repository.ChangeTracker.Changed.Count() == 0);
  Assert.IsTrue(Repository.ChangeTracker.Deleted.Count() == 0);
}

[Test]
public void Removing_A_Changed_Item_Sets_HasChanges_To_False()
{
  FakeEntity e1 = new FakeEntity("Hans", "Hans Müller", 1937);
  FakeEntity e2 = new FakeEntity("Toni", "Toni Müller", 1947);
  FakeEntity e3 = new FakeEntity("Markus", "Markus Müller", 1967);
  FakeEntity e4 = new FakeEntity("Sepp", "Sepp Müller", 1977);

  Repository.AddItem(e1);
  Repository.AddItem(e2);
  Repository.AddItem(e3);
  Repository.AddItem(e4);

  e1.Name = "Hansli";
  Repository.Update(e1);

  Repository.RemoveItem(e1);

  bool hasChanges = Repository.HasChanges;

  Assert.IsFalse(hasChanges);
  Assert.IsFalse(Repository.ContainsItem(e1));
  Assert.IsTrue(Repository.ChangeTracker.Changed.Count() == 0);
}

Implementation

Now we are going to implement the abstract base repository class RepositoryBase. It actually only keeps a collection of the available items and delegates the work related to change tracking - according to the Single Responsibility Principle - to a component called IChangeTracker<TEntity>, which is described later.

public abstract class RepositoryBase<TEntity> : IUnitOfWorkRepository<TEntity>
{
  private readonly ICollection<TEntity> _items = new HashSet<TEntity>();
  protected ICollection<TEntity> InternalItems
  {
    get
    {
      return _items;
    }
  }
  
  public void AddItem(TEntity entity)
  {
    lock (_syncRoot)
    {
      if (!InternalItems.Contains(entity))
      {
        AddItemInternal(entity);
      }
      else
      {
        throw new InvalidOperationException("Entity has already been added to repository");
      }
    }
  }
  private void AddItemInternal(TEntity entity)
  {
    InternalItems.Add(entity);
    OnItemAdded(entity);
  }
  protected virtual void OnItemAdded(TEntity entity)
  { }

  public void RemoveItem(TEntity entity)
  {
    lock (_syncRoot)
    {
      if (InternalItems.Contains(entity) || ChangeTracker.ContainsDeleted(entity))
      {
        RemoveItemInternal(entity);
        ChangeTracker.RemoveItem(entity);
      }
    }
  }
  private void RemoveItemInternal(TEntity entity)
  {
    InternalItems.Remove(entity);
    OnItemRemoved(entity);
  }
  protected virtual void OnItemRemoved(TEntity entity)
  { }
  
  public void ClearItems()
  {
    lock (_syncRoot)
    {
      IList<TEntity> itemsCopy = new List<TEntity>(InternalItems.Union(ChangeTracker.Deleted));
      foreach (TEntity item in itemsCopy)
      {
        RemoveItem(item);
      }
    }
  }

  public TEntity Find(Func<TEntity, bool> expression)
  {
    return InternalItems.Where(expression).SingleOrDefault();
  }

  public IQueryable<TEntity> FindAll(Func<TEntity, bool> expression)
  {
    return InternalItems.Where(expression).AsQueryable();
  }
  
  // ...
}

Please note the virtual methods OnItemAdded and OnItemRemoved. Those methods allow child classes to hook into the workflow. Generally this is a nice approach to design base classes but we will also need those two methods later for our implementation of self-tracking entities.

The methods defined in IUnitOfWork are implemented as described in the following code listing. Whenever one of those methods is called, we check for any occurrence in any other entity set and perform some cleanup. E.g. if we try to call update on an previously deleted entity, we are allowed to remove that entity from the deleted set as well. We also have to make sure that calling update will not add the entity to the updated set in case an item has been inserted, as we first have to insert it anyhow. And so on, I think all of the possible cases are self-explanatory and documented in the below code listing.

public virtual void Insert(TEntity entity)
{
  lock (_syncRoot)
  {
    if (!InternalItems.Contains(entity))
    {
      ChangeTracker.Insert(entity);
      AddItemInternal(entity);
    }
    else
    {
      throw new InvalidOperationException("Entity has not yet been added to repository");
    }
  }
}

public virtual void Update(TEntity entity)
{
  lock (_syncRoot)
  {
    if (InternalItems.Contains(entity))
    {
      if (ChangeTracker.ContainsDeleted(entity))
      {
        ChangeTracker.RemoveDeleted(entity);
      }

      if (!ChangeTracker.ContainsInserted(entity) && !ChangeTracker.ContainsChanged(entity))
      {
        ChangeTracker.Update(entity);
      }
    }
    else
    {
      throw new InvalidOperationException("Entity has not yet been added to repository");
    }
  }
}

public virtual void Delete(TEntity entity)
{
  lock (_syncRoot)
  {
    if (InternalItems.Contains(entity))
    {
      bool doDelete = true;

      if (ChangeTracker.ContainsInserted(entity))
      {
        ChangeTracker.RemoveInserted(entity);
        doDelete = false;
      }

      if (ChangeTracker.ContainsChanged(entity))
      {
        ChangeTracker.RemoveChanged(entity);
      }

      if (doDelete)
      {
        ChangeTracker.Delete(entity);
      }

      RemoveItemInternal(entity);
    }
    else
    {
      throw new InvalidOperationException("Entity has not yet been added to repository");
    }
  }
}

The missing methods are implemented as follows:

public virtual void RejectChanges()
{
  foreach (TEntity entity in ChangeTracker.Deleted)
  {
    AddItem(entity);
  }

  foreach (TEntity entity in ChangeTracker.Inserted)
  {
    RemoveItem(entity);
  }

  ChangeTracker.Reset();
}

public virtual void AcceptChanges()
{
  ChangeTracker.Reset();
}

public event EventHandler HasChangesChanged;
protected void NotifyHasChangesChangedListeners()
{
  EventHandler handler = HasChangesChanged;
  if (handler != null)
  {
    handler(this, new EventArgs());
  }
}

public bool HasChanges
{
  get
  {
    return ChangeTracker.HasChanges;
  }
}

Change Tracker

The IChangeTracker actually stores the entity status information. It is important to keep the data immutable so that nobody else can manipulate the data without using the designated methods.

public interface IChangeTracker<TEntity>
{
  IEnumerable<TEntity> Inserted { get; }
  void Insert(TEntity entity);
  bool ContainsInserted(TEntity entity);
  void RemoveInserted(TEntity entity);
  
  IEnumerable<TEntity> Changed { get; }
  void Update(TEntity entity);
  bool ContainsChanged(TEntity entity);
  void RemoveChanged(TEntity entity);

  IEnumerable<TEntity> Deleted { get; }
  void Delete(TEntity entity);
  bool ContainsDeleted(TEntity entity);
  void RemoveDeleted(TEntity entity);

  event EventHandler HasChangesChanged;
  bool HasChanges { get; }

  void RemoveItem(TEntity item);
  void Reset();
}

As mentioned above the class has to be immutable and so we have to make sure instead of returning references to our collections we return copies of them. We also use Hash Sets as entity collections for performane reasons as we expect to perform many read operations with the corresponding Contains methods.

public sealed class RepositoryStateManager<TEntity> : IChangeTracker<TEntity>
{
  private readonly object _syncRoot = new object();

  private void OnStateChanged()
  {
    lock (_syncRoot)
    {
      HasChanges = _deleted.Count > 0 || _changed.Count > 0 || _inserted.Count > 0;
    }
  }

  public event EventHandler HasChangesChanged;
  private void NotifyHasChangesChangedListeners()
  {
    EventHandler handler = HasChangesChanged;
    if (handler != null)
    {
      handler(this, new EventArgs());
    }
  }

  private bool _hasChanges;
  public bool HasChanges 
  { 
    get
    {
      return _hasChanges;
    }
    private set
    {
      if (_hasChanges != value)
      {
        _hasChanges = value;
        NotifyHasChangesChangedListeners();
      }
    }
  }

  public void RemoveItem(TEntity item)
  {
    lock (_syncRoot)
    {
      _inserted.Remove(item);
      _changed.Remove(item);
      _deleted.Remove(item);

      OnStateChanged();
    }
  }

  private readonly ICollection<TEntity> _deleted = new HashSet<TEntity>();
  public IEnumerable<TEntity> Deleted
  {
    get
    {
      lock (_syncRoot)
      {
        return new List<TEntity>(_deleted);
      }
    }
  }
  public bool ContainsDeleted(TEntity entity)
  {
    return _deleted.Contains(entity);
  }
  public void RemoveDeleted(TEntity entity)
  {
    lock (_syncRoot)
    {
      _deleted.Remove(entity);
      OnStateChanged();
    }
  }
  public void Delete(TEntity entity)
  {
    lock (_syncRoot)
    {
      _deleted.Add(entity);
      OnStateChanged();
    }
  }

  private readonly ICollection<TEntity> _changed = new HashSet<TEntity>();
  public IEnumerable<TEntity> Changed
  {
    get
    {
      lock (_syncRoot)
      {
        return new List<TEntity>(_changed);
      }
    }
  }
  public bool ContainsChanged(TEntity entity)
  {
      return _changed.Contains(entity);
  }
  public void RemoveChanged(TEntity entity)
  {
    lock (_syncRoot)
    {
      _changed.Remove(entity);
      OnStateChanged();
    }
  }
  public void Update(TEntity entity)
  {
    lock (_syncRoot)
    {
      _changed.Add(entity);
      OnStateChanged();
    }
  }

  private readonly ICollection<TEntity> _inserted = new HashSet<TEntity>();
  public IEnumerable<TEntity> Inserted
  {
    get
    {
      lock (_syncRoot)
      {
        return new List<TEntity>(_inserted);
      }
    }
  }
  public bool ContainsInserted(TEntity entity)
  {
    return _inserted.Contains(entity);
  }
  public void RemoveInserted(TEntity entity)
  {
    lock (_syncRoot)
    {
      _inserted.Remove(entity);
      OnStateChanged();
    }
  }
  public void Insert(TEntity entity)
  {
    lock (_syncRoot)
    {
      _inserted.Add(entity);
      OnStateChanged();
    }
  }

  public void Reset()
  {
    lock (_syncRoot)
    {
      _changed.Clear();
      _inserted.Clear();
      _deleted.Clear();

      HasChanges = false;
    }
  }
}

Immutable Collections

We have mentioned earlier that the collections returned by a IChangeTracker<TEntity> or a IRepository<TEntity> implementation should be immutable to prevent unwanted manipulations by clients. As a caller may get access to the source collection by casting the returned IEnumerable it is not enough to simply change the method signatures from a ICollection extension to IEnumerable.

The only solution appears to be to return a copy of the underlying collection. Of course this means a huge overhead as we have to iterate over all items each time a copy is created. Furthermore a new object is instantiated for each request which increases memory consumption and may cause pressure on the garbage collector.

Fortunately since December 2012 Microsoft delivers the Immutable Collections library as nuget package for the .NET framework 4.5 which guarantees that the collections may not be changed at all but can be efficiently mutated by allocating a new collection that shares much of the same memory with the original. As this approach seems to improve the overall performance we will rewrite the corresponding parts to use the new collections.

The method signatures should return IReadOnlyCollection instead of IEnumerable to make it transparent that we are going to return a non-writable collection.

public interface IRepository<TEntity>
{
  IReadOnlyCollection<TEntity> GetAll();
  // ...
}

public interface IChangeTracker<TEntity>
{
  IReadOnlyCollection<TEntity> Inserted { get; }
  IReadOnlyCollection<TEntity> Changed { get; }
  IReadOnlyCollection<TEntity> Deleted { get; }
  // ...
}

As invoking Add or Remove will not affect the collection but rather return a new instance, the implementation changes as follows:

public abstract class RepositoryBase<TEntity> : IUnitOfWorkRepository<TEntity>
{
  private IImmutableSet<TEntity> _items = ImmutableHashSet<TEntity>.Empty;
  public IReadOnlyCollection<TEntity> GetAll()
  {
    return _items;
  }
  private void AddItemInternal(TEntity entity)
  {
    _items = _items.Add(entity);

    OnItemAdded(entity);
  }
  private void RemoveItemInternal(TEntity entity)
  {
    _items = _items.Remove(entity);

    OnItemRemoved(entity);
  }
  
  // ...
}

public sealed class RepositoryStateManager<TEntity> : IChangeTracker<TEntity>
{
  private IImmutableSet<TEntity> _changed = ImmutableHashSet<TEntity>.Empty;
  public IReadOnlyCollection<TEntity> Changed
  {
    get
    {
      return _changed;
    }
  }
  public void RemoveChanged(TEntity entity)
  {
    lock (_syncRoot)
    {
      _changed = _changed.Remove(entity);

      OnStateChanged();
    }
  }
  public void Update(TEntity entity)
  {
    lock (_syncRoot)
    {
      _changed = _changed.Add(entity);

      OnStateChanged();
    }
  }
  // ... 
}

Review and Outlook

At this point we have a working implementation - also as download - of the Unit of Work pattern which requires to manually keep the entity sets in sync. Please run the unit tests again and make sure all tests succeed.

We now have assembled the fundament for implementing self-tracking entities. On one side it will reduce the developer’s work to interact with the entities, on the other side it adds further complexity we have to handle. But especially for developing client-side applications with the MVVM pattern in WPF you will love it. Therefore let’s dive into the next post, Part 2 - Entities!

References

  1. Unit-of-Work Pattern
  2. Observable Pattern
  3. Repository Pattern
  4. Test Driven Development (TDD)
  5. Continous Integration (CI)
  6. Single Responsibility Principle
  7. Immutable
  8. Hash Set
  9. Immutable Collection
  10. Immutable Collection Nuget Package