Skip to content

Commit c5683e9

Browse files
Per Kopsperkops
authored andcommitted
refactor: migrate DataReader/DataRow extensions to System.Text.Json
Replace Newtonsoft.Json with System.Text.Json for better performance. Remove KustoJsonSerializerHelper and custom Newtonsoft converters. Add shared KustoJsonSerializerOptions for consistent configuration.
1 parent 7484fb5 commit c5683e9

File tree

10 files changed

+247
-381
lines changed

10 files changed

+247
-381
lines changed

src/Atc.Kusto/Extensions/DataReaderExtensions.cs

Lines changed: 55 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,28 +2,75 @@
22
namespace Atc.Kusto;
33

44
/// <summary>
5-
/// Provides extension methods for reading and converting data from an <see cref="IDataReader"/> to objects using Newtonsoft.Json.
5+
/// Provides extension methods for reading and converting data from an <see cref="IDataReader"/> to objects using System.Text.Json.
66
/// These methods simplify the process of converting database records into strongly-typed objects.
7+
/// Note: Newtonsoft.Json.Linq is retained for handling <see cref="JToken"/> objects returned by the Kusto SDK for dynamic fields.
78
/// </summary>
89
public static class DataReaderExtensions
910
{
1011
/// <summary>
1112
/// Reads all rows from the <see cref="IDataReader"/> and converts them into an array of strongly-typed objects of type <typeparamref name="T"/>.
12-
/// The conversion is handled using Newtonsoft.Json with a custom converter to facilitate the transition between Newtonsoft.Json and System.Text.Json.
13+
/// The conversion is handled using System.Text.Json for optimal performance.
1314
/// </summary>
1415
/// <typeparam name="T">The type of objects to convert the data into.</typeparam>
1516
/// <param name="reader">The <see cref="IDataReader"/> from which to read the data.</param>
17+
/// <param name="options">Optional JSON serializer options. If not provided, default options with enum string conversion, case-insensitive property matching, and number string reading will be used.</param>
1618
/// <returns>An array of objects of type <typeparamref name="T"/> representing the data read from the reader.</returns>
1719
public static T[] ReadObjects<T>(
18-
this IDataReader reader)
20+
this IDataReader reader,
21+
JsonSerializerOptions? options = null)
1922
{
2023
ArgumentNullException.ThrowIfNull(reader);
2124

22-
return reader
23-
.ToJObjects()
24-
.Select(o => o.ToObject<T>(KustoJsonSerializerHelper.Serializer))
25-
.OfType<T>()
26-
.ToArray();
25+
options ??= KustoJsonSerializerOptions.Default;
26+
27+
var buffer = new ArrayBufferWriter<byte>();
28+
using var doc = new Utf8JsonWriter(buffer);
29+
var results = new List<T>();
30+
31+
while (reader.Read())
32+
{
33+
buffer.Clear();
34+
doc.Reset(buffer);
35+
36+
doc.WriteStartObject();
37+
38+
for (var i = 0; i < reader.FieldCount; i++)
39+
{
40+
var value = reader.GetValue(i);
41+
42+
var name = reader.GetName(i);
43+
if (options.PropertyNamingPolicy is { } np)
44+
{
45+
name = np.ConvertName(name);
46+
}
47+
48+
doc.WritePropertyName(name);
49+
50+
switch (value)
51+
{
52+
case JToken token:
53+
doc.WriteRawValue(token.ToString(Newtonsoft.Json.Formatting.None));
54+
break;
55+
case DBNull:
56+
doc.WriteNullValue();
57+
break;
58+
case SqlDecimal sd:
59+
doc.WriteNumberValue(sd.ToDecimal());
60+
break;
61+
default:
62+
JsonSerializer.Serialize(doc, value, options);
63+
break;
64+
}
65+
}
66+
67+
doc.WriteEndObject();
68+
doc.Flush();
69+
70+
results.Add(JsonSerializer.Deserialize<T>(buffer.WrittenSpan, options)!);
71+
}
72+
73+
return [.. results];
2774
}
2875

2976
/// <summary>
Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// ReSharper disable CheckNamespace
1+
// ReSharper disable CheckNamespace
22
namespace Atc.Kusto;
33

