6
\$\begingroup\$

There are a number of topics pertaining to entity change tracking. All the ones I've seen involve either 1) notifying when a property has changed or 2) merely determining whether or not an entity is dirty.

My goal is quite different. I wrote a mini-repository framework that generates SQL for inserts, selects, updates, and deletes. Updates are particularly tricky because the framework needs to support partial updates. To do this, you have to know which properties have changed and only generate the update SQL for those specific fields. Otherwise, you run the risk of overwriting existing data with nulls or default values unless you load the original entity from the database prior to the update.

Take this person class as an example:

public class Person
{
    public string FirstName { get; set; }

    public string LastName { get; set; }

    public DateTime DateOfBirth { get; set; }
}

Quite a bit of boilerplate code would be required to allow it to track its own property changes. Here's an example (leaving LastName and DateOfBirth alone for brevity):

public class Person
{
    HashSet<string> ChangedProperties = 
        new HashSet<string>(StringComparer.OrdinalIgnoreCase);

    string _FirstName = null;
    public string FirstName
    {
        get { return _FirstName; }
        set
        {
            ChangedProperties.Add(nameof(FirstName));
            _FirstName = value;
        }
    }

    public string LastName { get; set; }

    public DateTime DateOfBirth { get; set; }

    public string[] GetChangedProperties()
    {
        return ChangedProperties.ToArray();
    }
}

Imagine having to do this for 50 properties or more (some of the classes I work with have 90+ properties).

So I came up with a special tracking class that uses generics and expressions to encapsulate all of that boilerplate property code. Having actually begun to use it in my application, I've developed a bit of a love/hate relationship with it.

Here's the watered-down version of the class:

public abstract class PropertyChangeTracker
{
    public static PropertyChangeTracker<TEntity> Create<TEntity>(TEntity entity) where TEntity : class
    {
        return new PropertyChangeTracker<TEntity>(entity);
    }
}

public class PropertyChangeTracker<TEntity> : PropertyChangeTracker where TEntity : class
{
    readonly TEntity entity = null;
    HashSet<PropertyInfo> changedProperties = new HashSet<PropertyInfo>();

    public PropertyChangeTracker(TEntity entity)
    {
        this.entity = entity;
    }

    public void Set<TValue>(Expression<Func<TEntity, TValue>> expression, TValue value)
    {
        var member = (expression.Body as MemberExpression).Member as PropertyInfo;

        changedProperties.Add(member);

        member.SetValue(entity, value);
    }

    public PropertyInfo[] GetChangedProperties()
    {
        return changedProperties.ToArray();
    }
}

It uses reflection to set the property which is, by many engineer standards, a serious no-no that will result in a Christmas stocking full of coal. My production version of the above code actually uses dynamically-generated delegates using expression trees to set entity properties (I may post that code for review at another time), but this should get you started. So long as you're not using reflection haphazardly within thousands of iterations, it isn't really that bad, especially these days with faster hardware and the accumulation of all the .NET framework optimizations that have taken place since .NET's early days.

So here's how you use the class:

var bobJones = PersonRepo.GetPersonById(100);
var tracker = PropertyChangeTracker.Create(bobJones);

tracker.Set(e => e.LastName, "Jones");
tracker.Set(e => e.DateOfBirth, new DateTime(1970, 2, 15));

In that example, you load Bob from the database and make a correction to his last name and DOB. The tracking class will track that only those properties have changed. When you go to generate your update, you just do this:

    var Properties = Tracker.GetChangedProperties();

    string Sql = $@"
update persontable 
set {string.Join(", ", Properties.Select(pi => $"{pi.Name} = :{pi.Name}").ToArray())}
where id = 100";

My framework knows how to get the entity table name and primary key for the where clause, but this shows how the update fields are generated:

update persontable 
set LastName = :LastName, DateOfBirth = :DateOfBirth
where id = 100

You can then parameterize it like this:

    var Parameters = Properties.Select(p => new YourDbParameter(p.Name, p.GetValue(BobJones)));

So about that love/hate relationship I mentioned earlier. Here are the pros/cons to this:

Pros

  • The automation/encapsulation aspect prevents the need to write massive amounts of boilerplate code within your class, especially when you're dealing with a large number of properties. Going from auto-properties to properties with concrete setters and getters that contain tracking logic can increase your class code by as much as 11 lines of code per property.
  • The extraction of the property name from an expression eliminates magic strings, though .NET 4.6.1 now offers the nameof keyword which can mitigate this as shown in my first example.
  • This solution can be retrofitted for use with any existing class without having to convert every auto-property to complex properties with tracking logic.

Cons:

  • In an effort to eliminate boilerplate code within your class, you end up writing a bit more code anyway in the form of a lambda expressions every time you want to track changes to your class, but this is typically only noticeable with VB.NET and its verbose lambda syntax. If you just bite down and write all the tracking code within your class, you can, as Ron Popeil would say, set it and forget it and only have to update the code whenever you need to add a new property.

Overall, I really think the pros outweigh the cons, but want to see if anyone else can find a better way to achieve what I've done here or extol the virtues of another implementation.

\$\endgroup\$
5
  • \$\begingroup\$ Did you consider t4 for auto-generating boilerplate code? You can also either wrap entities in a generated wrapper that tracks changes to the properties or simply store a copy of the originally retrieved entity inside your Tracker. But maybe my real question is why you spend time writing your own ORM these days instead of focusing on your business logic. \$\endgroup\$ May 11, 2016 at 17:45
  • \$\begingroup\$ @MattiasÅslund Haven't heard of T4, but will look into it. I also don't want to store a copy of the original entity because that means I have to make a database call to get it and I am a minimalist when it comes to accessing resources. I am not really sure I understand the last question. \$\endgroup\$ May 11, 2016 at 17:52
  • \$\begingroup\$ @oscilatingcretin Microsoft has given up on their SelfTrackingEntities template for a reason: too much complexity and too little usability. Building your own template will get you to the same conclusion in the end. The problem is multifold, there are just too many aspects to think about when making a generic solution: change tracking, multi-level change tracking, graph navigation, object relations (aggregation vs composition vs any other kind of association), error resolution. \$\endgroup\$
    – dfhwze
    May 18, 2019 at 19:26
  • \$\begingroup\$ Also on a technical level, you'll get massive amounts of boiler-plate event plumbing (ipropertychanged, ieditableobject, ichangetracking, ierrorinfo, inotifiicationcollection, ..). Sometimes, it's just better to customize a model for a specific situation. \$\endgroup\$
    – dfhwze
    May 18, 2019 at 19:26
  • \$\begingroup\$ What a coincidence, I just asked a similar question: codereview.stackexchange.com/questions/226759/… o_0 \$\endgroup\$
    – dfhwze
    Aug 24, 2019 at 19:28

1 Answer 1

2
\$\begingroup\$

I suggest you using static AOP something like Fody or Postsharp , you do not need change you entity code, no implement no change code, just add a Attribute to POCO class, TrackChange.Fody can perfect to solve your problem, https://github.com/jrt324/TrackChange

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.