Skip to content

Commit 746ef9d

Browse files
authored
DictionarySource can evaluate IReadOnlyDictionary<TKey,TValue> (#353)
Closes #349 * DictionarySource can evaluate IReadOnlyDictionary<TKey,TValue> * DictionarySource.IsIReadOnlyDictionarySupported must be set to true (default is false)
1 parent 6a395ec commit 746ef9d

File tree

2 files changed

+210
-17
lines changed

2 files changed

+210
-17
lines changed

src/SmartFormat.Tests/Extensions/DictionarySourceTests.cs

Lines changed: 96 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
1-
using System.Collections.Generic;
1+
using System;
2+
using System.Collections;
3+
using System.Collections.Generic;
24
using System.Dynamic;
5+
using System.Linq;
36
using NUnit.Framework;
47
using SmartFormat.Core.Settings;
58
using SmartFormat.Extensions;
69
using SmartFormat.Tests.TestUtils;
10+
using SmartFormat.Utilities;
711

812
namespace SmartFormat.Tests.Extensions;
913

@@ -156,6 +160,96 @@ public void Dictionary_Dot_Notation_Nullable()
156160
Assert.That(result, Is.EqualTo(expected));
157161
}
158162

163+
[Test]
164+
public void Generic_Dictionary_String_String()
165+
{
166+
var dict = new Dictionary<string, string> { { "Name", "Joe" } };
167+
var smart = new SmartFormatter()
168+
.AddExtensions(new DefaultSource(), new DictionarySource())
169+
.AddExtensions(new DefaultFormatter());
170+
var result = smart.Format("{Name}", dict);
171+
172+
Assert.That(result, Is.EqualTo("Joe"));
173+
}
174+
175+
[Test]
176+
public void IReadOnlyDictionary_With_IConvertible_Key()
177+
{
178+
var roDict = new CustomReadOnlyDictionary<IConvertible, object?>(new Dictionary<IConvertible, object?> { { 1, 1 }, { "Two", 2 }, { "Three", "three" }, });
179+
var smart = new SmartFormatter()
180+
.AddExtensions(new DefaultSource(), new DictionarySource { IsIReadOnlyDictionarySupported = true })
181+
.AddExtensions(new DefaultFormatter());
182+
var result = smart.Format("{1}{Two}{Three}", roDict);
183+
184+
Assert.That(result, Is.EqualTo("12three"));
185+
}
186+
187+
[Test]
188+
public void IReadOnlyDictionary_With_String_Key()
189+
{
190+
var roDict = new CustomReadOnlyDictionary<string, object?>(new Dictionary<string, object?> { { "One", 1 }, { "Two", 2 }, { "Three", "three" }, });
191+
var smart = new SmartFormatter()
192+
.AddExtensions(new DefaultSource(), new DictionarySource { IsIReadOnlyDictionarySupported = true })
193+
.AddExtensions(new DefaultFormatter());
194+
var result = smart.Format("{One}{Two}{Three}", roDict);
195+
196+
Assert.That(result, Is.EqualTo("12three"));
197+
}
198+
199+
[Test]
200+
public void IReadOnlyDictionary_Cache_Should_Store_Types_It_Cannot_Handle()
201+
{
202+
var dictSource = new DictionarySource { IsIReadOnlyDictionarySupported = true };
203+
var kvp = new KeyValuePair<string, object?>("One", 1);
204+
var smart = new SmartFormatter()
205+
.AddExtensions(new DefaultSource(), dictSource, new KeyValuePairSource())
206+
.AddExtensions(new DefaultFormatter());
207+
var result = smart.Format("{One}", kvp);
208+
209+
Assert.That(result, Is.EqualTo("1"));
210+
Assert.That(dictSource.RoDictionaryTypeCache.Keys.Count, Is.EqualTo(1));
211+
Assert.That(dictSource.RoDictionaryTypeCache.Keys.First(), Is.EqualTo(typeof(KeyValuePair<string, object?>)));
212+
Assert.That(dictSource.RoDictionaryTypeCache.Values.First(), Is.Null);
213+
}
214+
215+
public class CustomReadOnlyDictionary<TKey, TValue> : IReadOnlyDictionary<TKey, TValue?>
216+
{
217+
private readonly IDictionary<TKey, TValue?> _dictionary;
218+
219+
public CustomReadOnlyDictionary(IDictionary<TKey, TValue?> dictionary)
220+
{
221+
_dictionary = dictionary;
222+
}
223+
224+
public IEnumerator<KeyValuePair<TKey, TValue?>> GetEnumerator()
225+
{
226+
return _dictionary.GetEnumerator();
227+
}
228+
229+
IEnumerator IEnumerable.GetEnumerator()
230+
{
231+
return GetEnumerator();
232+
}
233+
234+
public int Count => _dictionary.Count;
235+
236+
public bool ContainsKey(TKey key)
237+
{
238+
return _dictionary.ContainsKey(key);
239+
}
240+
241+
public bool TryGetValue(TKey key, out TValue? value)
242+
{
243+
return _dictionary.TryGetValue(key, out value);
244+
}
245+
246+
public TValue? this[TKey key] => _dictionary[key];
247+
248+
public IEnumerable<TKey> Keys => _dictionary.Keys;
249+
250+
public IEnumerable<TValue?> Values => _dictionary.Values;
251+
}
252+
159253
public class Address
160254
{
161255
public CityDetails? City { get; set; } = new();
@@ -202,4 +296,4 @@ public Dictionary<string, string> ToDictionary()
202296
}
203297
}
204298
}
205-
}
299+
}

src/SmartFormat/Extensions/DictionarySource.cs

Lines changed: 114 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,24 @@
22
// Copyright SmartFormat Project maintainers and contributors.
33
// Licensed under the MIT license.
44

5+
using System;
56
using System.Collections;
7+
using System.Collections.Concurrent;
68
using System.Collections.Generic;
9+
using System.Reflection;
710
using SmartFormat.Core.Extensions;
11+
using SmartFormat.Core.Settings;
812

913
namespace 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>
1624
public 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

Comments
 (0)