Problem
I have a list of POCO model objects (approximatly 1.000 to 10.000) and I want to track changes on them:
- Check if at least one of the items has changed (to show the user that something changed)
- Get all new, deleted and modified objects to update the state in the database
For the first version, it is enough to support POCOs with primitive property types. However, probably I'll extend it in future to support complex property types.
Solution
public class ChangeTracker<TEntity> where TEntity : class
{
private static readonly PropertyInfo[] thePropertyInfos;
private readonly List<TEntity> myEntities = new List<TEntity>();
private readonly List<TEntity> myDeletedEntites = new List<TEntity>();
private readonly HashSet<TEntity> myNewEntities = new HashSet<TEntity>();
private readonly Dictionary<TEntity, object[]> myStates = new Dictionary<TEntity, object[]>();
static ChangeTracker()
{
thePropertyInfos = typeof(TEntity).GetProperties().ToArray();
}
public void Add(TEntity entity)
{
if (myStates.ContainsKey(entity))
throw new InvalidOperationException("It is not possible to add an entity twice.");
myStates.Add(entity, new object[thePropertyInfos.Length]);
myNewEntities.Add(entity);
myEntities.Add(entity);
}
public void Delete(TEntity entity)
{
myEntities.Remove(entity);
if (!myNewEntities.Remove(entity))
myDeletedEntites.Add(entity);
}
public void AcceptChanges()
{
myNewEntities.Clear();
myDeletedEntites.Clear();
foreach (var entity in myEntities)
myStates[entity] = GetState(entity);
}
public bool HasChanges()
{
return GetEntities(EntityStates.Deleted
| EntityStates.Modified
| EntityStates.New)
.Any();
}
private static object[] GetState(TEntity entity)
{
return thePropertyInfos
.Select(pi => pi.GetValue(entity))
.ToArray();
}
private static void SetState(TEntity entity, object[] state)
{
for (int i = 0; i < state.Length; i++)
thePropertyInfos[i].SetValue(entity, state[i]);
}
private bool HasChanges(TEntity entity)
{
var currentState = GetState(entity);
var previousState = myStates[entity];
for (int i = 0; i < currentState.Length; i++)
{
if (currentState[i] == null &&
previousState[i] == null)
continue;
if (currentState[i] == null ||
!currentState[i].Equals(previousState[i]))
return true;
}
return false;
}
public IEnumerable<TEntity> GetEntities(EntityStates states)
{
if (states.HasFlag(EntityStates.New))
foreach (var entity in myNewEntities)
yield return entity;
if (states.HasFlag(EntityStates.Deleted))
foreach (var entity in myDeletedEntites)
yield return entity;
if (!states.HasFlag(EntityStates.Modified) &&
!states.HasFlag(EntityStates.Unmodified))
yield break;
foreach (var entity in myEntities.Where(e => !myNewEntities.Contains(e)))
{
var isModified = HasChanges(entity);
if (states.HasFlag(EntityStates.Modified) && isModified)
yield return entity;
else if (states.HasFlag(EntityStates.Unmodified) && !isModified)
yield return entity;
}
}
}
[Flags]
public enum EntityStates
{
New = 1,
Deleted = 2,
Modified = 4,
Unmodified = 8,
All = ~0,
}
Usage:
public class TestItem
{
public string Property { get; set; }
}
var changeTracker = new ChangeTracker<TestItem>();
var item1 = new TestItem();
var item2 = new TestItem();
var item3 = new TestItem();
var item4 = new TestItem();
var item5 = new TestItem();
changeTracker.Add(item1);
changeTracker.Add(item2);
changeTracker.Add(item3);
changeTracker.AcceptChanges();
changeTracker.Add(item4);
changeTracker.Add(item5);
item2.Property = "test";
changeTracker.Delete(item3);
changeTracker.Delete(item5);
Assert.IsTrue(changeTracker.HasChanges());
Assert.AreEqual(4, changeTracker.GetEntities(EntityStates.All).Count());
Assert.AreEqual(item1, changeTracker.GetEntities(EntityStates.Unmodified).Single());
Assert.AreEqual(item2, changeTracker.GetEntities(EntityStates.Modified).Single());
Assert.AreEqual(item3, changeTracker.GetEntities(EntityStates.Deleted).Single());
Assert.AreEqual(item4, changeTracker.GetEntities(EntityStates.New).Single());
Any feedback is welcome. However, I am specially intrested in alternative more performant solutions (without the overhead of boxing/unboxing and reflection).
[Edit]
I also tried binary serialization as "state storing strategy", but it is much slower (~100-200%) and it has the downside that the entity classes have to be marked as serializable.
thePropertyInfos
- a new prefix? ;-D \$\endgroup\$EntityStates
aFlags
enum? It doesn't make much sense that an entity can have more then one state at the same time. \$\endgroup\$changeTracker.GetEntities(EntityStates.Modified | EntityStates.New | EntityStates.Deleted)
\$\endgroup\$