-
-
Notifications
You must be signed in to change notification settings - Fork 507
Expand file tree
/
Copy pathDataDictionaryExtensions.cs
More file actions
194 lines (179 loc) · 7.57 KB
/
DataDictionaryExtensions.cs
File metadata and controls
194 lines (179 loc) · 7.57 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
using System.Text.Json;
using System.Text.Json.Nodes;
using Exceptionless.Core.Models;
using Foundatio.Serializer;
namespace Exceptionless.Core.Extensions;
public static class DataDictionaryExtensions
{
/// <summary>
/// Options for deserializing JsonElement values that may use PascalCase or snake_case
/// property names. Uses case-insensitive matching without a naming policy so both formats work.
/// </summary>
private static readonly JsonSerializerOptions CaseInsensitiveOptions = new()
{
PropertyNameCaseInsensitive = true
};
/// <summary>
/// Retrieves a typed value from the <see cref="DataDictionary"/>, deserializing if necessary.
/// </summary>
/// <typeparam name="T">The target type to deserialize to.</typeparam>
/// <param name="extendedData">The data dictionary containing the value.</param>
/// <param name="key">The key of the value to retrieve.</param>
/// <param name="serializer">The text serializer to use for deserialization.</param>
/// <returns>The deserialized value, or <c>default</c> if deserialization fails.</returns>
/// <exception cref="KeyNotFoundException">Thrown when the key is not found in the dictionary.</exception>
/// <remarks>
/// <para>This method handles multiple source formats in priority order:</para>
/// <list type="number">
/// <item><description>Direct type match - returns value directly</description></item>
/// <item><description><see cref="JsonDocument"/> - extracts root element and deserializes</description></item>
/// <item><description><see cref="JsonElement"/> - extracts raw JSON and deserializes via ITextSerializer</description></item>
/// <item><description><see cref="JsonNode"/> - extracts JSON string and deserializes via ITextSerializer</description></item>
/// <item><description><see cref="Dictionary{TKey,TValue}"/> - re-serializes to JSON then deserializes via ITextSerializer</description></item>
/// <item><description><see cref="List{T}"/> of objects - re-serializes to JSON then deserializes via ITextSerializer</description></item>
/// <item><description><see cref="Newtonsoft.Json.Linq.JObject"/> - uses ToObject for Elasticsearch compatibility</description></item>
/// <item><description>JSON string - deserializes via ITextSerializer</description></item>
/// <item><description>Fallback - attempts type conversion via ToType</description></item>
/// </list>
/// </remarks>
public static T? GetValue<T>(this DataDictionary extendedData, string key, ITextSerializer serializer)
{
if (!extendedData.TryGetValue(key, out object? data))
throw new KeyNotFoundException($"Key \"{key}\" not found in the dictionary.");
if (data is T value)
return value;
// JsonDocument -> JsonElement
if (data is JsonDocument jsonDocument)
data = jsonDocument.RootElement;
// JsonElement (from STJ deserialization when ObjectToInferredTypesConverter wasn't used)
if (data is JsonElement jsonElement)
{
try
{
// Fast-path for string type
if (typeof(T) == typeof(string))
{
object? s = jsonElement.ValueKind switch
{
JsonValueKind.String => jsonElement.GetString(),
JsonValueKind.Number => jsonElement.GetRawText(),
JsonValueKind.True => "true",
JsonValueKind.False => "false",
JsonValueKind.Null => null,
_ => jsonElement.GetRawText()
};
return (T?)s;
}
// Deserialize directly from JsonElement using case-insensitive matching.
// This handles both snake_case (from Elasticsearch) and PascalCase (from
// [JsonExtensionData] which preserves original property names).
var result = jsonElement.Deserialize<T>(CaseInsensitiveOptions);
if (result is not null)
return result;
}
catch (Exception ex) when (ex is JsonException or InvalidOperationException or FormatException)
{
// Ignored - fall through to next handler
}
}
// JsonNode (JsonObject/JsonArray/JsonValue)
if (data is JsonNode jsonNode)
{
try
{
string jsonString = jsonNode.ToJsonString();
var result = serializer.Deserialize<T>(jsonString);
if (result is not null)
return result;
}
catch (Exception ex) when (ex is JsonException or InvalidOperationException or FormatException)
{
// Ignored - fall through to next handler
}
}
// Dictionary<string, object?> from ObjectToInferredTypesConverter
// Re-serialize to JSON then deserialize to target type via ITextSerializer
if (data is Dictionary<string, object?> dictionary)
{
try
{
string? dictJson = serializer.SerializeToString(dictionary);
if (dictJson is not null)
{
var result = serializer.Deserialize<T>(dictJson);
if (result is not null)
return result;
}
}
catch
{
// Ignored - fall through to next handler
}
}
// List<object?> from ObjectToInferredTypesConverter (for array values)
if (data is List<object?> list)
{
try
{
string? listJson = serializer.SerializeToString(list);
if (listJson is not null)
{
var result = serializer.Deserialize<T>(listJson);
if (result is not null)
return result;
}
}
catch
{
// Ignored - fall through to next handler
}
}
// Newtonsoft.Json.Linq.JObject - for Elasticsearch compatibility.
// When data is read from Elasticsearch (which uses JSON.NET via NEST), complex objects
// in DataDictionary are deserialized as JObject. This handler converts them to the target type.
if (data is Newtonsoft.Json.Linq.JObject jObject)
{
try
{
return jObject.ToObject<T>();
}
catch
{
// Ignored - fall through to next handler
}
}
// JSON string - deserialize via ITextSerializer
if (data is string json && json.IsJson())
{
try
{
var result = serializer.Deserialize<T>(json);
if (result is not null)
return result;
}
catch
{
// Ignored - fall through to next handler
}
}
// Fallback: attempt direct type conversion
try
{
if (data != null)
{
return data.ToType<T>();
}
}
catch
{
// Ignored
}
return default;
}
public static void RemoveSensitiveData(this DataDictionary extendedData)
{
string[] removeKeys = [.. extendedData.Keys.Where(k => k.StartsWith('-'))];
foreach (string key in removeKeys)
extendedData.Remove(key);
}
}