Skip to content

Commit 4e654ad

Browse files
committed
Optimize IReadOnlyDictionary check by caching
- Introduced a thread-safe cache to IsIReadOnlyDictionary for improved performance and reduced allocations. - Added tests for nullable operator with enum-keyed dictionaries to ensure missing keys return empty values.
1 parent 3121a10 commit 4e654ad

2 files changed

Lines changed: 47 additions & 6 deletions

File tree

src/SmartFormat.Tests/Extensions/DictionarySourceTests.cs

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@
22
using System.Collections;
33
using System.Collections.Generic;
44
using System.Dynamic;
5-
using System.Globalization;
6-
using System.Linq;
75
using NUnit.Framework;
86
using SmartFormat.Core.Formatting;
97
using SmartFormat.Core.Settings;
@@ -164,7 +162,7 @@ public void Dictionary_Dot_Notation_Nullable()
164162
[Test]
165163
public void Dictionary_NullableOperator_MissingKey_ReturnsEmpty()
166164
{
167-
// Test for GitHub issue #522
165+
// Test similar to GitHub issue #522, but using string keys
168166
var sf = Smart.CreateDefaultSmartFormat();
169167

170168
var obj = new {
@@ -193,7 +191,39 @@ public void Dictionary_NullableOperator_MissingKey_ReturnsEmpty()
193191

194192
// Now add the missing key and verify that it is properly resolved
195193
obj.Changes.Add("Volume", 200);
196-
Assert.That(sf.Format("{Changes:{Count};{?.Volume};{Name}}", obj),
194+
Assert.That(sf.Format("{Changes.Count};{Changes?.Volume};{Changes.Name}", obj),
195+
Is.EqualTo("100;200;ABCD"));
196+
}
197+
198+
private enum ChangeKey { Count, Volume, Name }
199+
200+
[Test]
201+
public void Dictionary_NullableOperator_MissingEnumKey_ReturnsEmpty()
202+
{
203+
// Test for GitHub issue #522 with non-string dictionary keys
204+
var sf = Smart.CreateDefaultSmartFormat();
205+
var obj = new {
206+
Changes = new Dictionary<ChangeKey, object>
207+
{
208+
{ ChangeKey.Count, 100 },
209+
// Volume is intentionally missing
210+
// although it is accessed in the format string
211+
{ ChangeKey.Name, "ABCD" },
212+
}
213+
};
214+
// Use nullable syntax to avoid exceptions for selectors
215+
// that cannot be resolved with the DictionarySource:
216+
// 'Volume' may be missing, but 'Changes' is expected to be present
217+
var result =
218+
sf.Format("{Changes.Count};{Changes?.Volume};{Changes.Name}", obj);
219+
Assert.Multiple(() =>
220+
{
221+
Assert.That(result, Is.EqualTo("100;;ABCD"));
222+
});
223+
224+
// Now add the missing key and verify that it is properly resolved
225+
obj.Changes.Add(ChangeKey.Volume, 200);
226+
Assert.That(sf.Format("{Changes.Count};{Changes?.Volume};{Changes.Name}", obj),
197227
Is.EqualTo("100;200;ABCD"));
198228
}
199229

src/SmartFormat/Extensions/DictionarySource.cs

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
using System;
66
using System.Collections;
7+
using System.Collections.Concurrent;
78
using System.Collections.Generic;
89
using System.Reflection;
910
using SmartFormat.Core.Extensions;
@@ -166,18 +167,28 @@ private bool TryGetDictionaryProperties(Type type, out (PropertyInfo KeyProperty
166167
return true;
167168
}
168169

170+
private static readonly ConcurrentDictionary<Type, bool> RoDictionaryTypeBoolCache = new();
171+
169172
private static bool IsIReadOnlyDictionary(Type type)
170173
{
174+
if (RoDictionaryTypeBoolCache.TryGetValue(type, out var cached))
175+
return cached;
176+
171177
// No Linq for less garbage
178+
var result = false;
172179
foreach (var typeInterface in type.GetInterfaces())
173180
{
174181
if (typeInterface == typeof(IReadOnlyDictionary<,>) ||
175182
(typeInterface.IsGenericType
176183
&& typeInterface.GetGenericTypeDefinition() == typeof(IReadOnlyDictionary<,>)))
177-
return true;
184+
{
185+
result = true;
186+
break;
187+
}
178188
}
179189

180-
return false;
190+
RoDictionaryTypeBoolCache[type] = result;
191+
return result;
181192
}
182193

183194
#endregion

0 commit comments

Comments
 (0)