@@ -10,8 +10,14 @@ public static class ObjectPath
1010 {
1111 private static readonly char [ ] Separator = new [ ] { '.' , '[' , ']' } ;
1212
13+ /// <summary>
14+ /// Maximum number of entries in each cache. When exceeded, oldest entries are removed.
15+ /// </summary>
16+ private const int MaxCacheSize = 1000 ;
17+
1318 private static readonly ConcurrentDictionary < ( Type , string , bool ) , PropertyInfo ? > PropertyCache = new ( ) ;
1419 private static readonly ConcurrentDictionary < ( Type , string , bool ) , FieldInfo ? > FieldCache = new ( ) ;
20+ private static readonly ConcurrentDictionary < Type , DictionaryTypeInfo ? > DictionaryTypeCache = new ( ) ;
1521
1622 public static object ? GetValue ( object ? obj , string path , bool ignoreCase = true )
1723 {
@@ -261,45 +267,34 @@ public static bool TryGetValue<T>(object? obj, string path, out T? value, bool i
261267 throw new InvalidObjectPathException ( $ "Property '{ currentSegment } ' not found in path '{ fullPath } '.") ;
262268 }
263269
264- // Handle generic IDictionary<string, T> via reflection
270+ // Handle generic IDictionary<string, T> via cached reflection
265271 var objType = obj . GetType ( ) ;
266- var dictionaryInterface = objType . GetInterfaces ( )
267- . FirstOrDefault ( i => i . IsGenericType &&
268- i . GetGenericTypeDefinition ( ) == typeof ( IDictionary < , > ) &&
269- i . GetGenericArguments ( ) [ 0 ] == typeof ( string ) ) ;
270-
271- if ( dictionaryInterface != null )
272+ var dictTypeInfo = GetCachedDictionaryTypeInfo ( objType ) ;
273+
274+ if ( dictTypeInfo != null )
272275 {
273- // Use reflection to access the dictionary
274- var tryGetValueMethod = dictionaryInterface . GetMethod ( "TryGetValue" ) ;
275- var keysProperty = dictionaryInterface . GetProperty ( "Keys" ) ;
276- var indexer = dictionaryInterface . GetProperty ( "Item" ) ;
277-
278- if ( tryGetValueMethod != null && indexer != null )
276+ // Try exact key match
277+ var parameters = new object ? [ ] { currentSegment , null } ;
278+ var found = ( bool ) dictTypeInfo . TryGetValueMethod . Invoke ( obj , parameters ) ! ;
279+ if ( found )
279280 {
280- // Try exact key match
281- var parameters = new object ? [ ] { currentSegment , null } ;
282- var found = ( bool ) tryGetValueMethod . Invoke ( obj , parameters ) ! ;
283- if ( found )
284- {
285- return parameters [ 1 ] ;
286- }
287-
288- // Try case-insensitive match if enabled
289- if ( ignoreCase && keysProperty != null )
281+ return parameters [ 1 ] ;
282+ }
283+
284+ // Try case-insensitive match if enabled
285+ if ( ignoreCase && dictTypeInfo . KeysProperty != null )
286+ {
287+ var keys = ( System . Collections . IEnumerable ) dictTypeInfo . KeysProperty . GetValue ( obj ) ! ;
288+ foreach ( string key in keys )
290289 {
291- var keys = ( System . Collections . IEnumerable ) keysProperty . GetValue ( obj ) ! ;
292- foreach ( string key in keys )
290+ if ( string . Equals ( key , currentSegment , StringComparison . OrdinalIgnoreCase ) )
293291 {
294- if ( string . Equals ( key , currentSegment , StringComparison . OrdinalIgnoreCase ) )
295- {
296- return indexer . GetValue ( obj , new object [ ] { key } ) ;
297- }
292+ return dictTypeInfo . IndexerProperty . GetValue ( obj , new object [ ] { key } ) ;
298293 }
299294 }
300-
301- throw new InvalidObjectPathException ( $ "Property '{ currentSegment } ' not found in path '{ fullPath } '.") ;
302295 }
296+
297+ throw new InvalidObjectPathException ( $ "Property '{ currentSegment } ' not found in path '{ fullPath } '.") ;
303298 }
304299
305300 // Handle regular objects (properties and fields)
@@ -323,6 +318,7 @@ public static bool TryGetValue<T>(object? obj, string path, out T? value, bool i
323318 var key = ( type , propertyName , ignoreCase ) ;
324319 if ( ! PropertyCache . TryGetValue ( key , out var propertyInfo ) )
325320 {
321+ TrimCacheIfNeeded ( PropertyCache ) ;
326322 var flags = ignoreCase
327323 ? BindingFlags . Public | BindingFlags . Instance | BindingFlags . IgnoreCase
328324 : BindingFlags . Public | BindingFlags . Instance ;
@@ -337,6 +333,7 @@ public static bool TryGetValue<T>(object? obj, string path, out T? value, bool i
337333 var key = ( type , fieldName , ignoreCase ) ;
338334 if ( ! FieldCache . TryGetValue ( key , out var fieldInfo ) )
339335 {
336+ TrimCacheIfNeeded ( FieldCache ) ;
340337 var flags = ignoreCase
341338 ? BindingFlags . Public | BindingFlags . Instance | BindingFlags . IgnoreCase
342339 : BindingFlags . Public | BindingFlags . Instance ;
@@ -346,6 +343,34 @@ public static bool TryGetValue<T>(object? obj, string path, out T? value, bool i
346343 return fieldInfo ;
347344 }
348345
346+ private static DictionaryTypeInfo ? GetCachedDictionaryTypeInfo ( Type type )
347+ {
348+ if ( ! DictionaryTypeCache . TryGetValue ( type , out var typeInfo ) )
349+ {
350+ TrimCacheIfNeeded ( DictionaryTypeCache ) ;
351+
352+ var dictionaryInterface = type . GetInterfaces ( )
353+ . FirstOrDefault ( i => i . IsGenericType &&
354+ i . GetGenericTypeDefinition ( ) == typeof ( IDictionary < , > ) &&
355+ i . GetGenericArguments ( ) [ 0 ] == typeof ( string ) ) ;
356+
357+ if ( dictionaryInterface != null )
358+ {
359+ var tryGetValueMethod = dictionaryInterface . GetMethod ( "TryGetValue" ) ;
360+ var keysProperty = dictionaryInterface . GetProperty ( "Keys" ) ;
361+ var indexer = dictionaryInterface . GetProperty ( "Item" ) ;
362+
363+ if ( tryGetValueMethod != null && indexer != null )
364+ {
365+ typeInfo = new DictionaryTypeInfo ( tryGetValueMethod , keysProperty , indexer ) ;
366+ }
367+ }
368+
369+ DictionaryTypeCache [ type ] = typeInfo ;
370+ }
371+ return typeInfo ;
372+ }
373+
349374 private static bool TryGetPropertyIgnoreCase ( JsonElement jsonElement , string propertyName , out JsonElement jsonProperty )
350375 {
351376 foreach ( var property in jsonElement . EnumerateObject ( ) )
@@ -411,5 +436,48 @@ private static object GetJsonNumber(JsonElement jsonElement)
411436 // Last resort: decimal for very large/precise numbers
412437 return jsonElement . GetDecimal ( ) ;
413438 }
439+
440+ /// <summary>
441+ /// Trims a cache if it exceeds the maximum size by removing approximately half of the entries.
442+ /// </summary>
443+ private static void TrimCacheIfNeeded < TKey , TValue > ( ConcurrentDictionary < TKey , TValue > cache ) where TKey : notnull
444+ {
445+ if ( cache . Count > MaxCacheSize )
446+ {
447+ // Remove approximately half of the entries to avoid frequent trimming
448+ var keysToRemove = cache . Keys . Take ( cache . Count / 2 ) . ToList ( ) ;
449+ foreach ( var key in keysToRemove )
450+ {
451+ cache . TryRemove ( key , out _ ) ;
452+ }
453+ }
454+ }
455+
456+ /// <summary>
457+ /// Clears all internal caches. Useful for testing or when memory pressure is detected.
458+ /// </summary>
459+ public static void ClearCaches ( )
460+ {
461+ PropertyCache . Clear ( ) ;
462+ FieldCache . Clear ( ) ;
463+ DictionaryTypeCache . Clear ( ) ;
464+ }
465+ }
466+
467+ /// <summary>
468+ /// Cached reflection information for dictionary types.
469+ /// </summary>
470+ internal sealed class DictionaryTypeInfo
471+ {
472+ public MethodInfo TryGetValueMethod { get ; }
473+ public PropertyInfo ? KeysProperty { get ; }
474+ public PropertyInfo IndexerProperty { get ; }
475+
476+ public DictionaryTypeInfo ( MethodInfo tryGetValueMethod , PropertyInfo ? keysProperty , PropertyInfo indexerProperty )
477+ {
478+ TryGetValueMethod = tryGetValueMethod ;
479+ KeysProperty = keysProperty ;
480+ IndexerProperty = indexerProperty ;
481+ }
414482 }
415483}
0 commit comments