HiveBrain v1.2.0
Get Started
← Back to all entries
patterncsharpMinor

Framework to track changes and relationships in C#

Submitted by: @import:stackexchange-codereview··
0
Viewed 0 times
trackchangesandrelationshipsframework

Problem

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.

/// Base type that supports change tracking 
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.

/// 
/// Container class to hold TrackableEntity objects
/// 
public class EntityManager
{
    private static EntityManager instance = new EntityManager();
    private int next;
    private Dictionary entities;

    private EntityManager()
    {
        entities = new Dictionary();
    }

    public static EntityManager Instance
    {
        get { return instance; }
    }

    /// Adds the TrackableEntity in the container
    public void Create(TrackableEntity entity)
    {
        entity.Id = next++;
        entities[entity.Id] = entity;
    }

    /// Gets the TrackableEntity stored at id
    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 s

Solution

General

If you need to get a value of a Dictionary you shouldn't us ContainsKey() together with the Item property getter but TryGetValue(), because by using ContainsKey() in combination with the Item getter you are doing the check if the key exists twice.

See also: what-is-more-efficient-dictionary-trygetvalue-or-containskeyitem

TrackableEntity

This abstract class seems almost fine to me. The only missing is the lack of documentation. A framework should always have enough documentation targeting the the parts which are accesible from outside. So each public and protected property/method should be documentated.

While we talk about scope, an abstract class only serves one purpose, it is intended to be implemented. By making a setter of a property internal one who wants to implement such a class can't set these properties (if it is allowed to set them).

If I would implement the abstract TrackableEntity class, I would maybe want to set the IsDirty property to a specific value, but because its internal I just can't. So making properties which you feel should be set from outside protected would be a good decision.

EntityManager singleton

Hmmm, a singletone is coming along. How am I be able to write unit tests for a singleton or for classes which uses this singleton? Hmmm, I just can't easily do this, because I can't reset the state of this singleton between tests which is bad.

For instance assume we want to test the creation of a TrackableEntity object and because it comes handy we assert that TrackableEntity.Id == 1 so, no problem there, the test passes.

Next we write another test which test for TrackableEntity.Delete() and we assert with EntityManager.Instance.Lookup(TrackableEntity.Id) == null).

Then you run both tests and they passed and you ask yourself hey what is this guy talking about but then you change the order of the tests and hence the first written test fails because after the test for Delete the private int next of the EntityManager is 1. So if the creation test is executed the assert will fail.

What are possible ways out of this ? You can let EntityManager implement an interface IEntityManager having Create(), Lookup()andDelete()methods and use a private field inTrackableEntitywhich is holding a reference ofIEntityManager which is constructor injected.

The changes you would need to do are simple, some thing along these lines

public interface IEntityManager
{
    TrackableEntity Lookup(int id);
    void Create(TrackableEntity entity);

    void Delete(TrackableEntity entity);
}




public class EntityManager : IEntityManager
{
    private static IEntityManager instance = new EntityManager();
    private int next;
    private Dictionary entities;

    private EntityManager()
    {
        entities = new Dictionary();
    }

    public static IEntityManager Instance
    {
        get { return instance; }
    }

    /// Adds the TrackableEntity in the container
    public void Create(TrackableEntity entity)
    {
        entity.Id = next++;
        entities[entity.Id] = entity;
    }

    /// Gets the TrackableEntity stored at id
    public TrackableEntity Lookup(int id)
    {
        TrackableEntity entity;
        entities.TryGetValue(id, out entity);
        
        return entity;
    }

    public void Delete(TrackableEntity entity)
    {
        // we will target this later
        throw new NotImplementedException();
    }
}




public abstract class TrackableEntity
{
    private readonly IEntityManager manager;
    public TrackableEntity()
    {
        manager = EntityManager.Instance;
        manager.Create(this);
    }
    public TrackableEntity(IEntityManager manager)
    {
        this.manager = manager;
        manager.Create(this);
    }
    public int Id
    {
        get;
        internal set;
    }

    public bool IsDirty
    {
        get;
        internal set;
    }

    public void Commit()
    {
        IsDirty = false;
    }

    public void Delete()
    {
        manager.Delete(this);
    }
}


Now you use a mocked
IEntityManager obejct for unit tests.

Another maybe easier aproach would be like described in the answer to unit-testing-singletons

Short version: do not write your singletons as singletons. Write them as normal classes, and call them via an Inversion of Control container, where you have configured the class to be a singleton instead.

TrackableProperty

Using multiple code statements in the same line is hard to read and to debug and should be avoided.

Access to a dictionary should be changed like written above like so

``
/// Wrapper property to track changes
public class TrackableProperty where T : IEquatable
{
Dictionary values = new Dictionary();

/// Gets the owner's value
public T GetValue(TrackableEntity owner)
{
T foundValue;
values.TryGetValue(owner.Id, out foundValue);
return foundValue;
}

/// Sets the owner's

Code Snippets

public interface IEntityManager
{
    TrackableEntity Lookup(int id);
    void Create(TrackableEntity entity);

    void Delete(TrackableEntity entity);
}
public class EntityManager : IEntityManager
{
    private static IEntityManager instance = new EntityManager();
    private int next;
    private Dictionary<int, TrackableEntity> entities;

    private EntityManager()
    {
        entities = new Dictionary<int, TrackableEntity>();
    }

    public static IEntityManager 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)
    {
        TrackableEntity entity;
        entities.TryGetValue(id, out entity);
        
        return entity;
    }

    public void Delete(TrackableEntity entity)
    {
        // we will target this later
        throw new NotImplementedException();
    }
}
public abstract class TrackableEntity
{
    private readonly IEntityManager manager;
    public TrackableEntity()
    {
        manager = EntityManager.Instance;
        manager.Create(this);
    }
    public TrackableEntity(IEntityManager manager)
    {
        this.manager = manager;
        manager.Create(this);
    }
    public int Id
    {
        get;
        internal set;
    }

    public bool IsDirty
    {
        get;
        internal set;
    }

    public void Commit()
    {
        IsDirty = false;
    }

    public void Delete()
    {
        manager.Delete(this);
    }
}
/// <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)
    {
        T foundValue;
        values.TryGetValue(owner.Id, out foundValue);
        return foundValue;
    }

    /// <summary>Sets the owner's value</summary>
    public void SetValue(TrackableEntity owner, T value)
    {
        T foundValue;
        if (values.TryGetValue(owner.Id, out foundValue) && foundValue.Equals(value)
        {
            return;
        }
        owner.IsDirty = true;  
        values[owner.Id] = value;
    }
}
public void Delete(TrackableEntity entity) 
{
    if (entity is RelationshipEntity)
    {
        var casted = (RelationshipEntity)entity;
        casted.Accept(new RelationshipBreaker(casted));
    }
    entities.Remove(entity.Id);
}

Context

StackExchange Code Review Q#104752, answer score: 7

Revisions (0)

No revisions yet.