-
Notifications
You must be signed in to change notification settings - Fork 1
Home
Welcome! This is a library capable of comparing objects of the same type or entirely different types in a property-per-property basis in order to obtain inequality information (less than, equal, or greater than). The library allows custom property mapping as well as custom IComparer implementations for property data types. It also allows to ignore a property for comparison purposes under a variety of scenarios.
It is best to understand the basic process before talking about this step. Kindly move to the next step and once you have understood how this library works, come back and read more about Attributed Configuration.
Property discovery is based on Reflection. Since reflection is costly, this library will not execute reflection-based code automatically. You are in control of what gets scanned (see below) and when. This is the first step, and it is mandatory.
There are three ways to trigger scanning. Scanning, also known as Registration, is the process of obtaining property information for a specific data type (like a class or a struct). An object cannot be part of the comparison process unless its data type has been scanned. Scanning is thread-safe and holds type information during the entire life cycle of an application. It is also usually performed at the beginning of the application, but of course, you, as the developer, are free to place the calls anywhere you see fit, as long as they take place before object comparison.
Scanning will pick up the following pieces of information:
- A list of properties defined in the data type.
- A list of property maps specified for any properties marked with the
PropertyMapAttributeattibute. - An "Ignore Property" option per property marked with the
IgnoreForComparisonAttributeattribute. - Other useful property data, such as the property's return type.
Once scanning has taken place for all involved data types, comparison can take place.
Scanning, or Registration, takes place when:
- A data type is explicitly registered with the scanner
- A data type is configured via code using fluent interface type configuration
- A comparer configuration object is used to configure a comparer object locally using fluent interface comparer configuration
This step is optional, but rewarding. As mentioned in the Introduction, property comparison yields inequality results. What does this mean? It means that the property values are compared to one another in a way that it is possible to know not only that values are equal or not, but if one value is considered to be less (or greater) than the other. This is possible because property value comparison is done using implementations of the IComparer interface.
There are two ways to provide this functionality to user-defined property data types: The data type may implement IComparable, or a custom IComparer object may be provided. You can read all about this topic in the Property Value Comparison Customization page.
Once the data types involved in object comparison have been scanned, it is time to perform object comparison. This is a very simple process. You merely create an instance of the ObjectComparer class, specifying the data types of the objects to be compared. You can then call any of the Compare() overloads, depending on your needs.
The comparison routine produces a Boolean value that summarizes the comparison: It will be True if at least one property value comparison yielded a ComparisonResult.NotEqual result; otherwise, the value will be False.
Additionally, the Compare() overloads are capable of returning a collection of PropertyComparisonResult objects. There will be one of said objects per property found in the object passed as first object in the call to the Compare() method. This first object is referred to as the source object; the second object is referred to as the target object. Properties found in the target object that do not have a match in the source object will not produce one of these objects.
Each of these result objects contain relevant information about how the comparison was made, including any property maps used, and a Result property containing the most important piece of data: How the property values compared to one another. To find out more about how to interpret this value, refer to the Property Comparison Result Interpretation page.
This example is a unit test that aims to ensure that the object mapper used in an application does not miss any properties. The cool thing about this is that properties do not need to be added or removed to or from the list of assertions as the application evolves. Type scanning will always pick up all the latest property set for both data types. This means that the unit test automatically updates itself to include all the latest property name and type modifications, additions and deletions.
[Test]
[Description("Makes sure the Person ViewModel is properly filled by, say, AutoMapper.")]
public void TestPersonDtoToPersonVMMapping()
{
//Arrange.
Scanner.RegisterType<PersonDto>();
Scanner.RegisterType<PersonVM>();
PersonDto dto = DtoHelpers.CreatePersonDto();
ObjectComparer oc = ObjectComparer.Create<PersonVM, PersonDto>();
//Load the application's automatic object mapper configuration.
LoadAutoMapperConfig();
//Create the automapper object.
var autoMapper = CreateAutoMapper();
//Act.
PersonVM vm = autoMapper.Map<PersonVM>(dto);
//Assert.
bool isDifferent = oc.Compare(vm, dto);
isDifferent.Should().BeFalse(); //FluentAssertions is being used here.
}This example shows how individual property comparison results can be used track changes of a database record (represented in a Model object) in a RESTful web service. The record, just for fun, will be a purchase order. The model has a Total property that represents the total amount of money the purchase order is worth. If the amount is increasing, then an approval email is sent and the order's Status property is set to Waiting for Manager's Approval.
public IActionResult SavePurchaseOrder(PurchaseOrder order, CancellationToken cancelToken)
{
//Get the currently-saved version of the order.
PurchaseOrder currentState = Repository.GetPurchaseOrder(order.Id);
//Compare the incoming order with the current state.
ObjectComparer oc = ObjectComparer.Create<PurchaseOrder>();
var results = oc.Compare(order, currentState, out bool isDifferent);
if (isDifferent)
{
//Log the changes.
foreach (PropertyComparisonResult pcr in results)
{
Logger.Information($"{pcr}"); //PropertyComparisonResult has a nice custom ToString().
}
if (results[nameof(PurchaseOrder.Total)].Result == ComparisonResult.GreaterThan)
{
//The total amount of the purchase order increased. Request manager's approval.
order.Status = OrderStatus.AwaitingManagersApproval;
SendManagerApprovalEmail(order, results);
}
//Save the record.
Repository.SavePurchaseOrder(order);
}
return Response.Ok(order);
}If you would like to know more about property comparison results, see this wiki page.
It is usually always necessary to customize property comparison. Customization allows you to either map a property to another of a different name, or to ignore a property. There is more than one way to do this:
- Attributed Configuration
- Fluent Interface Type Configuration
- Fluent Interface Comparer Configuration
You can also control the implementation of the IComparer object being used for a specific property type. See Property Value Comparison Customization.