Click or drag to resize

How-to: Working with TrackerDog

This is a quick tutorial to learn how to use TrackerDog

Index of contents
How to turn objects into change-trackable ones

First of all, you need to design a regular class. Note that the class must not be sealed and its properties must be virtual to let the proxy generator override them to intercept both sets and gets:

C#
public class User
{
  public virtual string Name { get; set; }
  public virtual byte Age { get; set; }
}

Before being able to turn an object into a change-trackable one, you will need to tell TrackerDog that your class (i.e. the type) can be trackable:

C#
IObjectChangeTrackingConfiguration config = ObjectChangeTracking.CreateConfiguration();
config.TrackThisType<User>();

Above configuration will tell TrackerDog to track all property changes for the given type. This can be harmful and it's advisable that you tell TrackerDog which properties you want to track for the given type:

C#
// This will avoid some overhead since TrackerDog won't need to intercept absolutely all
// property changes
IObjectChangeTrackingConfiguration config = ObjectChangeTracking.CreateConfiguration();

config.TrackThisType<User>(t => t.IncludeProperties(u => u.Name, u => u.Age));

Now, to track an instance of User you need to use CreateFromTObject(TObject) method after getting ITrackableObjectFactory's implementation calling CreateConfiguration method:

C#
IObjectChangeTrackingConfiguration config = ObjectChangeTracking.CreateConfiguration();
config.TrackThisType<User>();

ITrackableObjectFactory trackableObjectFactory = config.CreateTrackableObjectFactory();

User user = trackableObjectFactory.CreateFrom(new User());

// or...
User user = new User();
user = trackableObjectFactory.CreateFrom(user);

// Also you can create a trackable object from the type directly without requiring an instance:
User user2 = trackableObjectFactory.CreateOf<User>();

Note that you won't be able to create trackable objects using CreateFromTObject(TObject) extension method if the whole object type has no paramterless constructor. Either if it has both a parameterless constructor or a one with parameters, and you want to create a trackable object with constructor arguments, you'll need to use CreateOfTObject(Object) method:

C#
public class A 
{
  public A() { }
  public A(int a, string b) { }
}

IObjectChangeTrackingConfiguration config = ObjectChangeTracking.CreateConfiguration();
config.TrackThisType<A>();

ITrackableObjectFactory trackableObjectFactory = config.CreateTrackableObjectFactory();

A a1 = trackableObjectFactory.CreateFrom(new A()); // Regular way
A a2 = trackacbleObjectFactory.CreateOf<A>(); // From type with parameterless constructor
A a3 = trackacbleObjectFactory.CreateOf<A>(101, "hello world"); // From type with constructor which has parameters

Now you can produce changes to the trackable object:

C#
user.Name = "Matías";
user.Age = 30;
How to track inheritance

TrackerDog can track inherited properties from a given type, but all base types must be configured separately. That is, members from base classes in the inheritance tree will be taken in account too when tracking for property changes:

C#
public class A
{
     public virtual string Text { get; set; }
}

public class B : A
{
     public virtual string Name { get; set; }
}

IObjectChangeTrackingConfiguration config = ObjectChangeTracking.CreateConfiguration();
config.TrackThisType<A>();
config.TrackThisType<B>();
Important note Important

A given type to track properties must be configured for the type where the whole properties are declared.

If you take last code sample as example, you will not configure A.Text on B

Configuring all types from a given assembly

Perhaps your project is getting bigger and configuring each type manually to be change-trackable can be extremely tedious, or you just want to simplify configuration code. No problem: TrackerDog can configure all types from a given assembly using two methods:

And this is a sample usage:

C#
IObjectChangeTrackingConfiguration config = ObjectChangeTracking.CreateConfiguration();
config.TrackTypesFromAssembly("MyAssemblyName");
config.TrackTypesFromAssembly(Assembly.GetExecutingAssembly());

While this saves a lot of time and makes things even simpler, it might happen that you need to customize the underlying configuration process some way.

Above goal can be achieved the second and/or third parameter on both mentioned methods:

Parameter

Description

configure

It is a delegate that will be called for each type being configured. The whole delegate receives a IConfigurableTrackableType implementation instance that should be enough to customize how the specific type will be tracked for changes.

searchSettings

It accepts a TypeSearchSettings instance. It provides extra information and behavior to configure how the whole methods will search for type and configure them as trackable types.

Attribute-based configuration

In addition to manually-configuring everything, you can mark certain types and their properties with attributes, and these will allow TrackerDog to configure them as change-trackable types.

Any type that should be change-trackable configured by attribute configuration will need the ChangeTrackableAttribute attribute:

C#
[ChangeTrackable]
public class A
{
    public virtual string Text { get; set; }
    public virtual int Number { get; set; }
}

Above code sample would mean that all properties should be included in the change-tracking process. Otherwise, you need to specify which ones should be change-tracked with the same attribute:

C#
[ChangeTrackable]
public class A
{
    [ChangeTrackable]
    public virtual string Text { get; set; }

    public virtual int Number { get; set; }
}

Last code listing above would mean that Number property must not be tracked. This is like a white list. If you want to specify which ones should not be tracked, it can be done with the DoNotTrackChangesAttribute attribute

C#
[ChangeTrackable]
public class A
{
    public virtual string Text { get; set; }

    [DoNotTrackChanges]
    public virtual int Number { get; set; }
}

In the other hand, there are some rules to understand how these attributes work:

  • If a type has been marked with [ChangeTrackable] and properties aren't marked with any of mentioned attributes, then, all properties are trackable

  • If a type has been marked with [ChangeTrackable] and one or more properties have been marked with [ChangeTrackable], only those with the whole attribute will be tracked.

  • If a type has been marked with [ChangeTrackable] and one or more properties have been marked with [DoNotTrackChanges], those with or without [ChangeTrackable] will be tracked.

TrackerDog's default behavior is to configure types with and without attributes, but you can override this behavior in certain conditions when calling the following methods:

All above listed methods accept an argument of type TypeSearchSettings and an instance of the whole search settings can set Mode property to AttributeConfigurationOnly which exactly meant to do restrict configuration to attribute-based one only.

This configuration approach is even more friendly when configuring types for an entire assembly!

Recursively-configuring trackable types

Configuring types that must be trackable can be a boring task. If you have found that you do not need to customize which properties to track on each type to track, maybe you have a lucky day: TrackerDog can configure types recursively using IObjectChangeTrackingConfiguration.TrackThisTypeRecursive``1(ActionIConfigurableTrackableType, FuncType, Boolean) and IObjectChangeTrackingConfiguration.TrackThisTypeRecursive(Type, ActionIConfigurableTrackableType, FuncType, Boolean) methods.

See following code listing:

C#
public class A
{
    public virtual B { get; set; }
}

public class B
{
    public virtual C { get; set; }
}

public class C
{
    public virtual B B { get; set; }
}

Instead of configuring each type separately, now you can configure A and the rest will be done auto-magically:

C#
IObjectChangeTrackingConfiguration config = ObjectChangeTracking.CreateConfiguration();
// This will also configure B and C
config.TrackThisTypeRecursive<A>();

Are you looking for customizing how each type is configured? It is still possible, but since TrackerDog does not know the types to configure until run-time, configuration gets worse because you cannot use expression trees but reflection:

C#
IObjectChangeTrackingConfiguration config = ObjectChangeTracking.CreateConfiguration();
config.TrackThisTypeRecursive<A>
(
    configure: t =>
    {
        if(t.Type = typeof(A)) 
        {
            t.IncludeProperties(t.Type.GetProperty("Text"), t.Type.GetProperty("B"));
        }
        else if(t.Type == typeof(B))
        {
            t.IncludeProperty(t.Type.GetProperty("C"));
        }
        // ...and so on
    }
);

Also, there is a third parameter of the whole method that can filter which types to track during the recursive configuration:

C#
IObjectChangeTrackingConfiguration config = ObjectChangeTracking.CreateConfiguration();
config.TrackThisTypeRecursive<A>
(
    configure: t =>
    {
        if(t.Type = typeof(A)) 
        {
            t.IncludeProperties(t.Type.GetProperty("Text"), t.Type.GetProperty("B"));
        }
        else if(t.Type == typeof(B))
        {
            t.IncludeProperty(t.Type.GetProperty("C"));
        }
        // ...and so on
    },
    // You want A, B, C and other types will not be configured! You can invent any valid filter based on the type that
    // must or must not be configured!
    filter: t => new [] { "A", "B", "C" }.Contains(t.Name)
);

The whole filter can be provided with or without the configure action. If you just want to filter types based on some convention, it is fine to provide the filter and no other parameter.

How to track interface implementation changes

In some cases, you will need to track changes of types that are unknown but you already know that there will be types that implement a given interface. Imagine that you want to track property changes of properties part of some interface:

C#
public interface IWhatever
{
    string Text { get; set; }
}

Now you can track the unknown! Let's configure IWhatever:

C#
IObjectChangeTrackingConfiguration config = ObjectChangeTracking.CreateConfiguration();
config.TrackThisType<IWhatever>();

Interface implementation change tracking will behave slightly different than classes, because TrackerDog will auto-magically configure these interface implementations and also all associations/aggregates that may be defined in the whole interface. For example, let's improve the IWhatever interface:

C#
public class Person
{
     public virtual string Name { get; set; }
}

public interface IWhatever
{
    string Text { get; set; }
    Person Person { get; set; }
}

IObjectChangeTrackingConfiguration config = ObjectChangeTracking.CreateConfiguration();
config.TrackThisType<IWhatever>();

Tracking IWhatever will automatically configure Person, and if there would be other associations at any level, it would do it too.

Finally, interface tracking configuration can also define which properties to track like the rest of the cases:

C#
IObjectChangeTrackingConfiguration config = ObjectChangeTracking.CreateConfiguration();
config.TrackThisType<IWhatever>(t => t.IncludeProperties(w => w.Text, w => w.Person));
How to turn trackable objects into untrackable objects

Sometimes, once an object has been tracked for changes for a while, some layer may need to get the object with latest property values but the object itself may not be change-trackable anymore.

A good use case for this is serialization. When serializing a trackable object, resulting serialization will contain not just source object properties but also change-tracking internal ones, and this can be very dangerous. In fact, since TrackerDog uses Castle DynamicProxy, serialized proxies will not be able to be deserialized because proxy requires some run-time details that will be available only when the object was created in memory

Turning an object to untrackable requires a call to ToUntrackedTObject(TObject):

C#
User untrackedUser = user.ToUntracked();

Calling the whole method not only will untrack the target object but also any association, including collections and its items.

In the other hand, it's possible to get an untracked version of some collection directly calling ToUntrackedEnumerable(IEnumerable, Type):

C#
public class A
{
}

public class B
{
    public virtual List<A> ListOfA { get; set; } = new List<A>();
}

IObjectChangeTrackingConfiguration config = ObjectChangeTracking.CreateConfiguration();
config.TrackThisType<A>()
      .TrackThisType<B>();

ITrackableObjectFactory trackableObjectFactory = config.CreateTrackableObjectFactory();

B b = trackableObjectFactory.CreateFrom(new B());

IList<A> list = b.ListOfA.ToUntrackedEnumerable(typeof(IList<>));
Note Note

The whole method to convert a collection to an untracked one requires a type, where the type is a collection type that must be configured. Follow this link to learn more.

Checking which properties have been changed

Call GetChangeTracker(Object) extension method:

C#
IObjectChangeTracker changeTracker = user.GetChangeTracker();

IImmutableSet<IObjectPropertyChangeTracking> changedProperties = changeTracker.ChangedProperties;

// Also you can get unchanged properties
IImmutableSet<IObjectPropertyChangeTracking> unchangedProperties = changeTracker.UnchangedProperties;

This will give sets of changed and unchanged properties typed as IObjectPropertyChangeTracking. Mostly this is fine, but the whole interface exposes the affected property as string. If you need further info to perform reflection operations, you should cast set elements to IDeclaredObjectPropertyChangeTracking:

C#
IEnumerable<IObjectPropertyChangeTracking> changedProperties = changeTracker.ChangedProperties.OfType<IDeclaredObjectPropertyChangeTracking>();

// Also you can get unchanged properties
IEnumerable<IObjectPropertyChangeTracking> unchangedProperties = changeTracker.UnchangedProperties.OfType<IDeclaredObjectPropertyChangeTracking>();

Now you'll be able to access Property.

Since IDeclaredObjectPropertyChangeTracking also implements IObjectPropertyChangeTracking, unless you need the property info of tracked property, there's no need to perform the whole cast.

Getting the value of some property before it was changed
Getting current property value

Call CurrentPropertyValueT, TReturn(T, ExpressionFuncT, TReturn) extension method:

C#
string currentUserName = user.CurrentPropertyValue(u => u.Name);

// Ok, you would also be able to achieve the same result doing so:
currentUserName = user.Name;

// But having this method lets you build an expression tree to select some tracked property "current value"...

// You can also get a property tracking object
IObjectChangeTracking userNameChangeTracking = user.GetPropertyTracking(u => u.Name);

currentUserName = userNameChangeTracking.CurrentValue;
Checking if a property changed its value since it was started to be tracked
How do I accept or undo changes

Call AcceptChanges(Object) or UndoChanges(Object) extension methods:

C#
user.AcceptChanges();

// or...
user.UndoChanges();
Tracking collection changes

TrackerDog supports tracking collection changes (i.e. it can observe collection changes). See the following code snippet to see how to use collection change tracking (if you want to know how to customize this feature please jump to Understanding and configuring collection change tracking):

C#
public class App 
{
    public virtual List<User> Users { get; } = new List<User>();
}

public class User 
{
    public string Name { get; set; }
    public byte Age { get; set; }
}

IObjectChangeTrackingConfiguration config = ObjectChangeTracking.CreateConfiguration();
config.TrackThisType<App>(t => t.IncludeProperty(app => app.Users))
      .TrackThisType<User>(t => t.IncludeProperties(u => u.Name, u => u.Age));

ITrackableObjectFactory trackableObjectFactory = config.CreateTrackableObjectFactory();

App app = trackableObjectFactory.CreateFrom(new App());
app.Users.Add(new User { Name = "Matías", Age = 30 });

IReadOnlyChangeTrackableCollection trackableCollection =
              (IReadOnlyChangeTrackableCollection)app.CurrentPropertyValue(app => app.Users);

IImmutableSet<object> addedItems = trackableCollection.AddedItems;
IImmutableSet<object> removedItems = trackableCollection.RemovedItems;

// If you need typed items...
IEnumerable<User> typedAddedUsers = addedItems.Cast<User>();
IEnumerable<User> typedRemovedUsers = removedItems.Cast<User>();

It might happen that you need to initialize some collection change tracking. For example, you want to consider changes after some other ones. Therefore, you would use ClearChangesTItem(ICollectionTItem):

C#
App app = trackableObjectFactory.CreateOf<App>();
app.Users.Add(new User { Name = "Matías", Age = 30 });

app.Users.ClearChanges();

// Now TrackerDog will only know about this new user on App.Users
app.Users.Add(new User { Name = "John", Age = 50 });
Object graph changes

When an aggregate root has many associations like 1-n, 1-1 or even M-N, TrackerDog works the same way as a POCO with no associations with other objects:

C#
public class A
{
    public virtual string Text { get; set; }
    public virtual B B { get; set; }
}

public class B
{
    public virtual string Text { get; set; }
    public virtual C C { get; set; }
}

public class C
{
    public virtual string Text { get; set; }
    public virtual IList<D> ListOfD { get; set; }
}

public class D
{
    public virtual string Text { get; set; }
}          

IObjectChangeTrackingConfiguration config = ObjectChangeTracking.CreateConfiguration();
config.TrackThisType<A>(t => t.IncludeProperties(a => a.Text, a => a.B))
      .TrackThisType<B>(t => t.IncludeProperties(b => b.Text, b => b.C))
      .TrackThisType<C>(t => t.IncludeProperties(c => c.Text, c => c.ListOfD))
      .TrackThisType<D>(t => t.IncludeProperty(d => d.Text));

ITrackableObjectFactory trackableObjectFactory = config.CreateTrackableObjectFactory();

// It will track the full object graph changes!
A a = trackableObjectFactory.CreateFrom
(
  new A
  {
    Text = initialValue,
    B = new B
    {
        Text = initialValue,
        C = new C
        {
            Text = initialValue,
            ListOfD = new List<D> { new D { Text = "initialValue" } }
        }
    }
  }
);

In addition, if you need to perform some action whenever some property changes, you can bind an handler to IObjectChangeTrackerChanged event:

C#
// Continuation of previous code sample...
IObjectChangeTracker changeTracker = a.GetChangeTracker();
changeTracker.Changed += (sender, e) => 
{
    // Do stuff here whenever a property changes
};

The whole event will give you an instance of DeclaredObjectPropertyChangeEventArgs event arguments, which will provide access to which property was changed, the target object that changed, and also an associated IObjectGraphTrackingInfo implementation instance to introspect full aggregate/association hierarchy.

Observe property and collection changes

All change-tracked objects implement INotifyPropertyChanged and change-tracked collection properties implement INotifyCollectionChanged.

Note that collection changes will also trigger a PropertyChanged event on the object in the other side of the 1-n association.

C#
public class App 
{
    public virtual string Name { get; set; }
    public List<User> Users { get; } = new List<User>();
}

public class User 
{
    public virtual string Name { get; set; }
    public virtual byte Age { get; set; }
}

IObjectChangeTrackingConfiguration config = ObjectChangeTracking.CreateConfiguration();
config.TrackThisType<App>(t => t.IncludeProperty(app => app.Users))
      .TrackThisType<User>(t => t.IncludeProperties(u => u.Name, u => u.Age));

ITrackableObjectFactory trackableObjectFactory = config.CreateTrackableObjectFactory();

App app = trackableObjectFactory.CreateFrom(new App());

((INotifyPropertyChanged)app).PropertyChanged += (sender, e) =>
{
    string propertyName = e.PropertyName;
};

