How did I do?*

Handling list comparisons in .NET with IEqualityComparer

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