Skip to content

Instantly share code, notes, and snippets.

@dbones
Created August 14, 2011 19:00
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save dbones/1145185 to your computer and use it in GitHub Desktop.
Save dbones/1145185 to your computer and use it in GitHub Desktop.
Compares 2 objects to see if they are the same or have the same values, tries to handle circluar references (not fully tested, written in a rush)
/// <summary>
/// Compares 2 objects to see if they are the same or have the same values
/// </summary>
public class ObjectComparer
{
private readonly IList<object> _compared;
private readonly IDictionary<Type,IList<string>> _skipList;
public ObjectComparer()
{
_compared = new List<object>();
_skipList = new Dictionary<Type, IList<string>>();
IncludeCollections = true;
IncludeFields = true;
IncludeProperties = true;
IncludeDictionaries = true;
}
public virtual bool IncludeProperties{ get; set; }
public virtual bool IncludeFields { get; set; }
public virtual bool IncludeCollections { get; set; }
public virtual bool IncludeDictionaries { get; set; }
/// <summary>
/// add fields to skip (remember if you use a property to return a field, it must be skipped too)
/// </summary>
/// <typeparam name="T">the object type the field is located</typeparam>
/// <param name="ignoreList">the fields to skip</param>
public virtual void SkipField<T>(params string[] ignoreList)
{
AddToSkipList<T>(ignoreList);
}
/// <summary>
/// add properties to skip (remember to skip the field, if its not a auto property)
/// </summary>
/// <typeparam name="T">the object type the properties is located</typeparam>
/// <param name="ignoreList">the properties to skip</param>
public virtual void SkipProperty<T>(params string[] ignoreList)
{
AddToSkipList<T>(ignoreList);
string[] ignoreBackingFields = ignoreList.Select(item => string.Format("<{0}>k__BackingField", item)).ToArray();
AddToSkipList<T>(ignoreBackingFields);
}
/// <summary>
/// add a property to skip (remember to skip the field, if its not a auto property)
/// </summary>
/// <typeparam name="T">the object type the property is located</typeparam>
/// <param name="memberExpression">the property to skip</param>
public virtual void SkipProperty<T>(Expression<Func<T, object>> memberExpression)
{
string name = ((MemberExpression)((UnaryExpression)memberExpression.Body).Operand).Member.Name;
SkipProperty<T>(name);
}
protected virtual void AddToSkipList<T>(params string[] ignoreList)
{
Type type = typeof (T);
if (_skipList.ContainsKey(type))
{
IList<string> list = _skipList[type];
foreach (var s in ignoreList)
{
list.Add(s);
}
}
else
{
_skipList.Add(type, new List<string>(ignoreList));
}
}
/// <summary>
/// pass in 2 objects, will state if they are the same
/// please also set any items to skip before running this.
/// </summary>
/// <returns>true if they are the same</returns>
public virtual bool Compare(object a, object b)
{
_compared.Clear();
return Run(a, b);
}
protected virtual bool Run(object a, object b)
{
//note this as compaired! try to resolve the circular reference
if (a != null && a.GetType().IsClass && _compared.Contains(a)) return true;
_compared.Add(a);
//they are the same object via the == operator
if (a == b) return true;
if (a.Equals(b)) return true;
Type aType = a.GetType();
Type bType = b.GetType();
if (!aType.IsAssignableFrom(bType)) return false;
//the == and Equals() should have sufficed for a value type
if (!aType.IsClass || a is string) return false;
//check to see if the item is a dictionary
IDictionary aDictionary = a as IDictionary;
IDictionary bDictionary = b as IDictionary;
if (IncludeDictionaries && aDictionary != null) return CompareDictionary(aDictionary, bDictionary);
//check collection
IEnumerable aEnumerable = a as IEnumerable;
IEnumerable bEnumerable = b as IEnumerable;
if (IncludeCollections && aEnumerable != null) return CompareCollection(aEnumerable, bEnumerable);
//check fields
if (IncludeFields)
{
FieldInfo[] fields = aType.GetFields(
BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
foreach (var fieldInfo in fields)
{
if(ToSkip(aType, fieldInfo.Name)) continue;
object aValue = fieldInfo.GetValue(a);
object bValue = fieldInfo.GetValue(b);
bool areEqual = Run(aValue, bValue);
if (!areEqual) return false;
}
}
//check properties
if (IncludeProperties)
{
PropertyInfo[] properties = aType.GetProperties(
BindingFlags.Instance | BindingFlags.GetProperty | BindingFlags.NonPublic | BindingFlags.Public);
foreach (var propertyInfo in properties)
{
if (ToSkip(aType, propertyInfo.Name)) continue;
MethodInfo methodInfo = propertyInfo.GetGetMethod(true);
if (methodInfo == null) continue;
object aValue = methodInfo.Invoke(a, null);
object bValue = methodInfo.Invoke(b, null);
bool areEqual = Run(aValue, bValue);
if (!areEqual) return false;
}
}
return true;
}
protected virtual bool CompareDictionary(IDictionary a, IDictionary b)
{
if (a.Keys.Count != b.Keys.Count) return false;
foreach (object key in a.Keys)
{
if (!b.Contains(key)) return false;
bool areEqual = Run(a[key], b[key]);
if (!areEqual) return false;
}
return true;
}
protected virtual bool CompareCollection(IEnumerable a, IEnumerable b)
{
IEnumerator aEnumerator = a.GetEnumerator();
IEnumerator bEnumerator = b.GetEnumerator();
while (aEnumerator.MoveNext())
{
//not the same length
if (bEnumerator.MoveNext() == false) return false;
bool areEqual = Run(aEnumerator.Current, bEnumerator.Current);
if (!areEqual) return false;
}
//not the same length
if (bEnumerator.MoveNext()) return false;
return true;
}
protected virtual bool ToSkip(Type a, string attribute)
{
if (!IncludeProperties && attribute.StartsWith("<")) return true; //fix
return _skipList.ContainsKey(a) && _skipList[a].Contains(attribute);
}
}
@dbones
Copy link
Author

dbones commented Aug 14, 2011

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment