Dale
Dale

Reputation: 353

C# IsEqual with ignorable list

I have some classes with lots of simple properties (from a datamodel I have no control over) -- I'd like to be able to find if the new version of an object is the same as an old version, but don't want to do 20 different "IsEqual" methods (I don't really like the "IsEqual" name because it is not an analog to ==). Another wrinkle, in most of the cases I don't want it to do a deep compare, but in some cases I do want that.

I'd like something along the lines of:

//Property could be PropertyInfo if that is necessary
bool IsEqual<T>(T first, T second, List<Property> ignorableProperties=emptyList, bool recurse=false)
{
    //the comparison code returning if they are equal ignoring 
    //the properties in the ignorableProperties list, recursing if recurse == true
    //not sure how I'd handle the comparison of sub-objects in the recursive step.
}

Upvotes: 0

Views: 409

Answers (3)

Shimmy Weitzhandler
Shimmy Weitzhandler

Reputation: 104781

public static bool AreEqual<T>(this T first, T second, 
  bool recurse = false, params string[] propertiesToSkip)
{
  if (Equals(first, second)) return true;

  if (first == null)
    return second == null;
  else if (second == null)
    return false;

  if (propertiesToSkip == null) propertiesToSkip = new string[] { };
  var properties = from t in first.GetType().GetProperties()
                   where t.CanRead
                   select t;

  foreach (var property in properties)
  {
    if (propertiesToSkip.Contains(property.Name)) continue;

    var v1 = property.GetValue(first, null);
    var v2 = property.GetValue(second, null);

    if (recurse)
      if (!AreEqual(v1, v2, true, propertiesToSkip))
        return false;
      else
        continue;

    if (!Equals(v1, v2)) return false;
  }
  return true;
}

Upvotes: 1

Alex Norcliffe
Alex Norcliffe

Reputation: 2489

Here's how we achieve this in the Umbraco Framework, with a base class called AbstractEquatableObject which is a modified version of Sharp Architecture's BaseObject http://umbraco.codeplex.com/SourceControl/changeset/view/2b4d693de19c#Source%2fLibraries%2fUmbraco.Framework%2fAbstractEquatableObject.cs

Implementors override GetMembersForEqualityComparison() and the base class caches the PropertyInfo objects once per Type for the application in a ConcurrentDictionary<Type, IEnumerable<PropertyInfo>>.

I've pasted the class here, although it refers to LogHelper elsewhere in the Framework so you can remove that (or just use our Framework lib, there's other useful stuff in there).

If you want a helper for getting a PropertyInfo from an expression, to avoid magic strings all over the place (e.g. replaced with x => x.MyProperty), check out the GetPropertyInfo methods of our ExpressionHelper at http://umbraco.codeplex.com/SourceControl/changeset/view/2b4d693de19c#Source%2fLibraries%2fUmbraco.Framework%2fExpressionHelper.cs

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Umbraco.Framework.Diagnostics;

namespace Umbraco.Framework
{
    /// <summary>
    /// Objects implementing <see cref="AbstractEquatableObject{T}"/> are provided with common functionality for establishing domain-specific equality
    /// and a robust implementation of GetHashCode
    /// </summary>
    /// <typeparam name="T"></typeparam>
    [Serializable]
    public abstract class AbstractEquatableObject<T> where T : AbstractEquatableObject<T>
    {
        /// <summary>
        /// Returns the real type in case the <see cref="object.GetType" /> method has been proxied.
        /// </summary>
        /// <returns></returns>
        /// <remarks></remarks>
        protected internal virtual Type GetNativeType()
        {
            // Returns the real type in case the GetType method has been proxied
            // See http://groups.google.com/group/sharp-architecture/browse_thread/thread/ddd05f9baede023a for clarification
            return GetType();
        }

        /// <summary>Returns a hash code for this instance.</summary>
        /// <returns>A hash code for this instance, suitable for use in hashing algorithms and data structures like a hash table. </returns>
        public override int GetHashCode()
        {
            unchecked
            {
                // Based on an algorithm set out at http://sharp-architecture.googlecode.com/svn/trunk/src/SharpArch/SharpArch.Core/DomainModel/BaseObject.cs

                var naturalIdMembers = EnsureEqualityComparisonMembersCached();

                // It's possible for two objects to return the same hash code based on 
                // identically valued properties, even if they're of two different types, 
                // so we include the object's type in the hash calculation
                var hashCode = GetType().GetHashCode();

                if (!naturalIdMembers.Any()) return base.GetHashCode();

                foreach (var value in naturalIdMembers
                    .Select(x => x.GetValue(this, null))
                    .Where(x => !ReferenceEquals(x, null)))
                {
                    // Check if the property value is null or default (e.g. Guid.Empty)
                    // In which case we just want to use the base GetHashCode because we have no other way
                    // of determining if the instances are different
                    if (value.Equals(value.GetType().GetDefaultValue()))
                        hashCode = (hashCode * 41) ^ base.GetHashCode();
                    else
                        hashCode = (hashCode * 41) ^ value.GetHashCode();
                }

                return hashCode;
            }
        }

        /// <summary>Determines whether the specified object is equal to this instance.</summary>
        /// <param name="obj">The <see cref="System.Object"/> to compare with this instance.</param>
        /// <returns><c>true</c> if the specified <see cref="System.Object"/> is equal to this instance; otherwise, <c>false</c>.</returns>
        public override bool Equals(object obj)
        {
            if (ReferenceEquals(obj, null)) return false;
            var incoming = obj as AbstractEquatableObject<T>;
            if (ReferenceEquals(incoming, null)) return false;
            if (ReferenceEquals(this, incoming)) return true;


            // (APN Oct 2011) Disabled the additional check for GetNativeType().Equals(incoming.GetNativeType())
            // so that we can compare RelationById with Relation using Equals however this may need reinstating
            // and using IComparable instead
            return CompareCustomEqualityMembers(incoming);
        }

