Recently, I wanted to see how I might could track state changes to objects, and manage relationships (1 to 1, 1 to N, N to N) between types in C#. This was a really interesting project, and I'm wondering how it might be improved.
Full code
The full code can be found here, which includes a test project.
Basics: TrackableEntity
, EntityManager
and TrackableProperty
Everything in my EntityTracker
project works with TrackableEntity
, which does what you might think.
/// <summary>Base type that supports change tracking </summary>
public abstract class TrackableEntity
{
public int Id
{
get;
internal set;
}
public bool IsDirty
{
get;
internal set;
}
public void Commit()
{
IsDirty = false;
}
public TrackableEntity()
{
EntityManager.Instance.Create(this);
}
public void Delete()
{
EntityManager.Instance.Delete(this);
}
}
All the TrackableEntity
objects are stored in a globally accessible repository, EntityManager
. I'll save discussion of the Delete
method referenced by TrackableEntity
for later.
/// <summary>
/// Container class to hold TrackableEntity objects
/// </summary>
public class EntityManager
{
private static EntityManager instance = new EntityManager();
private int next;
private Dictionary<int, TrackableEntity> entities;
private EntityManager()
{
entities = new Dictionary<int, TrackableEntity>();
}
public static EntityManager Instance
{
get { return instance; }
}
/// <summary>Adds the TrackableEntity in the container</summary>
public void Create(TrackableEntity entity)
{
entity.Id = next++;
entities[entity.Id] = entity;
}
/// <summary>Gets the TrackableEntity stored at id</summary>
public TrackableEntity Lookup(int id)
{
return entities.ContainsKey(id) ? entities[id] : null;
}
}
To actually track changes to properties in TrackableEntity
, I created an object to wrap each property which allows me to manage the state of the IsDirty
flag. This object actually holds all values for a type that declares a TrackableProperty
and looks up the owner's value by id.
/// <summary>Wrapper property to track changes</summary>
public class TrackableProperty<T> where T : IEquatable<T>
{
Dictionary<int, T> values = new Dictionary<int, T>();
/// <summary>Gets the owner's value</summary>
public T GetValue(TrackableEntity owner)
{
return values.ContainsKey(owner.Id) ?
values[owner.Id] : default(T);
}
/// <summary>Sets the owner's value</summary>
public void SetValue(TrackableEntity owner, T value)
{
if (values.ContainsKey(owner.Id) && values[owner.Id].Equals(value))
return;
owner.IsDirty = true; values[owner.Id] = value;
}
}
Here's a simple example of how TrackableEntity
and TrackableProperty
are used:
[DebuggerDisplay("Name = {Name}")]
abstract class NamedEntity : RelationshipEntity
{
public static readonly TrackableProperty<string> NameProperty
= new TrackableProperty<string>();
public string Name
{
get { return NameProperty.GetValue(this); }
set { NameProperty.SetValue(this, value); }
}
}
Relationships
The three relationships (1 to 1, 1 to N, and N to N) I support all implement the following generic interface.
/// <summary>Base type for relationships between RelationshipEntity objects</summary>
public interface IRelationship<T1, T2>
where T1 : RelationshipEntity
where T2 : RelationshipEntity
{
void CreateRelationship(T1 t1, T2 t2);
void DeleteRelationship(RelationshipEntity entity);
}
/// <summary>Base type for relationships between TrackableEntity objects </summary>
public abstract class RelationshipEntity : TrackableEntity
{
public abstract void Accept(IRelationshipVisitor visitor);
}
Implementation of Relationship1To1
The efficiency of navigating in the reverse direction (i.e. getting T1
from T2
) could be improved. For now, however, I'm just using a linear search to make sure the relationship is maintained.
/// <summary>Represents a 1 to 1 relationship</summary>
public class Relationship1To1<T1, T2> : IRelationship<T1, T2>
where T1 : RelationshipEntity
where T2 : RelationshipEntity
{
Dictionary<int, int> forward = new Dictionary<int, int>();
/// <summary>
/// Creates the relationship; t1 and t2
/// may only reference one another
/// </summary>
public void CreateRelationship(T1 t1, T2 t2)
{
DeleteRelationship(t1);
DeleteRelationship(t2);
forward[t1.Id] = t2.Id;
}
public void DeleteRelationship(RelationshipEntity entity)
{
var kvps = forward.Where(kvp =>
kvp.Key == entity.Id || kvp.Value == entity.Id);
foreach (var item in kvps.ToList())
forward.Remove(item.Key);
}
public T2 GetForward(T1 owner)
{
return forward.ContainsKey(owner.Id) ?
(T2)EntityManager.Instance.Lookup(forward[owner.Id]) : null;
}
public T1 GetReverse(T2 owner)
{
foreach (KeyValuePair<int, int> kvp in forward)
if (kvp.Value == owner.Id)
return (T1)EntityManager.Instance.Lookup(kvp.Key);
return null;
}
}
Implementation of Relationship1ToN
This seemed pretty straightforward. I use a dictionary to ensure that each child has only one parent.
/// <summary>Represents a 1 to N relationship</summary>
public class Relationship1ToN<T1, T2> : IRelationship<T1, T2>
where T1 : RelationshipEntity
where T2 : RelationshipEntity
{
Dictionary<int, T1> children = new Dictionary<int, T1>();
/// <summary>
/// Creates the relationship; t2 may only have one owner, t1
/// </summary>
public void CreateRelationship(T1 t1, T2 t2)
{
children[t2.Id] = t1;
}
public void DeleteRelationship(RelationshipEntity entity)
{
var kvps = children.Where(x => x.Key == entity.Id || x.Value.Id == entity.Id);
foreach (var item in kvps.ToList())
children.Remove(item.Key);
}
public IEnumerable<T2> GetFoward(T1 parent)
{
return children.Where(pair => pair.Value.Id == parent.Id)
.Select(pair => (T2)EntityManager.Instance.Lookup(pair.Key));
}
public T1 GetReverse(T2 owner)
{
return children.ContainsKey(owner.Id) ?
children[owner.Id] : null;
}
}
Implementation of RelationshipNToN
This seemed most straightforward. Anyone can own anyone:
/// <summary>Represents an N to N relationship</summary>
public class RelationshipNToN<T1, T2> : IRelationship<T1, T2>
where T1 : RelationshipEntity
where T2 : RelationshipEntity
{
Dictionary<int, HashSet<int>> forward = new Dictionary<int, HashSet<int>>();
Dictionary<int, HashSet<int>> reverse = new Dictionary<int, HashSet<int>>();
/// <summary>Creates the relationship</summary>
public void CreateRelationship(T1 t1, T2 t2)
{
if (!forward.ContainsKey(t1.Id)) forward[t1.Id] = new HashSet<int>();
if (!reverse.ContainsKey(t2.Id)) reverse[t2.Id] = new HashSet<int>();
forward[t1.Id].Add(t2.Id);
reverse[t2.Id].Add(t1.Id);
}
public void DeleteRelationship(RelationshipEntity entity)
{
var kvps = forward.Where(x => x.Key == entity.Id || x.Value.Contains(entity.Id))
.Union(reverse.Where(x => x.Key == entity.Id || x.Value.Contains(entity.Id)));
foreach (var kvp in kvps.ToList())
{
reverse.Remove(kvp.Key);
forward.Remove(kvp.Key);
}
}
public IEnumerable<T2> GetForward(T1 owner)
{
return forward.ContainsKey(owner.Id) ?
from key in forward[owner.Id]
select (T2)EntityManager.Instance.Lookup(key) : Enumerable.Empty<T2>();
}
public IEnumerable<T1> GetReverse(T2 owner)
{
return reverse.ContainsKey(owner.Id) ?
from key in reverse[owner.Id]
select (T1)EntityManager.Instance.Lookup(key) : Enumerable.Empty<T1>();
}
}
Deleting relationships
If we delete an TrackableEntity
, we need to remove all references to it. To me, it seemed that the best way to visit all relationships in the object graph was with the visitor pattern. At each relationship, we call implementation of DeleteRelationship
for the entity to be deleted.
RelationshipEntity
must be able to accept an IRelationshipVisitor
. Since we have three implementations of IRelationship
, we should provide three ways to visit.
/// <summary>Base type for visiting relationships in the object graph</summary>
public interface IRelationshipVisitor
{
void Visit<T1, T2>(Relationship1To1<T1, T2> relationship)
where T1 : RelationshipEntity
where T2 : RelationshipEntity;
void Visit<T1, T2>(Relationship1ToN<T1, T2> relationship)
where T1 : RelationshipEntity
where T2 : RelationshipEntity;
void Visit<T1, T2>(RelationshipNToN<T1, T2> relationship)
where T1 : RelationshipEntity
where T2 : RelationshipEntity;
}
The visitor for deletion is RelationshipBreaker
(omitted here for brevity). To delete in the EntityManager
, all we have to do is visit the relationships and then delete the entity.
/// <summary>
/// Deletes the TrackableEntity from the container
/// and removes all references from relationships
/// </summary>
public void Delete(TrackableEntity entity)
{
if (entity is RelationshipEntity)
{
var casted = (RelationshipEntity)entity;
casted.Accept(new RelationshipBreaker(casted));
}
entities.Remove(entity.Id);
}
Example code
Here are some examples of entities with relationships
class A : NamedEntity
{
public static readonly Relationship1To1<A, B> relationship
= new Relationship1To1<A, B>();
public B B
{
get { return relationship.GetForward(this); }
set { relationship.CreateRelationship(this, value); }
}
public C Parent
{
get { return C.relationship.GetReverse(this); }
}
public override void Accept(IRelationshipVisitor visitor)
{
visitor.Visit<A, B>(A.relationship);
visitor.Visit<C, A>(C.relationship);
}
}
class B : NamedEntity
{
public A A
{
get { return A.relationship.GetReverse(this); }
set { A.relationship.CreateRelationship(value, this); }
}
public override void Accept(IRelationshipVisitor visitor)
{
visitor.Visit<A, B>(A.relationship);
}
}
class C : NamedEntity
{
public static readonly Relationship1ToN<C, A> relationship
= new Relationship1ToN<C, A>();
public IEnumerable<A> Children
{
get { return relationship.GetFoward(this); }
}
// how to avoid this -- would like for the relationship to return
// a custom collection (this has add/remove etc) that enforces the relationship
public void Add(A item)
{
relationship.CreateRelationship(this, item);
}
public override void Accept(IRelationshipVisitor visitor)
{
visitor.Visit<C, A>(relationship);
}
}
Questions and points to review
- In the implementation of
Delete
, I cast to a relationship type. I had tried providing two implementations ofDelete
; one for theTrackableEntity
(no relationships) and another forRelationshipEntity
. This didn't work, however (the delete call for a test entity kept going through theTrackableEntity
overload and not to theRelationshipEntity
overload). Why is this? - I expose the children of a
Relationship1ToN
as anIEnumerable<T2>
. As a result, I have to provide additional methods to add an item to this relationship that are exposed in the entity classes. Is there any way to abstract this? Maybe instead of returning anIEnumerable<T2>
, would it be possible to return anICollection<T2>
, and provide checks to maintain the relationship? - General design -- how did I do?