diff --git a/Compare-NET-Objects/Cache.cs b/Compare-NET-Objects/Cache.cs
index 42399c4..2dfc0fa 100644
--- a/Compare-NET-Objects/Cache.cs
+++ b/Compare-NET-Objects/Cache.cs
@@ -1,219 +1,188 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Reflection;
-
-namespace KellermanSoftware.CompareNetObjects
-{
- ///
- /// Cache for properties, fields, and methods to speed up reflection
- ///
- internal static class Cache
- {
- ///
- /// Reflection Cache for property info
- ///
- private static readonly Dictionary _propertyCache;
-
- ///
- /// Reflection Cache for field info
- ///
- private static readonly Dictionary _fieldCache;
-
- ///
- /// Reflection Cache for methods
- ///
- private static readonly Dictionary _methodList;
-
- ///
- /// Static constructor
- ///
- static Cache()
- {
- _propertyCache = new Dictionary();
- _fieldCache = new Dictionary();
- _methodList = new Dictionary();
- }
-
- ///
- /// Clear the cache
- ///
- public static void ClearCache()
- {
- lock(_propertyCache)
- _propertyCache.Clear();
-
- lock(_fieldCache)
- _fieldCache.Clear();
-
- lock(_methodList)
- _methodList.Clear();
- }
-
- ///
- /// Get a list of the fields within a type
- ///
- ///
- ///
- ///
- public static IEnumerable GetFieldInfo(ComparisonConfig config, Type type)
- {
- lock (_fieldCache)
- {
- bool isDynamicType = TypeHelper.IsDynamicObject(type);
-
- if (config.Caching && _fieldCache.ContainsKey(type))
- return _fieldCache[type];
-
- FieldInfo[] currentFields;
-
-#if !NETSTANDARD1_3
- BindingFlags flags = BindingFlags.Public | BindingFlags.Instance;
- if (config.ComparePrivateFields || isDynamicType)
- {
- flags |= BindingFlags.NonPublic;
- }
- if (config.CompareStaticFields)
- {
- flags |= BindingFlags.Static;
- }
-
- Func fieldIsBacking = field => field.Name.Contains("k__BackingField");
- List list = new List();
- Type t = type;
- do
- {
- foreach (var field in t.GetFields(flags))
- {
- if (!config.ComparePrivateFields)
- {
- list.Add(field);
- }
- else if (config.ComparePrivateFields && !config.CompareBackingFields && !fieldIsBacking(field))
- {
- list.Add(field);
- }
- else if (config.ComparePrivateFields && config.CompareBackingFields)
- {
- list.Add(field);
- }
- else if (isDynamicType)
- {
- list.Add(field);
- }
- }
- t = t.BaseType;
- } while (t != null);
- currentFields = list.ToArray();
-#else
- currentFields = type.GetFields(); //Default is public instance and static
-#endif
- if (config.Caching)
- _fieldCache.Add(type, currentFields);
-
- return currentFields;
- }
- }
-
-
-
- ///
- /// Get the value of a property
- ///
- ///
- ///
- ///
- ///
- ///
- public static object GetPropertyValue(ComparisonConfig config, Type type, object objectValue, string propertyName)
- {
- lock (_propertyCache)
- return GetPropertyInfo(config, type).First(o => o.Name == propertyName).GetValue(objectValue, null);
- }
-
- ///
- /// Get a list of the properties in a type
- ///
- ///
- ///
- ///
- public static IEnumerable GetPropertyInfo(ComparisonConfig config, Type type)
- {
- lock (_propertyCache)
- {
- bool isDynamicType = TypeHelper.IsDynamicObject(type);
-
- if (config.Caching && _propertyCache.ContainsKey(type))
- return _propertyCache[type];
-
- PropertyInfo[] currentProperties;
-
-#if NETSTANDARD1_3
- if (!config.CompareStaticProperties)
- currentProperties = type.GetProperties(BindingFlags.Public | BindingFlags.Instance);
- else
- currentProperties = type.GetProperties(); //Default is public instance and static
-#else
- //All the implementation examples that I have seen for dynamic objects use private fields or properties
- if ((config.ComparePrivateProperties || isDynamicType) && !config.CompareStaticProperties)
- {
- currentProperties =
- type.GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
- }
- else if ((config.ComparePrivateProperties || isDynamicType) && config.CompareStaticProperties)
- {
- currentProperties =
- type.GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.NonPublic |
- BindingFlags.Static);
- }
- else if (!config.CompareStaticProperties)
- {
- currentProperties = type.GetProperties(BindingFlags.Public | BindingFlags.Instance);
- }
- else
- {
- currentProperties = type.GetProperties(); //Default is public instance and static
- }
-#endif
-
- if (config.Caching)
- {
- _propertyCache.Add(type, currentProperties);
- }
-
- return currentProperties;
- }
- }
-
- ///
- /// Get a method by name
- ///
- ///
- ///
- ///
- public static MethodInfo GetMethod(Type type, string methodName)
- {
- lock (_methodList)
- return GetMethods(type).FirstOrDefault(m => m.Name == methodName);
- }
-
- ///
- /// Get the cached methods for a type
- ///
- ///
- ///
- public static IEnumerable GetMethods(Type type)
- {
- lock (_methodList)
- {
- if (_methodList.ContainsKey(type))
- return _methodList[type];
-
- MethodInfo[] myMethodInfo = type.GetMethods();
- _methodList.Add(type, myMethodInfo);
- return myMethodInfo;
- }
- }
-
- }
-}
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+
+namespace KellermanSoftware.CompareNetObjects
+{
+ ///
+ /// Cache for properties, fields, and methods to speed up reflection.
+ /// Uses ConcurrentDictionary for lock-free reads in concurrent scenarios.
+ ///
+ internal static class Cache
+ {
+ ///
+ /// Reflection Cache for property info
+ ///
+ private static ConcurrentDictionary _propertyCache = new ConcurrentDictionary();
+
+ ///
+ /// Reflection Cache for field info
+ ///
+ private static ConcurrentDictionary _fieldCache = new ConcurrentDictionary();
+
+ ///
+ /// Reflection Cache for methods
+ ///
+ private static ConcurrentDictionary _methodList = new ConcurrentDictionary();
+
+ ///
+ /// Clear the cache
+ ///
+ public static void ClearCache()
+ {
+ _propertyCache.Clear();
+ _fieldCache.Clear();
+ _methodList.Clear();
+ }
+
+ ///
+ /// Get a list of the fields within a type
+ ///
+ ///
+ ///
+ ///
+ public static IEnumerable GetFieldInfo(ComparisonConfig config, Type type)
+ {
+ if (config.Caching && _fieldCache.TryGetValue(type, out var cached))
+ return cached;
+
+ bool isDynamicType = TypeHelper.IsDynamicObject(type);
+ FieldInfo[] currentFields;
+
+#if !NETSTANDARD1_3
+ BindingFlags flags = BindingFlags.Public | BindingFlags.Instance;
+ if (config.ComparePrivateFields || isDynamicType)
+ {
+ flags |= BindingFlags.NonPublic;
+ }
+ if (config.CompareStaticFields)
+ {
+ flags |= BindingFlags.Static;
+ }
+
+ Func fieldIsBacking = field => field.Name.Contains("k__BackingField");
+ List list = new List();
+ Type t = type;
+ do
+ {
+ foreach (var field in t.GetFields(flags))
+ {
+ if (!config.ComparePrivateFields)
+ {
+ list.Add(field);
+ }
+ else if (config.ComparePrivateFields && !config.CompareBackingFields && !fieldIsBacking(field))
+ {
+ list.Add(field);
+ }
+ else if (config.ComparePrivateFields && config.CompareBackingFields)
+ {
+ list.Add(field);
+ }
+ else if (isDynamicType)
+ {
+ list.Add(field);
+ }
+ }
+ t = t.BaseType;
+ } while (t != null);
+ currentFields = list.ToArray();
+#else
+ currentFields = type.GetFields(); //Default is public instance and static
+#endif
+ if (config.Caching)
+ _fieldCache.TryAdd(type, currentFields);
+
+ return currentFields;
+ }
+
+
+
+ ///
+ /// Get the value of a property
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public static object GetPropertyValue(ComparisonConfig config, Type type, object objectValue, string propertyName)
+ {
+ return GetPropertyInfo(config, type).First(o => o.Name == propertyName).GetValue(objectValue, null);
+ }
+
+ ///
+ /// Get a list of the properties in a type
+ ///
+ ///
+ ///
+ ///
+ public static IEnumerable GetPropertyInfo(ComparisonConfig config, Type type)
+ {
+ if (config.Caching && _propertyCache.TryGetValue(type, out var cached))
+ return cached;
+
+ bool isDynamicType = TypeHelper.IsDynamicObject(type);
+ PropertyInfo[] currentProperties;
+
+#if NETSTANDARD1_3
+ if (!config.CompareStaticProperties)
+ currentProperties = type.GetProperties(BindingFlags.Public | BindingFlags.Instance);
+ else
+ currentProperties = type.GetProperties(); //Default is public instance and static
+#else
+ //All the implementation examples that I have seen for dynamic objects use private fields or properties
+ if ((config.ComparePrivateProperties || isDynamicType) && !config.CompareStaticProperties)
+ {
+ currentProperties =
+ type.GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
+ }
+ else if ((config.ComparePrivateProperties || isDynamicType) && config.CompareStaticProperties)
+ {
+ currentProperties =
+ type.GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.NonPublic |
+ BindingFlags.Static);
+ }
+ else if (!config.CompareStaticProperties)
+ {
+ currentProperties = type.GetProperties(BindingFlags.Public | BindingFlags.Instance);
+ }
+ else
+ {
+ currentProperties = type.GetProperties(); //Default is public instance and static
+ }
+#endif
+
+ if (config.Caching)
+ {
+ _propertyCache.TryAdd(type, currentProperties);
+ }
+
+ return currentProperties;
+ }
+
+ ///
+ /// Get a method by name
+ ///
+ ///
+ ///
+ ///
+ public static MethodInfo GetMethod(Type type, string methodName)
+ {
+ return GetMethods(type).FirstOrDefault(m => m.Name == methodName);
+ }
+
+ ///
+ /// Get the cached methods for a type
+ ///
+ ///
+ ///
+ public static IEnumerable GetMethods(Type type)
+ {
+ return _methodList.GetOrAdd(type, t => t.GetMethods());
+ }
+
+ }
+}
diff --git a/Compare-NET-Objects/ComparisonConfig.cs b/Compare-NET-Objects/ComparisonConfig.cs
index dbfecec..4f5ed68 100644
--- a/Compare-NET-Objects/ComparisonConfig.cs
+++ b/Compare-NET-Objects/ComparisonConfig.cs
@@ -21,6 +21,7 @@ public class ComparisonConfig
#region Class Variables
private Action _differenceCallback;
private int _maxStructDepth;
+ private int _maxObjectDepth;
#endregion
#region Constructors
@@ -112,6 +113,19 @@ public int MaxStructDepth
}
}
+ ///
+ /// When comparing classes, the maximum depth to compare children. Default is 5. Set to 0 for unlimited.
+ ///
+#if !NETSTANDARD
+ [DataMember]
+#endif
+ public int MaxObjectDepth
+ {
+ get { return _maxObjectDepth; }
+ set { _maxObjectDepth = value; }
+ }
+
+
///
/// If true, unknown object types will be ignored instead of throwing an exception. The default is false.
///
@@ -678,6 +692,7 @@ public void Reset()
CollectionMatchingSpec = new Dictionary>();
IgnoreUnknownObjectTypes = false;
MaxStructDepth = 2;
+ MaxObjectDepth = 5;
CaseSensitive = true;
IgnoreStringLeadingTrailingWhitespace = false;
IgnoreMissingProperties = true;
diff --git a/Compare-NET-Objects/ComparisonResult.cs b/Compare-NET-Objects/ComparisonResult.cs
index 69e7cdc..d711893 100644
--- a/Compare-NET-Objects/ComparisonResult.cs
+++ b/Compare-NET-Objects/ComparisonResult.cs
@@ -37,6 +37,7 @@ public ComparisonResult(ComparisonConfig config)
/// Keep track of the depth of structs being compared. Used with ComparisonConfig.MaxStructDepth
///
internal int CurrentStructDepth { get; set; }
+ internal int CurrentObjectDepth { get; set; }
///
/// Configuration
diff --git a/Compare-NET-Objects/TypeComparers/ClassComparer.cs b/Compare-NET-Objects/TypeComparers/ClassComparer.cs
index 8ea5642..edb481b 100644
--- a/Compare-NET-Objects/TypeComparers/ClassComparer.cs
+++ b/Compare-NET-Objects/TypeComparers/ClassComparer.cs
@@ -39,8 +39,13 @@ public override bool IsTypeMatch(Type type1, Type type2)
///
public override void CompareType(CompareParms parms)
{
+ if (parms.Config.MaxObjectDepth > 0
+ && parms.Result.CurrentObjectDepth >= parms.Config.MaxObjectDepth)
+ return;
+
try
{
+ parms.Result.CurrentObjectDepth++;
parms.Result.AddParent(parms.Object1);
parms.Result.AddParent(parms.Object2);
@@ -72,6 +77,7 @@ public override void CompareType(CompareParms parms)
}
finally
{
+ parms.Result.CurrentObjectDepth--;
parms.Result.RemoveParent(parms.Object1);
parms.Result.RemoveParent(parms.Object2);
}