44
/// <summary>
@@ -8,23 +8,40 @@ public static class DataRowExtensions
88
{
99
/// <summary>
1010
/// Maps the specified <see cref="DataRow"/> to an object of type <typeparamref name="T"/>.
11-
/// The mapping is performed by converting the row's values to a JSON object using a shared serializer.
11+
/// The mapping is performed by converting the row's values to JSON using System.Text.Json
12+
/// and then deserializing to the target type.
13+
/// Special handling is provided for SqlDecimal values returned by Kusto to ensure proper conversion.
1214
/// </summary>
1315
/// <typeparam name="T">The target type to which the row is mapped.</typeparam>
1416
/// <param name="row">The data row to map.</param>
17+
/// <param name="options">Optional JSON serializer options. If not provided, default options with enum string conversion and case-insensitive property matching will be used.</param>
1518
/// <returns>An instance of type <typeparamref name="T"/> created from the DataRow.</returns>
16-
public static T? MapDataRow<T>(this DataRow row)
19+
public static T? MapDataRow<T>(this DataRow row, JsonSerializerOptions? options = null)
1720
{
1821
ArgumentNullException.ThrowIfNull(row);
1922

23+
options ??= KustoJsonSerializerOptions.Default;
24+
2025
// Convert the DataRow to a dictionary using its columns.
26+
// Pre-process values to handle SqlDecimal and DBNull since System.Text.Json doesn't support them.
2127
var dict = row.Table.Columns
2228
.Cast<DataColumn>()
23-
.ToDictionary(col => col.ColumnName, col => row[col], StringComparer.Ordinal);
24-
25-
// Create a JObject from the dictionary using the shared serializer.
26-
var jObject = Newtonsoft.Json.Linq.JObject.FromObject(dict, KustoJsonSerializerHelper.Serializer);
29+
.ToDictionary(
30+
col => col.ColumnName,
31+
col =>
32+
{
33+
var value = row[col];
34+
return value switch
35+
{
36+
SqlDecimal sd => sd.ToDecimal(),
37+
DBNull => null,
38+
_ => value,
39+
};
40+
},
41+
StringComparer.Ordinal);
2742

28-
return jObject.ToObject<T>(KustoJsonSerializerHelper.Serializer);
43+
// Serialize to JSON and deserialize to the target type using System.Text.Json.
44+
var json = JsonSerializer.Serialize(dict, options);
45+
return JsonSerializer.Deserialize<T>(json, options);
2946
}
3047
}

src/Atc.Kusto/Serialization/Internal/KustoJsonSerializerHelper.cs

Lines changed: 0 additions & 21 deletions
This file was deleted.

src/Atc.Kusto/Serialization/Internal/NewtonsoftDecimalConverter.cs

Lines changed: 0 additions & 72 deletions
This file was deleted.

src/Atc.Kusto/Serialization/Internal/NewtonsoftObjectConverter.cs

Lines changed: 0 additions & 59 deletions
This file was deleted.
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
namespace Atc.Kusto.Utilities.Internal;
2+
3+
/// <summary>
4+
/// Provides shared JSON serializer options for consistent Kusto data deserialization across the library.
5+
/// </summary>
6+
internal static class KustoJsonSerializerOptions
7+
{
8+
/// <summary>
9+
/// Gets the default JSON serializer options configured for Kusto data deserialization.
10+
/// Includes enum string conversion, case-insensitive property matching, and support for reading numbers from strings.
11+
/// </summary>
12+
public static JsonSerializerOptions Default { get; } = new()
13+
{
14+
Converters = { new JsonStringEnumConverter() },
15+
PropertyNameCaseInsensitive = true,
16+
NumberHandling = JsonNumberHandling.AllowReadingFromString,
17+
};
18+
}

test/Atc.Kusto.Tests/Extensions/DataReaderExtensionsTests.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,10 @@ public void ReadObjects_Will_Return_Objects_Read_From_DataReader(
3333
.GetValues(null!)
3434
.ReturnsForAnyArgs(c => c.Arg<object[]>().CopyFrom(values[index], 0));
3535

36+
dataReader
37+
.GetValue(0)
38+
.ReturnsForAnyArgs(c => values[index][c.Arg<int>()]);
39+
3640
// Act
3741
var actual = dataReader.ReadObjects<TestObject>();
3842

@@ -66,6 +70,10 @@ public void CanReadObjects(
6670
.GetValues(null!)
6771
.ReturnsForAnyArgs(c => c.Arg<object[]>().CopyFrom(values[index], 0));
6872

73+
dataReader
74+
.GetValue(0)
75+
.ReturnsForAnyArgs(c => values[index][c.Arg<int>()]);
76+
6977
dataReader
7078
.NextResult()
7179
.Returns(true);

0 commit comments

Comments
 (0)