Created
August 14, 2011 19:00
-
-
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)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/// <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); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
example usage here: http://www.dbones.co.uk/blog/post/2011/8/objectcomparer-testing-class