((INotifyCollectionChanged)app.Users).CollectionChanged += (sender, e) =>
{
    IEnumerable<object> changedItems = e.ChangedItems;
};

app.Name = "MyApp"; // This will trigger a PropertyChanged event on app

 // This will both trigger a CollectionChanged event on app.Users and a PropertyChanged event on
 // app.
app.Users.Add(new User { Name = "Matías" });
Track dynamic objects

A dynamic object (i.e. a derived class of DynamicObject) can't be tracked per se. For example, ExpandoObject can't be tracked because it's a sealed class and either way its members aren't virtual.

TrackerDog supports dynamic objects but they must be non-sealed class and their members must be virtual. That is, TrackerDog will be able to track custom dynamic objects. For example:

C#
public class TestDynamicObject : DynamicObject
{
    private string value;

    public override bool TryGetMember(GetMemberBinder binder, out object result)
    {
        result = value;

        return true;
    }

    public override bool TrySetMember(SetMemberBinder binder, object value)
    {
        this.value = value?.ToString();
        return true;
    }
}

Above class has no sense in a real-world case, since any try to set a member sets the same class field and whenever any member is tried to be got, it will return the last set value. But this sample dynamic object is a good enough to test that TrackerDog can track dynamic objects:

C#
IObjectChangeTrackingConfiguration config = ObjectChangeTracking.CreateConfiguration();
config.TrackThisType<DynamicObject>());

ITrackableObjectFactory trackableObjectFactory = config.CreateTrackableObjectFactory();

dynamic trackable = trackableObjectFactory.CreateFrom(new TestDynamicObject());
trackable.Text = "hello world";
trackable.Text = "hello world2";

Now the issue will be using [M:TrackerDog.ITrackableObjectFactory.CreateFrom``1(``0))] with a dynamic object. You'll need to cast the dynamic-typed variable into object to be able to call all object change tracking-related extension methods:

C#
// Note that all extension methods that relate to dynamic objects will require
// you to give the property name as string, because expression trees can't work with 
// dynamic objects, and anyway, "Text" property doesn't exist since it's not declared but
// dynamically-handled using DynamicObject.TryGetMemeber and DynamicObject.TrySetMember
string currentTextValue = ((object)trackable).OldPropertyValue("Text");
IObjectChangeTracker changeTracker ((object)trackable).GetChangeTracker();
// ...and so on.

When getting property trackings, there's a difference between declared properties and dynamic properties

  • ...when you want a tracking of a declared property (i.e. a design-time declared property), you'll get a IDeclaredObjectPropertyChangeTracking, and it will provide access you to the Property which is of type PropertyInfo.

  • ...when you want a tracking of a dynamic property (i.e. a run-time added property), you'll get a IObjectPropertyChangeTracking, and it will provide access you to the PropertyName which provides less info than a PropertyInfo since the property belongs to the object where it was declared but since it's not an actual property, you don't know more than just its name.

For example, if you want to get a dynamic property tracking, you would do as follows:

C#
IObjectChangeTracker changeTracker ((object)trackable).GetChangeTracker();
IObjectPropertyChangeTracking propertyTracking = tracker.GetDynamicTrackingByProperty("Text");
Useful metadata

Once trackable types have been already configured, TrackerDog can expose useful metadata that is being used internally but it can be also required outside of the project's code base.

Object paths

Sometimes external tools and frameworks might require to know the full path to some property part of a given trackable type.

For example, consider the following class hierarchy:

C#
public class A
{
  public string Text { get; set; }
  public B B { get; set; }
}

public class B
{
    public string Name { get; set; }
    public C C { get; set; }
} 

public class C 
{
    public string Description { get; set; }
}

Once trackable types have been already configured, it's very easy to get a trackable type metadata:

C#
IObjectChangeTrackingConfiguration config = ObjectChangeTracking.CreateConfiguration()
                                                  .TrackThisType<A>();

ITrackableType metadata = config.GetTrackableType(typeof(A));

What if some project needs to know the path to C.Description property starting from A (i.e. A.B.C.Description)?

No problem, because ITrackableType has an ObjectPaths property which holds a collection of all properties of some given trackable type and its associations, and each property is represented by the IObjectPropertyInfo interface, which has, for example, Path property that gives the path to some particular property starting from the trackable type. For example, the so-called A.B.C.Description

Read the tests too

In addition to this tutorial, you might learn more reading the unit/integration tests provided as part of project's source code. You can also clone the repository in your own computer and run the whole tests using the Visual Studio debugger to check how TrackerDog works step by step!

See Also