        /// <summary>
        /// Implements the operator ==.
        /// </summary>
        /// <param name="left">The left.</param>
        /// <param name="right">The right.</param>
        /// <returns>The result of the operator.</returns>
        /// <remarks></remarks>
        public static bool operator ==(AbstractEquatableObject<T> left, AbstractEquatableObject<T> right)
        {
            // If both are null, or both are same instance, return true.
            if (ReferenceEquals(left, right)) return true;

            // If one is null, but not both, return false.
            if (((object)left == null) || ((object)right == null)) return false;

            return left.Equals(right);
        }

        /// <summary>
        /// Implements the operator !=.
        /// </summary>
        /// <param name="left">The left.</param>
        /// <param name="right">The right.</param>
        /// <returns>The result of the operator.</returns>
        /// <remarks></remarks>
        public static bool operator !=(AbstractEquatableObject<T> left, AbstractEquatableObject<T> right)
        {
            return !(left == right);
        }

        /// <summary>
        /// A static <see cref="ConcurrentDictionary{Type, IEnumerable{PropertyInfo}}"/> cache of natural ids for types which may implement this abstract class.
        /// </summary>
        protected readonly static ConcurrentDictionary<Type, IEnumerable<PropertyInfo>> EqualityComparisonMemberCache = new ConcurrentDictionary<Type, IEnumerable<PropertyInfo>>();

        /// <summary>
        /// Gets the natural id members.
        /// </summary>
        /// <returns></returns>
        /// <remarks></remarks>
        protected abstract IEnumerable<PropertyInfo> GetMembersForEqualityComparison();

        /// <summary>
        /// Ensures the natural id members are cached in the static <see cref="EqualityComparisonMemberCache"/>.
        /// </summary>
        /// <returns></returns>
        /// <remarks></remarks>
        protected internal virtual IEnumerable<PropertyInfo> EnsureEqualityComparisonMembersCached()
        {
            return EqualityComparisonMemberCache.GetOrAdd(GetNativeType(), x => GetMembersForEqualityComparison());
        }

        /// <summary>
        /// Establishes if the natural id of this instance matches that of <paramref name="compareWith"/>
        /// </summary>
        /// <param name="compareWith">The instance with which to compare.</param>
        /// <returns></returns>
        /// <remarks></remarks>
        protected internal virtual bool CompareCustomEqualityMembers(AbstractEquatableObject<T> compareWith)
        {
            // Standard input checks - if it's the same instance, or incoming is null, etc.
            if (ReferenceEquals(this, compareWith)) return true;
            if (ReferenceEquals(compareWith, null)) return false;

            // Get the natural id spec
            var naturalIdMembers = EnsureEqualityComparisonMembersCached();

            // If the overriding objct hasn't specified a natural id, just return the base Equals implementation
            if (!naturalIdMembers.Any()) return base.Equals(compareWith);

            // We have a natural id specified, so compare the members
            foreach (var naturalIdMember in naturalIdMembers)
            {
                try
                {
                    // Get the property values of this instance and the incoming instance
                    var localValue = naturalIdMember.GetValue(this, null);
                    var incomingValue = naturalIdMember.GetValue(compareWith, null);

                    // If the property values refere to the same instance, or both refer to null, continue the loop
                    if (ReferenceEquals(localValue, incomingValue) || (ReferenceEquals(localValue, null) && ReferenceEquals(incomingValue, null)))
                        continue;

                    // If this property value doesn't equal the incoming value, the comparison fails so we can return straight away
                    if (!localValue.Equals(incomingValue)) return false;
                }
                catch (Exception ex)
                {
                    // If there was an error accessing one of the properties, log it and return false
                    LogHelper.TraceIfEnabled<AbstractEquatableObject<T>>("Error comparing {0} to {1}: {2}",
                                                                         () => GetNativeType().Name,
                                                                         () => compareWith.GetNativeType().Name,
                                                                         () => ex.Message);
                    return false;
                }
            }

            // To get this far means we haven't had any misses, so return true
            return true;
        }
    }
}

Upvotes: 1

mellamokb
mellamokb

Reputation: 56779

Here's something along those lines from our code base. It uses a list of properties to compare, rather than a list of properties to ignore. Then it returns a list of which properties did not match:

public static List<PropertyInfo> CompareObjects<T>(T o1, T o2, List<PropertyInfo> props) where T : class
{
    var type = typeof(T);
    var mismatched = CompareObjects(type, o1, o2, props);
    return mismatched;
}

public static List<PropertyInfo> CompareObjects(Type t, object o1, object o2, List<PropertyInfo> props)
{
    List<PropertyInfo> mismatched = null;
    foreach (PropertyInfo prop in props)
    {
        if (prop.GetValue(o1, null) == null && prop.GetValue(o2, null) == null) ;
        else if (
            prop.GetValue(o1, null) == null || prop.GetValue(o2, null) == null ||
                !prop.GetValue(o1, null).Equals(prop.GetValue(o2, null)))
        {
            if (mismatched == null) mismatched = new List<PropertyInfo>();
            mismatched.Add(prop);
        }
    }
    return mismatched;
}

If you wanted an IsEqual method it would be a matter of returning true/false when you find mismatched properties instead.

Hope this helps!

Upvotes: 1

Related Questions