This library offers methods returning a collection of objects containing information concerning any deltas, i.e., changes, on the properties of an object. The collection can be generated by providing two instances of an object, or an instance of an object and a JObject containing entries keyed to properties on the object. This can be useful for creating audit logs or generating SQL. For example, if you have a PATCH endpoint receiving an instance of an object and you want to generate SQL updating only the changed properties of the object, this library provides a list of objects--one for each changed property--containing information that can be used to create that SQL.
Compatible with the following:
- .NET Framework 4.5
- .NET Framework 4.6
- .NET Framework 4.6.1
- .NET Standard 1.6
- .NET Standard 2.0
This library contains an overloaded extension method GetDeltaObjects off of T that accepts either another instance of T or a JObject. In addition, the library offers a class called DeltaObjectEngine implementing IDeltaObjectEngine, which offers the overloaded method GetDeltaObjects providing functionality identical to the extension method just described. Using the interface rather than the extension method offers the possibility of mocking the functionality in tests.
If an object of type T is passed as the final argument, the return type is a List<DeltaObject>. If an object of type JObject is passed as the final argument, the return type is a DeltaGroup.
A DeltaObject has the following public properties:
PropertyNameThe name of the property.PropertyAliasThe alias of the property. This defaults to the property's name, and can be set using theDeltaObjectAliasattribute, discussed below.OriginalValueThe original value of the property.NewValueThe new value of the property.StringifiedOriginalValueThe original value of the property as astring.StringifiedNewValueThe new value of the property as astring.ValueConversionStatusThe status of the conversion of the new value of the property into the property's type. This is anenumwith fields ofSuccessandFail. Note that this property is only relevant when aDeltaObjectis calculated using aJObject, since it is only in that situation where it is possible that a new value could not be converted into the property's type.
A DeltaObject is generated only for non-indexed properties of the following types:
- primitives
decimalstringDateTimeDateTimeOffsetTimeSpanGuid- nullables, e.g.,
int?,DateTime? - enums
Any property on an object not among the above types will be ignored by the delta-object generator. In addition, you may add attributes to properties or to a class to have the delta-object generator ignore certain properties in certain situations, discussed below in the attributes section.
A DeltaGroup has the following properties:
ValueConversionStatusThe status of the conversions of the new values of the properties into their associated types. This is anenumwith fields ofNoneFailed,SomeFailed, andAllFailed. The value isNoneFailedwhen no values failed to be converted or when there are no deltas. The value isSomeFailedwhen some values failed to be converted and some conversions succeeded. The value isAllFailedwhen all values failed to be converted.DeltaObjectsAList<DeltaObject>whereValueConversionStatuson eachDeltaObjectisSuccess.DeltaObjectsValueConversionFailAList<DeltaObject>whereValueConversionStatuson eachDeltaObjectisFail. When the value ofValueConversionStatusis anything other thanNoneFailed, thenDeltaObjectsValueConversionFailwill not be empty.
public class Customer
{
public string FirstName { get; set; }
public string LastName { get; set; }
public int Transactions { get; set; }
public DateTime DateOfBirth { get; set; }
}
var originalCustomer = new Customer
{
FirstName = "originalFirstName",
LastName = "originalLastName",
Transactions = 30,
DateOfBirth = new DateTime(1919, 10, 10)
};
var updatedCustomer = new Customer
{
FirstName = "newFirstName",
LastName = "originalLastName",
Transactions = 95,
DateOfBirth = new DateTime(2009, 2, 3)
};
var deltaObjects = originalCustomer.GetDeltaObjects(updatedCustomer);
foreach (var deltaObject in deltaObjects)
{
Console.WriteLine(
$"Property name: {deltaObject.PropertyName}\n" +
$"Property alias: {deltaObject.PropertyAlias}\n" +
$"Original value: {deltaObject.OriginalValue}\n" +
$"New value: {deltaObject.NewValue}\n\n" +
$"********************************************\n");
}The above code will print the following to the console:
Property name: FirstName
Property alias: FirstName
Original value: originalFirstName
New value: newFirstName
********************************************
Property name: Transactions
Property alias: Transactions
Original value: 30
New value: 95
********************************************
Property name: DateOfBirth
Property alias: DateOfBirth
Original value: 10/10/1919 12:00:00 AM
New value: 2/3/2009 12:00:00 AM
********************************************
As you can see, a DeltaObject was generated for each property on Customer that had a changed value.
public class Customer
{
public string FirstName { get; set; }
public string LastName { get; set; }
public int Transactions { get; set; }
public DateTime DateOfBirth { get; set; }
}
var originalCustomer = new Customer
{
FirstName = "originalFirstName",
LastName = "originalLastName",
Transactions = 30,
DateOfBirth = new DateTime(1919, 10, 10)
};
var newCustomer = new
{
FirstName = "newFirstName",
LastName = "originalLastName",
Transactions = "fifty",
DateOfBirth = "December 8, 1979"
};
var newCustomerJObject = JObject.FromObject(newCustomer);
var customerDeltaGroup = originalCustomer.GetDeltaObjects(newCustomerJObject);
Console.WriteLine($"Group value conversion status: {customerDeltaGroup.ValueConversionStatus}\n" +
$"********************************************\n");
foreach (var deltaObject in customerDeltaGroup.DeltaObjects)
{
Console.WriteLine(
$"Property name: {deltaObject.PropertyName}\n" +
$"Property alias: {deltaObject.PropertyAlias}\n" +
$"Original value: {deltaObject.OriginalValue}\n" +
$"New value: {deltaObject.NewValue}\n" +
$"Value conversion status: {deltaObject.ConversionStatus}\n\n" +
$"********************************************\n");
}
foreach (var deltaObject in customerDeltaGroup.DeltaObjectsValueConversionFail)
{
Console.WriteLine(
$"Property name: {deltaObject.PropertyName}\n" +
$"Property alias: {deltaObject.PropertyAlias}\n" +
$"Original value: {deltaObject.OriginalValue}\n" +
$"New value: {deltaObject.NewValue}\n" +
$"Value conversion status: {deltaObject.ConversionStatus}\n\n" +
$"********************************************\n");
}The above code will print the following to the console:
Group value conversion status: SomeFailed
********************************************
Property name: FirstName
Property alias: FirstName
Original value: originalFirstName
New value: newFirstName
Value conversion status: Success
********************************************
Property name: DateOfBirth
Property alias: DateOfBirth
Original value: 10/10/1919 12:00:00 AM
New value: 12/8/1979 12:00:00 AM
Value conversion status: Success
********************************************
Property name: Transactions
Property alias: Transactions
Original value: 30
New value: fifty
Value conversion status: Fail
********************************************
Notice that the conversion status of the new value for the Transactions property is Fail because the string "fifty" cannot be converted into an int.
Properties on an object are associated with JObject properties without regard to casing. Therefore, the above example would produce the same result if the property on the JObject were firstname rather than FirstName.
This attribute can be applied to a property. It assigns a value to the PropertyAlias property of the DeltaObject. For example:
public class Customer
{
public string FirstName { get; set; }
[DeltaObjectAlias("last_name")]
public string LastName { get; set; }
}
var originalCustomer = new Customer
{
FirstName = "originalFirstName",
LastName = "originalLastName"
};
var newCustomerWithAlias = new Customer
{
FirstName = "newFirstName",
LastName = "newLastName"
};
var deltaObjects = originalCustomer.GetDeltaObject(newCustomerWithAlias);
foreach (var deltaObject in deltaObjects)
{
Console.WriteLine(
$"Property name: {deltaObject.PropertyName}\n" +
$"Property alias: {deltaObject.PropertyAlias}\n" +
$"Original value: {deltaObject.OriginalValue}\n" +
$"New value: {deltaObject.NewValue}\n\n" +
$"********************************************\n");
}The above code will print the following to the console:
Property name: FirstName
Property alias: FirstName
Original value: originalFirstName
New value: newFirstName
********************************************
Property name: LastName
Property alias: last_name
Original value: originalLastName
New value: newLastName
********************************************
As you can see, the LastName property has an alias of last_name in its DeltaObject, and any property without a specified alias will have an alias on its DeltaObject equal to the name of the property. This can be useful if you want to generate SQL and the property name differs from the database column.
This attribute can be applied to a property to force the delta-object generator to ignore that property in all cases.
This attribute can be applied to a property to force the delta-object generator to ignore that property when its value is equal to the property type's default value. This attribute can also be applied to a class, which will force the delta-object generator to ignore all properties on the class whose value is default.
The delta-object generator uses reflection to get information about a type and, for the sake of performance, caches the results in a static class. Below are some performance measurements observed on an i7-6700 4.00GHz CPU without running operations in parallel (note that the "Properties on Object" column counts only those properties that the delta-object generator does not ignore):
| Number of Objects | Properties on Object | Seconds to Generate Delta Objects |
|---|---|---|
| 1000 | 4 | 0.011 |
| 1 million | 4 | 3.150 |
| 1 million | 20 | 11.857 |