What would be the best way to implement change tracking on an object

To do this you can't really use automatic getter & setters, and you need to set IsDirty in each setter.

I generally have a "setProperty" generic method that takes a ref parameter, the property name and the new value. I call this in the setter, allows a single point where I can set isDirty and raise Change notification events e.g.

protected bool SetProperty<T>(string name, ref T oldValue, T newValue) where T : System.IComparable<T>
    {
        if (oldValue == null || oldValue.CompareTo(newValue) != 0)
        {
            oldValue = newValue;
            PropertyChanged?.Invoke(this, new System.ComponentModel.PropertyChangedEventArgs(name));
            isDirty = true;
            return true;
        }
        return false;
    }
// For nullable types
protected void SetProperty<T>(string name, ref Nullable<T> oldValue, Nullable<T> newValue) where T : struct, System.IComparable<T>
{
    if (oldValue.HasValue != newValue.HasValue || (newValue.HasValue && oldValue.Value.CompareTo(newValue.Value) != 0))
    {
        oldValue = newValue;
        PropertyChanged?.Invoke(this, new System.ComponentModel.PropertyChangedEventArgs(name));
    }
}

You can implement the IChangeTracking or IRevertibleChangeTracking interfaces, now included in .NET Standard 2.0.

Implementation is as follows:

IChangeTracking:

class Entity : IChangeTracking
{
  string _FirstName;
  public string FirstName
  {
    get => _FirstName;
    set
    {
      if (_FirstName != value)
      {
        _FirstName = value;
        IsChanged = true;
      }
    }
  }

  string _LastName;
  public string LastName
  {
    get => _LastName;
    set
    {
      if (_LastName != value)
      {
        _LastName = value;
        IsChanged = true;
      }
    }
  }

  public bool IsChanged { get; private set; }    
  public void AcceptChanges() => IsChanged = false;
}

IRevertibleChangeTracking:

class Entity : IRevertibleChangeTracking
{
  Dictionary<string, object> _Values = new Dictionary<string, object>();

  string _FirstName;
  public string FirstName
  {
    get => _FirstName;
    set
    {
      if (_FirstName != value)
      {
        if (!_Values.ContainsKey(nameof(FirstName)))
          _Values[nameof(FirstName)] = _FirstName;
        _FirstName = value;
        IsChanged = true;
      }
    }
  }

  string _LastName;
  public string LastName
  {
    get => _LastName;
    set
    {
      if (_LastName != value)
      {
        if (!_Values.ContainsKey(nameof(LastName)))
          _Values[nameof(LastName)] = _LastName;
        _LastName = value;
        IsChanged = true;
      }
    }
  }

  public bool IsChanged { get; private set; }

  public void RejectChanges()
  {
    foreach (var property in _Values)
      GetType().GetRuntimeProperty(property.Key).SetValue(this, property.Value);
    AcceptChanges();
  }

  public void AcceptChanges()
  {
    _Values.Clear();
    IsChanged = false;
  }
}

Another option, which I like the most, is to use a change tracking library, such as TrackerDog, that generates all the boilerplate code for you, while just need to provide POCO entities.

There are more ways to achieve this if you don't want to implement all the properties by hand. One option is to use a weaving library, such as Fody.PropertyChanged and Fody.PropertyChanging, and handle the change methods to cache old values and track object state. Another option is to have the object's graph stored as MD5 or some other hash, and reset it upon any change, you might be surprised, but if you don't expect zillion changes and if you request it only on demand, it can work really fast.

Here is an example implementation (Note: requires Json.NET and Fody/PropertyChanged:

[AddINotifyPropertyChangedInterface]
class Entity : IChangeTracking
{
  public string UserName { get; set; }
  public string LastName { get; set; }

  public bool IsChanged { get; private set; }

    string hash;
  string GetHash()
  {
    if (hash == null)
      using (var md5 = MD5.Create())
      using (var stream = new MemoryStream())
      using (var writer = new StreamWriter(stream))
      {
        _JsonSerializer.Serialize(writer, this);
        var hash = md5.ComputeHash(stream);
        this.hash = Convert.ToBase64String(hash);
      }
    return hash;
  }

  string acceptedHash;
  public void AcceptChanges() => acceptedHash = GetHash();

  static readonly JsonSerializer _JsonSerializer = CreateSerializer();
  static JsonSerializer CreateSerializer()
  {
    var serializer = new JsonSerializer();
    serializer.Converters.Add(new EmptyStringConverter());
    return serializer;
  }

  class EmptyStringConverter : JsonConverter
  {
    public override bool CanConvert(Type objectType) 
      => objectType == typeof(string);

    public override object ReadJson(JsonReader reader,
      Type objectType,
      object existingValue,
      JsonSerializer serializer)
      => throw new NotSupportedException();

    public override void WriteJson(JsonWriter writer, 
      object value,
      JsonSerializer serializer)
    {
      if (value is string str && str.All(char.IsWhiteSpace))
        value = null;

      writer.WriteValue(value);
    }

    public override bool CanRead => false;  
  }   
}

Dan's solution is perfect.

Another option to consider if you're going to have to do this on multiple classes (or maybe you want an external class to "listen" for changes to the properties):

  • Implement the INotifyPropertyChanged interface in an abstract class
  • Move the IsDirty property to your abstract class
  • Have Class1 and all other classes that require this functionality to extend your abstract class
  • Have all your setters fire the PropertyChanged event implemented by your abstract class, passing in their name to the event
  • In your base class, listen for the PropertyChanged event and set IsDirty to true when it fires

It's a bit of work initially to create the abstract class, but it's a better model for watching for data changes as any other class can see when IsDirty (or any other property) changes.

My base class for this looks like the following:

public abstract class BaseModel : INotifyPropertyChanged
{
    /// <summary>
    /// Initializes a new instance of the BaseModel class.
    /// </summary>
    protected BaseModel()
    {
    }

    /// <summary>
    /// Fired when a property in this class changes.
    /// </summary>
    public event PropertyChangedEventHandler PropertyChanged;

    /// <summary>
    /// Triggers the property changed event for a specific property.
    /// </summary>
    /// <param name="propertyName">The name of the property that has changed.</param>
    public void NotifyPropertyChanged(string propertyName)
    {
        if (this.PropertyChanged != null)
        {
            this.PropertyChanged.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}

Any other model then just extends BaseModel, and calls NotifyPropertyChanged in each setter.


Set IsDirty to true in all of your setters.

You might also consider making the setter for IsDirty private (or protected, if you may have child classes with additional properties). Otherwise you could have code outside of the class negating its internal mechanism for determining dirtiness.


If there are a very large number of such classes, all having that same pattern, and you frequently have to update their definitions, consider using code generation to automatically spit out the C# source files for all the classes, so that you don't have to manually maintain them. The input to the code generator would just be a simple text file format that you can easily parse, stating the names and types of the properties needed in each class.

If there are just a small number of them, or the definitions change very infrequently during your development process, then it's unlikely to be worth the effort, in which case you may as well maintain them by hand.

Update:

This is probably way over the top for a simple example, but it was fun to figure out!

In Visual Studio 2008, if you add a file called CodeGen.tt to your project and then paste this stuff into it, you'll have the makings of a code generation system:

<#@ template debug="false" hostspecific="true" language="C#v3.5" #>
<#@ output extension=".cs" #>
<#@ assembly name="System.Core" #>
<#@ import namespace="System.Linq" #>

<# 

// You "declare" your classes here, as in these examples:

var src = @"

Foo:     string Prop1, 
         int Prop2;

Bar:     string FirstName,
         string LastName,
         int Age;
";

// Parse the source text into a model of anonymous types

Func<string, bool> notBlank = str => str.Trim() != string.Empty;

var classes = src.Split(';').Where(notBlank).Select(c => c.Split(':'))
    .Select(c => new 
    {
        Name = c.First().Trim(),
        Properties = c.Skip(1).First().Split(',').Select(p => p.Split(' ').Where(notBlank))
                      .Select(p => new { Type = p.First(), Name = p.Skip(1).First() })
    });
#>

// Do not edit this file by hand! It is auto-generated.

namespace Generated 
{
<# foreach (var cls in classes) {#>    class <#= cls.Name #> 
    {
        public bool IsDirty { get; private set; }
        <# foreach (var prop in cls.Properties) { #>

        private <#= prop.Type #> _storage<#= prop.Name #>; 

        public <#= prop.Type #> <#= prop.Name #> 
        {
            get { return _storage<#= prop.Name #>; }
            set 
            {
                IsDirty = true;
                _storage<#= prop.Name #> = value;
            }
        } <# } #>

    }

<# } #>
}

There's a simple string literal called src in which you declare the classes you need, in a simple format:

Foo:     string Prop1,
         int Prop2;

Bar:     string FirstName,
         string LastName,
         int Age;

So you can easily add hundreds of similar declarations. Whenever you save your changes, Visual Studio will execute the template and produce CodeGen.cs as output, which contains the C# source for the classes, complete with the IsDirty logic.

You can change the template of what is produced by altering the last section, where it loops through the model and produces the code. If you've used ASP.NET, it's similar to that, except generating C# source instead of HTML.