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); }