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