Using custom EqualityComparer to check if C# item has been updated in a list

I am comparing 2 lists of objects using a custom comparer like so:

public class LocationEqualityComparer : IEqualityComparer<LocationData> {     public bool Equals(LocationData x, LocationData y)     {         var idComparer = string.Equals(x.Id, y.Id,              System.StringComparison.OrdinalIgnoreCase);          var nameComparer = string.Equals(x.Name, y.Name,              System.StringComparison.OrdinalIgnoreCase);         var addressComparer = string.Equals(x.Address, y.Address,              System.StringComparison.OrdinalIgnoreCase);         var postcodeComparer = string.Equals(x.PostCode, y.PostCode,              System.StringComparison.OrdinalIgnoreCase);                if (idComparer && nameComparer && addressComparer && postcodeComparer)          {             return true;          }          return false;      } } 

This works great for me when using Linq to check the equality using: If I have two lists of LocationData (previousRun and currentRun, I get the correct result with: List<LocationData> result = previousRun.Intersect(currentRun, new LocationEqualityComparer()).ToList();

I am also able to check which items have been added or deleted between the lists using Except in Linq.

What I want to be able to do is check if an item has been UPDATED between the lists. This is because they represent an old list (previous run) and a new list (current run). So for example the LocationData object will have the same Id, same address and same postcode but might have a slightly different name.

Does anyone know how I can get a list of objects that have been updated between lists (i.e. only one or maybe two properties have changed) but not defined as added or deleted?

Thank you

Add Comment
3 Answer(s)

You could simply write a method that does the property comparison, but which returns true if a specific number of properties match (you said 1 or 2, so I guess it’s variable?):

public static bool IsUpdated(LocationData previous, LocationData current,      int numPropsToMatch = 2) {     // If they are equal, return false     if (new LocationEqualityComparer().Equals(previous, current)) return false;      int numMatchingProps = 0;      if (string.Equals(previous.Id, current.Id,         System.StringComparison.OrdinalIgnoreCase)) numMatchingProps++;          if (string.Equals(previous.Name, current.Name,         System.StringComparison.OrdinalIgnoreCase)) numMatchingProps++;          if (string.Equals(previous.Address, current.Address,         System.StringComparison.OrdinalIgnoreCase)) numMatchingProps++;          if (string.Equals(previous.PostCode, current.PostCode,         System.StringComparison.OrdinalIgnoreCase)) numMatchingProps++;      // Change to == if you *only* want a specific number to match     return numMatchingProps >= numPropsToMatch; } 

Then you can just use this method in your Linq statement:

List<LocationData> updated = currentRun     .Where(curr => previousRun.Any(prev => IsUpdated(prev, curr)))     .ToList(); 

Note that it’s highly likely that more than one location will have the same postal code, so that should probably not be included, but since it wasn’t specified I left it.

Answered on August 31, 2020.
Add Comment

You could say that two things are the same if they are structurally the same, or you could say they are the same if their identifies are the same.

Your comparer basically checks if they are structurally equal. That is, you are comparing all fields to figure out if all properties are the same.

That means you are treating LocationData as a value-object.

However, it has an id, which indicates that it is truly an entity.

Those should be treated differently and you can read more about these concepts here and here.

With the assumption that the "Id" in fact is there to uniquely identify the location, which would be the purpose of an "Id" in most cases, the problem of finding updates becomes trivial.

One way could be with an extension-class such as this:

public static class LocationDataExt {     public static bool IsUpdated(this LocationData previous, LocationData current)     {         if (!string.Equals(previous.Id, current.Id, StringComparison.OrdinalIgnoreCase))             return false;   // it is not updated, because it is not the same entity          // else, return true if any other property has changed         return !string.Equals(previous.Name, current.Name, StringComparison.OrdinalIgnoreCase) ||                 !string.Equals(previous.Address, current.Address, StringComparison.OrdinalIgnoreCase) ||                 !string.Equals(previous.PostCode, current.PostCode, StringComparison.OrdinalIgnoreCase);      } } 

Where you’d use it to find the ones updates like this:

var updated = currentRun             .Where(current => previousRun.Any(previous => previous.IsUpdated(current)))             .ToList(); 
Answered on August 31, 2020.
Add Comment

Well, to get items that were updated between runs you need to write up new IEqualityComparer for such a case. Basically checking that ID is still the same as it was, but anything else may be changed, like Name, Address, and so on. Here is an example of such comparer with a test – works on my side.

public class LocationIdEqualityComparer : IEqualityComparer<LocationData> {     public bool Equals(LocationData x, LocationData y)     {         bool idComparer = string.Equals(x.Id, y.Id,             StringComparison.OrdinalIgnoreCase);         bool nameComparer = string.Equals(x.Name, y.Name,             StringComparison.OrdinalIgnoreCase);         bool addressComparer = string.Equals(x.Address, y.Address,             StringComparison.OrdinalIgnoreCase);         bool postcodeComparer = string.Equals(x.PostCode, y.PostCode,             StringComparison.OrdinalIgnoreCase);                  // so you need to check that ID is the same, but everything else may be different         return idComparer && (!nameComparer || !addressComparer || !postcodeComparer);     }      public int GetHashCode(LocationData obj)     {         return obj.Id.GetHashCode();     } }  class TestUpdatedItemsInList {     [Test]     public void TestItemsAreUpdated()     {         List<LocationData> originalList = new List<LocationData>         {             new LocationData("1", "first", "somewhere1", "postCode1"),             new LocationData("2", "second", "somewhere2", "postCode2"),             new LocationData("3", "third", "somewhere3", "postCode3"),             new LocationData("4", "fourth", "somewhere4", "postCode4"),         };          List<LocationData> updatedList = new List<LocationData>         {             new LocationData("1", "1st", "somewhere1", "postCode1"),             new LocationData("2", "second", "who knows where", "postCode2"),             new LocationData("3", "third", "somewhere3", "updated postCode3"),             new LocationData("4", "fourth", "somewhere4", "postCode4"),             new LocationData("5", "fifth", "somewhere5", "postCode5"),             new LocationData("6", "sixth", "somewhere6", "postCode6"),         };                  // newly added and updated items will end up here         var differentItems = updatedList.Except(originalList, new LocationFullEqualityComparer());         // only updated items will be here         var updatedItems = updatedList.Except(originalList, new LocationIdEqualityComparer());         // only non-changed items will be here (item 4)         var itemsWithoutChanges = updatedList.Intersect(originalList, new LocationFullEqualityComparer());           Assert.That(differentItems, Has.Exactly(5).Items);         Assert.That(updatedItems, Has.Exactly(3).Items);          Assert.That(itemsWithoutChanges, Has.Exactly(1).Items);     } }  public class LocationData {     public LocationData(string id, string name, string address, string postCode)     {         Id = id;         Name = name;         Address = address;         PostCode = postCode;     }      public string Id { get; set; }     public string Name { get; set; }     public string Address { get; set; }     public string PostCode { get; set; }      public override string ToString()     {         return $"{Id}, {Name}, {Address}, {PostCode}";     } }  // code provided by you public class LocationFullEqualityComparer : IEqualityComparer<LocationData> {     public bool Equals(LocationData x, LocationData y)     {         bool idComparer = string.Equals(x.Id, y.Id,             StringComparison.OrdinalIgnoreCase);         bool nameComparer = string.Equals(x.Name, y.Name,             StringComparison.OrdinalIgnoreCase);         bool addressComparer = string.Equals(x.Address, y.Address,             StringComparison.OrdinalIgnoreCase);         bool postcodeComparer = string.Equals(x.PostCode, y.PostCode,             StringComparison.OrdinalIgnoreCase);          return idComparer && nameComparer && addressComparer && postcodeComparer;     }      public int GetHashCode(LocationData obj)     {         return obj.Id.GetHashCode();     } } 
Answered on August 31, 2020.
Add Comment

Your Answer

By posting your answer, you agree to the privacy policy and terms of service.