11using System . Text . Json ;
22using System . Text . Json . Nodes ;
33using Exceptionless . Core . Models ;
4+ using Foundatio . Serializer ;
45
56namespace Exceptionless . Core . Extensions ;
67
78public static class DataDictionaryExtensions
89{
10+ /// <summary>
11+ /// Options for deserializing JsonElement values that may use PascalCase or snake_case
12+ /// property names. Uses case-insensitive matching without a naming policy so both formats work.
13+ /// </summary>
14+ private static readonly JsonSerializerOptions CaseInsensitiveOptions = new ( )
15+ {
16+ PropertyNameCaseInsensitive = true
17+ } ;
918 /// <summary>
1019 /// Retrieves a typed value from the <see cref="DataDictionary"/>, deserializing if necessary.
1120 /// </summary>
1221 /// <typeparam name="T">The target type to deserialize to.</typeparam>
1322 /// <param name="extendedData">The data dictionary containing the value.</param>
1423 /// <param name="key">The key of the value to retrieve.</param>
15- /// <param name="options ">The JSON serializer options to use for deserialization.</param>
24+ /// <param name="serializer ">The text serializer to use for deserialization.</param>
1625 /// <returns>The deserialized value, or <c>default</c> if deserialization fails.</returns>
1726 /// <exception cref="KeyNotFoundException">Thrown when the key is not found in the dictionary.</exception>
1827 /// <remarks>
1928 /// <para>This method handles multiple source formats in priority order:</para>
2029 /// <list type="number">
2130 /// <item><description>Direct type match - returns value directly</description></item>
2231 /// <item><description><see cref="JsonDocument"/> - extracts root element and deserializes</description></item>
23- /// <item><description><see cref="JsonElement"/> - deserializes using provided options </description></item>
24- /// <item><description><see cref="JsonNode"/> - deserializes using provided options </description></item>
25- /// <item><description><see cref="Dictionary{TKey,TValue}"/> - re-serializes to JSON then deserializes (for ObjectToInferredTypesConverter output) </description></item>
26- /// <item><description><see cref="List{T}"/> of objects - re-serializes to JSON then deserializes</description></item>
27- /// <item><description><see cref="Newtonsoft.Json.Linq.JObject"/> - uses ToObject for Elasticsearch compatibility (data read from Elasticsearch uses JSON.NET) </description></item>
28- /// <item><description>JSON string - parses and deserializes </description></item>
32+ /// <item><description><see cref="JsonElement"/> - extracts raw JSON and deserializes via ITextSerializer </description></item>
33+ /// <item><description><see cref="JsonNode"/> - extracts JSON string and deserializes via ITextSerializer </description></item>
34+ /// <item><description><see cref="Dictionary{TKey,TValue}"/> - re-serializes to JSON then deserializes via ITextSerializer </description></item>
35+ /// <item><description><see cref="List{T}"/> of objects - re-serializes to JSON then deserializes via ITextSerializer </description></item>
36+ /// <item><description><see cref="Newtonsoft.Json.Linq.JObject"/> - uses ToObject for Elasticsearch compatibility</description></item>
37+ /// <item><description>JSON string - deserializes via ITextSerializer </description></item>
2938 /// <item><description>Fallback - attempts type conversion via ToType</description></item>
3039 /// </list>
3140 /// </remarks>
32- public static T ? GetValue < T > ( this DataDictionary extendedData , string key , JsonSerializerOptions options )
41+ public static T ? GetValue < T > ( this DataDictionary extendedData , string key , ITextSerializer serializer )
3342 {
3443 if ( ! extendedData . TryGetValue ( key , out object ? data ) )
3544 throw new KeyNotFoundException ( $ "Key \" { key } \" not found in the dictionary.") ;
@@ -42,18 +51,46 @@ public static class DataDictionaryExtensions
4251 data = jsonDocument . RootElement ;
4352
4453 // JsonElement (from STJ deserialization when ObjectToInferredTypesConverter wasn't used)
45- if ( data is JsonElement jsonElement &&
46- TryDeserialize ( jsonElement , options , out T ? jsonElementResult ) )
54+ if ( data is JsonElement jsonElement )
4755 {
48- return jsonElementResult ;
56+ try
57+ {
58+ // Fast-path for string type
59+ if ( typeof ( T ) == typeof ( string ) )
60+ {
61+ object ? s = jsonElement . ValueKind switch
62+ {
63+ JsonValueKind . String => jsonElement . GetString ( ) ,
64+ JsonValueKind . Number => jsonElement . GetRawText ( ) ,
65+ JsonValueKind . True => "true" ,
66+ JsonValueKind . False => "false" ,
67+ JsonValueKind . Null => null ,
68+ _ => jsonElement . GetRawText ( )
69+ } ;
70+
71+ return ( T ? ) s ;
72+ }
73+
74+ // Deserialize directly from JsonElement using case-insensitive matching.
75+ // This handles both snake_case (from Elasticsearch) and PascalCase (from
76+ // [JsonExtensionData] which preserves original property names).
77+ var result = jsonElement . Deserialize < T > ( CaseInsensitiveOptions ) ;
78+ if ( result is not null )
79+ return result ;
80+ }
81+ catch
82+ {
83+ // Ignored - fall through to next handler
84+ }
4985 }
5086
5187 // JsonNode (JsonObject/JsonArray/JsonValue)
5288 if ( data is JsonNode jsonNode )
5389 {
5490 try
5591 {
56- var result = jsonNode . Deserialize < T > ( options ) ;
92+ string jsonString = jsonNode . ToJsonString ( ) ;
93+ var result = serializer . Deserialize < T > ( jsonString ) ;
5794 if ( result is not null )
5895 return result ;
5996 }
@@ -64,15 +101,18 @@ public static class DataDictionaryExtensions
64101 }
65102
66103 // Dictionary<string, object?> from ObjectToInferredTypesConverter
67- // Re-serialize to JSON then deserialize to target type with proper naming policy
104+ // Re-serialize to JSON then deserialize to target type via ITextSerializer
68105 if ( data is Dictionary < string , object ? > dictionary )
69106 {
70107 try
71108 {
72- string dictJson = JsonSerializer . Serialize ( dictionary , options ) ;
73- var result = JsonSerializer . Deserialize < T > ( dictJson , options ) ;
74- if ( result is not null )
75- return result ;
109+ string ? dictJson = serializer . SerializeToString ( dictionary ) ;
110+ if ( dictJson is not null )
111+ {
112+ var result = serializer . Deserialize < T > ( dictJson ) ;
113+ if ( result is not null )
114+ return result ;
115+ }
76116 }
77117 catch
78118 {
@@ -85,10 +125,13 @@ public static class DataDictionaryExtensions
85125 {
86126 try
87127 {
88- string listJson = JsonSerializer . Serialize ( list , options ) ;
89- var result = JsonSerializer . Deserialize < T > ( listJson , options ) ;
90- if ( result is not null )
91- return result ;
128+ string ? listJson = serializer . SerializeToString ( list ) ;
129+ if ( listJson is not null )
130+ {
131+ var result = serializer . Deserialize < T > ( listJson ) ;
132+ if ( result is not null )
133+ return result ;
134+ }
92135 }
93136 catch
94137 {
@@ -111,12 +154,12 @@ public static class DataDictionaryExtensions
111154 }
112155 }
113156
114- // JSON string
157+ // JSON string - deserialize via ITextSerializer
115158 if ( data is string json && json . IsJson ( ) )
116159 {
117160 try
118161 {
119- var result = JsonSerializer . Deserialize < T > ( json , options ) ;
162+ var result = serializer . Deserialize < T > ( json ) ;
120163 if ( result is not null )
121164 return result ;
122165 }
@@ -142,49 +185,9 @@ public static class DataDictionaryExtensions
142185 return default ;
143186 }
144187
145- private static bool TryDeserialize < T > ( JsonElement element , JsonSerializerOptions options , out T ? result )
146- {
147- result = default ;
148-
149- try
150- {
151- // Fast-path for common primitives where the element isn't an object/array
152- // (Deserialize<T> also works for these, but this avoids some edge cases and allocations)
153- if ( typeof ( T ) == typeof ( string ) )
154- {
155- object ? s = element . ValueKind switch
156- {
157- JsonValueKind . String => element . GetString ( ) ,
158- JsonValueKind . Number => element . GetRawText ( ) ,
159- JsonValueKind . True => "true" ,
160- JsonValueKind . False => "false" ,
161- JsonValueKind . Null => null ,
162- _ => element . GetRawText ( )
163- } ;
164-
165- result = ( T ? ) s ;
166- return true ;
167- }
168-
169- // General case
170- var deserialized = element . Deserialize < T > ( options ) ;
171- if ( deserialized is not null )
172- {
173- result = deserialized ;
174- return true ;
175- }
176- }
177- catch
178- {
179- // Ignored
180- }
181-
182- return false ;
183- }
184-
185188 public static void RemoveSensitiveData ( this DataDictionary extendedData )
186189 {
187- string [ ] removeKeys = extendedData . Keys . Where ( k => k . StartsWith ( '-' ) ) . ToArray ( ) ;
190+ string [ ] removeKeys = [ .. extendedData . Keys . Where ( k => k . StartsWith ( '-' ) ) ] ;
188191 foreach ( string key in removeKeys )
189192 extendedData . Remove ( key ) ;
190193 }
0 commit comments