22// Copyright SmartFormat Project maintainers and contributors.
33// Licensed under the MIT license.
44
5+ using System ;
56using System . Collections ;
7+ using System . Collections . Concurrent ;
68using System . Collections . Generic ;
9+ using System . Reflection ;
710using SmartFormat . Core . Extensions ;
11+ using SmartFormat . Core . Settings ;
812
913namespace SmartFormat . Extensions ;
1014
1115/// <summary>
1216/// Class to evaluate sources of types <see cref="IDictionary"/>,
13- /// generic <see cref="IDictionary{TKey,TValue}"/> and dynamic <see cref="System.Dynamic.ExpandoObject"/>.
17+ /// generic <see cref="IDictionary{TKey,TValue}"/>, dynamic <see cref="System.Dynamic.ExpandoObject"/>,
18+ /// and <see cref="IReadOnlyDictionary{TKey,TValue}"/>.
1419/// Include this source, if any of these types shall be used.
20+ /// <para/>
21+ /// For support of <see cref="IReadOnlyDictionary{TKey,TValue}"/> <see cref="IsIReadOnlyDictionarySupported"/> must be set to <see langword="true"/>.
22+ /// This uses Reflection and is slower than the other types despite caching.
1523/// </summary>
1624public class DictionarySource : Source
1725{
@@ -25,28 +33,119 @@ public override bool TryEvaluateSelector(ISelectorInfo selectorInfo)
2533
2634 var selector = selectorInfo . SelectorText ;
2735
28- // See if current is an IDictionary and contains the selector:
36+ // See if current is an IDictionary (including generic dictionaries) and contains the selector:
2937 if ( current is IDictionary rawDict )
3038 foreach ( DictionaryEntry entry in rawDict )
3139 {
3240 var key = entry . Key as string ?? entry . Key . ToString ( ) ! ;
3341
34- if ( key . Equals ( selector , selectorInfo . FormatDetails . Settings . GetCaseSensitivityComparison ( ) ) )
35- {
36- selectorInfo . Result = entry . Value ;
37- return true ;
38- }
42+ if ( ! key . Equals ( selector , selectorInfo . FormatDetails . Settings . GetCaseSensitivityComparison ( ) ) )
43+ continue ;
44+
45+ selectorInfo . Result = entry . Value ;
46+ return true ;
3947 }
4048
41- // this check is for dynamics and generic dictionaries
42- if ( current is not IDictionary < string , object ? > dict ) return false ;
49+ // This check is for dynamics (ExpandoObject):
50+ if ( current is IDictionary < string , object ? > dict )
51+ {
52+ // We're using the CaseSensitivityType of the dictionary,
53+ // not the one from Settings.GetCaseSensitivityComparison().
54+ // This is faster and has less GC than Key.Equals(...)
55+ if ( ! dict . TryGetValue ( selector , out var val ) ) return false ;
56+
57+ selectorInfo . Result = val ;
58+ return true ;
59+ }
60+
61+ // This is for IReadOnlyDictionary<,> using Reflection
62+ if ( IsIReadOnlyDictionarySupported && TryGetDictionaryValue ( current , selector ,
63+ selectorInfo . FormatDetails . Settings . GetCaseSensitivityComparison ( ) , out var value ) )
64+ {
65+ selectorInfo . Result = value ;
66+ return true ;
67+ }
68+
69+ return false ;
70+ }
71+
72+ #region *** IReadOnlyDictionary<,> ***
73+
74+ /// <summary>
75+ /// Gets the type cache <see cref="IDictionary{TKey,TValue}"/> for <see cref="IReadOnlyDictionary{TKey,TValue}"/>.
76+ /// It could e.g. be pre-filled or cleared in a derived class.
77+ /// </summary>
78+ /// <remarks>
79+ /// Note: For reading, <see cref="Dictionary{TKey, TValue}"/> and <see cref="ConcurrentDictionary{TKey,TValue}"/> perform equally.
80+ /// For writing, <see cref="ConcurrentDictionary{TKey, TValue}"/> is slower with more garbage (tested under net5.0).
81+ /// </remarks>
82+ protected internal readonly IDictionary < Type , ( PropertyInfo , PropertyInfo ) ? > RoDictionaryTypeCache =
83+ SmartSettings . IsThreadSafeMode
84+ ? new ConcurrentDictionary < Type , ( PropertyInfo , PropertyInfo ) ? > ( )
85+ : new Dictionary < Type , ( PropertyInfo , PropertyInfo ) ? > ( ) ;
86+
87+ /// <summary>
88+ /// Gets or sets, whether the <see cref="IReadOnlyDictionary{TKey,TValue}"/> interface should be supported.
89+ /// Although caching is used, this is still slower than the other types.
90+ /// Default is <see langword="false"/>.
91+ /// </summary>
92+ public bool IsIReadOnlyDictionarySupported { get ; set ; } = false ;
93+
94+ private bool TryGetDictionaryValue ( object obj , string key , StringComparison comparison , out object ? value )
95+ {
96+ value = null ;
97+
98+ if ( ! TryGetDictionaryProperties ( obj . GetType ( ) , out var propertyTuple ) ) return false ;
99+
100+ var keys = ( IEnumerable ) propertyTuple ! . Value . KeyProperty . GetValue ( obj ) ;
101+
102+ foreach ( var k in keys )
103+ {
104+ if ( ! k . ToString ( ) . Equals ( key , comparison ) )
105+ continue ;
106+
107+ value = propertyTuple . Value . ItemProperty . GetValue ( obj , new [ ] { k } ) ;
108+ return true ;
109+ }
43110
44- // We're using the CaseSensitivityType of the dictionary,
45- // not the one from Settings.GetCaseSensitivityComparison().
46- // This is faster and has less GC than Key.Equals(...)
47- if ( ! dict . TryGetValue ( selector , out var val ) ) return false ;
111+ return false ;
112+ }
113+
114+ private bool TryGetDictionaryProperties ( Type type , out ( PropertyInfo KeyProperty , PropertyInfo ItemProperty ) ? propertyTuple )
115+ {
116+ // try to get the properties from the cache
117+ if ( RoDictionaryTypeCache . TryGetValue ( type , out propertyTuple ) )
118+ return propertyTuple != null ;
119+
120+ if ( ! IsIReadOnlyDictionary ( type ) )
121+ {
122+ // don't check the type again, although it's not a IReadOnlyDictionary
123+ RoDictionaryTypeCache [ type ] = null ;
124+ return false ;
125+ }
126+
127+ // get Key and Item properties of the dictionary
128+ propertyTuple = ( type . GetProperty ( nameof ( IDictionary . Keys ) ) , type . GetProperty ( "Item" ) ) ;
129+
130+ System . Diagnostics . Debug . Assert ( propertyTuple . Value . KeyProperty != null && propertyTuple . Value . ItemProperty != null , "Key and Item properties must not be null" ) ;
48131
49- selectorInfo . Result = val ;
132+ RoDictionaryTypeCache [ type ] = propertyTuple ;
50133 return true ;
51134 }
52- }
135+
136+ private static bool IsIReadOnlyDictionary ( Type type )
137+ {
138+ // No Linq for less garbage
139+ foreach ( var typeInterface in type . GetInterfaces ( ) )
140+ {
141+ if ( typeInterface == typeof ( IReadOnlyDictionary < , > ) ||
142+ ( typeInterface . IsGenericType
143+ && typeInterface . GetGenericTypeDefinition ( ) == typeof ( IReadOnlyDictionary < , > ) ) )
144+ return true ;
145+ }
146+
147+ return false ;
148+ }
149+
150+ #endregion
151+ }
0 commit comments