Comparing Collections of Type BusinessCollectionBase

A while ago (actually a long while ago but I didn't have the time to post this earlier), I was approached by Amanda Myer with a question about comparing and sorting BusinessBase objects in collections that inherit BusinessCollectionBase, discussed in my article series on N-Layer design in .NET 3.5.

I replied with a quick tip on overriding Equals and GetHashCode (both virtual methods on the Object class) but it turned out she was already overriding those but needed more, especially in the area of the collection classes. Some time later I got another reply, with full working code to compare single instances and collections that inherit BusinessBase and BusinessCollectionBase. When you implemented the changes proposed by Amanda, you can write tests like this:

public class Person : BusinessBase
{
  public override int Id { get; set; }
  
  [NotNullOrEmpty(Message = "First name is required.")]
  public string FirstName { get; set; }

  [NotNullOrEmpty(Message = "Last name is required.")]
  public string LastName { get; set; }

  [NotNullOrEmpty(Message = "Email address is required.")]
  [ValidEmail(Message = "A valid e-mail address is required.")]
  public string EmailAddress { get; set; }

  [ValidRange(Message = "Age must be between 0 and 150.", Min = 0, Max = 150)]
  public int Age { get; set; }
}

public class People : BusinessCollectionBase<Person> {}

...

[TestClass]
public class PersonTests
{
  [TestMethod]
  public void PeopleCanBeCompared()
  {
    Person myPerson1 = new Person { FirstName = "Imar", LastName = "Spaanjaars" };
    Person myPerson2 = new Person { FirstName = "Imar", LastName = "Spaanjaars" };
    Assert.IsTrue(myPerson1.Equals(myPerson2));
  }
}

This unit tests creates two instances of the Person class, sets identical properties and then calls Equals to see if they are identical. Normally, this code would fail as the two are not pointing to the same instance of Person and .NET by default determines equality for reference types based on their reference. However, with Amanda's code, this test now passes, because of the following code in BusinessBase:

#region IEquatable

/// <summary>
/// Determines if two objects have the same values in all their
/// public properties and fields.
/// </summary>
public bool Equals(BusinessBase other)
{
  if (other == null) return base.Equals(other);

  // Make sure both classes are of the same type
  Type _t = this.GetType();
  if (!other.GetType().Equals(_t)) return false;

  // Compare all properties and ensure they're the same
  PropertyInfo[] props = _t.GetProperties();
  for (int i = 0; i < props.Length; i++)
  {
    object value1 = props[i].GetValue(this, new object[] { });
    object value2 = props[i].GetValue(other, new object[] { });
    // If both are null, or both are the same instance, skip to the next
    // iteration.
    if (ReferenceEquals(value1, null) && ReferenceEquals(value2, null))
    {
      continue;
    }

    // If one is null, but not both, return false.
    if (ReferenceEquals(value1, null) || ReferenceEquals(value2, null))
    {
      return false;
    }

    // If none are null, compare values.
    if (!value1.Equals(value2))
    {
      return false;
    }
  }
  // Got this far - they must be identical!
  return true;
}

public override bool Equals(Object obj)
{
  if (obj == null) return base.Equals(obj);

  if (!(obj is BusinessBase))
    throw new InvalidCastException(@"The 'obj' argument is 
                       not a BusinessBase object.");
  else
    return Equals(obj as BusinessBase);
}

public override int GetHashCode()
{
  int FinalHashCode = 17;
  int OtherCoPrimeNumber = 37;
  Type _t = this.GetType();
  
  // Compare all properties and ensure they're the same
  PropertyInfo[] props = _t.GetProperties();
  for (int i = 0; i < props.Length; i++)
  {
    object value1 = props[i].GetValue(this, new object[] { });
    // If value is null, skip to the next iteration.
    if (value1 != null)
    {
      FinalHashCode = FinalHashCode * OtherCoPrimeNumber + value1.GetHashCode();
    }
  }
  return FinalHashCode;
}

public static bool operator ==(BusinessBase a, BusinessBase b)
{
  // If both are null, or both are the same instance, return true.
  if (ReferenceEquals(a, null) && ReferenceEquals(b, null))
  {
    return true;
  }

  // If one is null, but not both, return false.
  if (ReferenceEquals(a, null) || ReferenceEquals(b, null))
  {
    return false;
  }
  // If neither is null, call the Equals method to compare properties.
  return a.Equals(b);
}

public static bool operator !=(BusinessBase a, BusinessBase b)
{
  return !(a == b);
} 
#endregion

This code uses reflection to loop through the properties of a BusinessBase instance and compares them one by one with the properties of another instance. Because of the reflection, be careful when using code like this in performance critical applications or in extensive loops.

You'll find similar code in the BusinessCollectionBase, BrokenRule and BrokenRulesCollection classes in the code download that comes with this article. Since the code is added to the collections, you can execute test code as follows:

[TestMethod]
public void CollectionsOfPeopleCanBeCompared()
{
  People people1 = new People();
  People people2 = new People();

  Person myPerson1 = new Person { FirstName = "Imar", LastName = "Spaanjaars" };
  people1.Add(myPerson1);

  Person myPerson2 = new Person { FirstName = "Amanda", LastName = "Myer" };
  people1.Add(myPerson2);

  Person myPerson3 = new Person { FirstName = "Imar", LastName = "Spaanjaars" };
  people2.Add(myPerson3);

  Person myPerson4 = new Person { FirstName = "Amanda", LastName = "Myer" };
  people2.Add(myPerson4);

  Assert.IsTrue(people1.Equals(people2));
}

[TestMethod]
public void CollectionsOfPeopleCanBeComparedRegardlessTheOrderOfItemsInTheCollection()
{
  People people1 = new People();
  People people2 = new People();

  Person myPerson1 = new Person { FirstName = "Imar", LastName = "Spaanjaars" };
  people1.Add(myPerson1);

  Person myPerson2 = new Person { FirstName = "Amanda", LastName = "Myer" };
  people1.Add(myPerson2);

  Person myPerson4 = new Person { FirstName = "Amanda", LastName = "Myer" };
  people2.Add(myPerson4);

  Person myPerson3 = new Person { FirstName = "Imar", LastName = "Spaanjaars" };
  people2.Add(myPerson3);

  Assert.IsTrue(people1.Equals(people2));
}  

Pretty cool, don't you think? Because the collections are sorted internally before they are compared, it doesn't matter in which order the items were added to the collection initially.

If you have the original ContactManager application up and running, all you need to do is drop the attached files in their respective projects in Visual Studio. Otherwise, you may want to run a compare tool on the the original files and the ones you can download below to see what has changed and what you need to copy to your own implementation.

Thanks for the follow up and working code Amanda! I found it very useful and I am sure others will too!

Disclaimer: while I ran a number of unit tests against this new code that all passed, this code hasn't been tested as thoroughly as the other code in the ContactManager application, If you find an issue (a bug, a strange design decision or something else), please let me know.

Download Files

The modified classes (C# only)

Where to Next?

Wonder where to go next? You can post a comment on this article. You can read existing comments below or you can post a comment yourself on this article .


Consider making a donation
Please consider making a donation using PayPal. Your donation helps me to pay the bills so I can keep running Imar.Spaanjaars.Com, providing fresh content as often as possible.



Talk Back! Comment on Imar.Spaanjaars.Com

I am interested in what you have to say about this article. Feel free to post any comments, remarks or questions you may have about this article. The Talk Back feature is not meant for technical questions that are not directly related to this article. So, a post like "Hey, can you tell me how I can upload files to a MySQL database in PHP?" is likely to be removed. Also spam and unrealistic job offers will be deleted immediately.

When you post a comment, you have to provide your name and the comment. Your e-mail address is optional and you only need to provide it if you want me to contact you. It will not be displayed along with your comment. I got sick and tired of the comment spam I was receiving, so I have protected this page with a simple calculation exercise. This means that if you want to leave a comment, you'll need to complete the calculation before you hit the Post Comment button.

If you want to object to a comment made by another visitor, be sure to contact me and I'll look into it ASAP. Don't forget to mention the page link, or the QuickDocId of the document.

For more information about the Talk Back feature, check out this news item.