Table of Contents
Introduction
When dealing with data, you are likely to encounter situations where you need to compare two sets of data. A common example is when an object is updated, you may need to determine what, if anything, has actually been changed from the original.
Sometimes a straight 1-1 comparison isn't suitable - maybe if both objects are technically the same based on their values, but have different Id
values - a comparison in this situation would fail when you might want or expect it to pass.
Comparing objects property by property can work well enough in small volumes, but can become quite cumbersome if there are a lot of them. Additionally, if the comparison is used in various places across the codebase you could end up creating large blocks of duplicate comparison code, thus breaking away from the principle of DRY (Don't Repeat Yourself).
In this article we'll briefly cover how to write custom comparison logic using the .NET IEqualityComparer
interface, and how it can be implemented to handle list comparisons in a single line of code.
Equality comparison in .NET
C# and .NET come with a few common comparison operators, which developers from almost language or framework will be familiar with, such as:
bool obj1 == obj2;
bool obj1 is obj2;
bool obj1.Equals(obj2);
There are also several more specific methods available for higher-level comparisons, such as:
bool type1.IsAssignableFrom(type2)
bool type1.IsEquivalentTo(type2)
bool type1.IsInstanceOfType(obj1)
Each method compares two values, but in different ways. Whether that be by value or memory reference, or by type reflection.
Creating a custom equality comparer
.NET provides anĀ IEqualityComparer
interface as part of the System.Collections.Generic
namespace, defining two methods which can be implemented to create your own custom equality comparison logic.
Assuming our class definition is as follows:
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public float Price { get; set; }
}
we will define a class which compares the values between two separate Product
lists, which will only flag as different if the Name
or Description
values have changed.
Create a new class called ProductComparer
, and implement the IEqualityComparer
interface, specifying your Product
as the generic type.
Implementing the Equals method
public class ProductComparer : IEqualityComparer<Product>
{
public bool Equals(Product? x, Product? y)
{
if (ReferenceEquals(x, y))
{
return true;
}
if (x is null || y is null)
{
return false;
}
return x.Name == y.Name && x.Description == y.Description;
}
}
Implementing the first method, Equals
, our first condition will initially check whether two objects reference the same object in memory. This means that the check will be marked as equal if both items literally reference the same item, and doesn't need to drill down into the property values.
It should be noted that if both objects are null, they will be classed as equal by default. If this isn't the desired result, the second condition covers this by asserting that two nulls do not class as equal - this may or may not be suitable for your situation, but just something to consider.
Finally, the third condition checks the two properties we actually want to compare, Name
and Description
, in this case two strings, and returns the boolean result of that expression.
Implementing the GetHashCode method
The GetHashCode
check is a higher level comparison than Equals
, which is used to quickly determine inequality. This action is performed first for performance reasons, then the latter delves deeper into the object to determine definite equality.
The implementation logic is fairly similar, albeit using numerical comparisons:
public int GetHashCode([DisallowNull] Product obj)
{
if (obj is null)
{
return 0;
}
var hashProductName = obj.Name == null ? 0 : obj.Name.GetHashCode();
var hashProductDescription = obj.Description == null ? 0 : obj.Description.GetHashCode();
return hashProductName ^ hashProductDescription;
}
The method returns an integer representing the result of the comparison between the two hashes. You can have a read of Microsoft C# documentation for more information on the caret operator, but in summary:
The result of hashProductName ^ hashProductDescription
is true if hashProductName
evaluates to true and hashProductDescription
evaluates to false
OR
hashProductName
evaluates to false and hashProductDescription
evaluates to true. Otherwise, the result is false.
Implementing the comparer in your code
Providing you have two lists, for example:
var originalProducts = new List<Product>()
{
new() { Id = 1, Name = 'ProductName1', Description = 'ProductDescription1', Price = 199.99 },
new() { Id = 2, Name = 'ProductName2', Description = 'ProductDescription2', Price = 29.99 },
}
var updatedProducts = new List<Product>()
{
new() { Id = 1, Name = 'ProductName1', Description = 'UpdatedProductDescription1', Price = 199.99 },
new() { Id = 2, Name = 'UpdatedProductName2', Description = 'ProductDescription2', Price = 29.99 },
}
the custom comparer can be implemented with a single line of code using IEnumerable.SequenceEqual
:
var productDetailsHaveChanged = !updatedProducts.SequenceEqual(originalProducts, new ProductComparer()); // true