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 article describes - in five posts - how to easily implement self-tracking entities that fit well into an ModelViewViewModel (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:
- Fundamentals
- Entities
- Validation
- Composite Entity
- MVVM
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 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);
}
[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 Continuous Integration (CI).
Details
[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 performance 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!