danielwertheim

danielwertheim


notes from a passionate developer

Share


Sections


Tags


Disclaimer

This is a personal blog. The opinions expressed here represent my own and not those of my employer, nor current or previous. All content is published "as is", without warranty of any kind and I don't take any responsibility and can't be liable for any claims, damages or other liabilities that might be caused by the content.

How to build a simple object graph delta comparer in C# using Structurizer

Recently I heard of the need of taking two typed objects and producing the delta of their properties. So lets look at a simple solution to an object compare that inspects the complete graph of two objects and produces a result of properties that doesn't match.

I have a simple project: "Structurizer" (GitHub and NuGet), that turns an object graph to structure indices, where each index contains: name, path, data type and the value for a leaf. The data is extracted using cached model meta and IL code is generated to access the properties in an efficient manner. All this provides a good foundation for building an object compare solution.

Remember. This is just a blog post, aiming at providing "inspiration". It's not an "all mighty feature complete solution".

A model

Lets define a sample model for this post. The well known Customer.

public class Customer
{
  public string FirstName { get; set; }
  public string LastName { get; set; }
  public Address PrimaryAddress { get; set; }
  public string[] Tags { get; set; }
}

public class Address
{
  public string Street { get; set; }
  public string Zip { get; set; }
  public string City { get; set; }
  public string Country { get; set; }
}

What is a structure index?

It's the value of a certain leaf in your graph. It looks like this:

public interface IStructureIndex
{
  //Full name without index in arrays etc
  string Name { get; }

  //Like Name, but also contains the index position in arrays etc
  string Path { get; }

  //The value of the property
  object Value { get; }

  //The data type
  Type DataType { get; }

  //Data type classification
  DataTypeCode DataTypeCode { get; }
}

A test

The test just shows some simple usage. The result of the compare exposes IStructureIndex data directly. Maybe you don't want to expose external library models, in that case you could map to something under your control.

[Test]
public void Sample()
{
  var a = new Customer
  {
    FirstName = "Daniel",
    LastName = "Andersson",
    PrimaryAddress = new Address
    {
      Street = "Some street 1",
      Zip = "11111",
      City = "City1",
      Country = "Sweden"
    },
    Tags = new[] { "Test1", "Test3" }
  };

  var b = new Customer
  {
    FirstName = "Daniel",
    LastName = "Olsson",
    PrimaryAddress = new Address
    {
      Street = "Some street 1",
      Zip = "22222",
      City = "City2",
      Country = "Sweden"
    },
    Tags = new[] { "Test1", "Test2", "Test3" }
  };

  var comparer = new ObjectComparer();
  var result = comparer.Compare(a, b);

  result.Deltas.Should().HaveCount(4);
  result.Deltas[0].Name.Should().Be("LastName");
  result.Deltas[0].A.Value.Should().Be("Andersson");
  result.Deltas[0].B.Value.Should().Be("Olsson");

  result.Deltas[1].Name.Should().Be("PrimaryAddress.Zip");
  result.Deltas[1].A.Value.Should().Be("11111");
  result.Deltas[1].B.Value.Should().Be("22222");

  result.Deltas[2].Name.Should().Be("PrimaryAddress.City");
  result.Deltas[2].A.Value.Should().Be("City1");
  result.Deltas[2].B.Value.Should().Be("City2");

  result.Deltas[3].Name.Should().Be("Tags");
  result.Deltas[3].Path.Should().Be("Tags[1]");
  result.Deltas[3].A.Value.Should().Be("Test3");
  result.Deltas[3].B.Value.Should().Be("Test2");
}

The implementation

Lets first have a look at what the ObjectComparer.Comare result looks like:

public class ObjectDelta<TObject>
{
  public TObject ObjA { get; }
  public TObject ObjB { get; }
  public IReadOnlyList<PropertyDelta> Deltas { get; }

  public ObjectDelta(
    TObject objA,
    TObject objB,
    IReadOnlyList<PropertyDelta> deltas)
  {
    ObjA = objA;
    ObjB = objB;
    Deltas = deltas;
  }
}

public class PropertyDelta
{
  public string Name => A.Name;
  public string Path => A.Path;

  public IStructureIndex A { get; }
  public IStructureIndex B { get; }

  public PropertyDelta(
    IStructureIndex a,
    IStructureIndex b)
  {
    A = a;
    B = b;
  }
}

Now to the final piece, and also where you would invest some time. I did a simple Zip between the different properties. Using the fact that I know that they are produced in the same order. But maybe you want to look at Index.Path and their associated values?

public class ObjectComparer
{
  public ObjectDelta<TObject> Compare<TObject>(TObject objA, TObject objB) where TObject : class
  {
    var builder = Cache<TObject>.Builder;
    var objAStructure = builder.CreateStructure(objA);
    var objBStructure = builder.CreateStructure(objB);

    var propertyDeltas = objAStructure.Indexes.Zip(objBStructure.Indexes, (a, b) =>
    {
      if (a.Value == null && b.Value == null)
        return null;

      if (ReferenceEquals(a.Value, b.Value))
        return null;

      if (Equals(a, b))
        return null;

      return new PropertyDelta(a, b);
    })
    .Where(pd => pd != null)
    .ToList();

    return new ObjectDelta<TObject>(objA, objB, propertyDeltas);
  }
}

internal static class Cache<TObject> where TObject : class
{
  internal static readonly IStructureBuilder Builder;

  static Cache()
  {
    var typeConfig = new StructureTypeConfigurations();
    typeConfig.Register<TObject>();
    Builder = StructureBuilder.Create(typeConfig);
  }
}

That's it. Hope it got you thinking.

Cheers,

//Daniel

View Comments