diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj
index dc5fc9dd51..9cccd8bae6 100644
--- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj
+++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj
@@ -615,6 +615,9 @@
Microsoft\Data\SqlTypes\SqlTypeWorkarounds.cs
+
+ Microsoft\Data\SqlTypes\SqlJson.cs
+
Resources\ResCategoryAttribute.cs
diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlDataReader.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlDataReader.cs
index 094692be03..70c91bc184 100644
--- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlDataReader.cs
+++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlDataReader.cs
@@ -14,6 +14,7 @@
using System.IO;
using System.Reflection;
using System.Runtime.CompilerServices;
+using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using System.Xml;
@@ -2538,6 +2539,18 @@ virtual public SqlXml GetSqlXml(int i)
return sx;
}
+ ///
+ /// Retrieves the column at ordinal as a SqlJson.
+ ///
+ ///
+ ///
+ virtual public SqlJson GetSqlJson(int i)
+ {
+ ReadColumn(i);
+ SqlJson json = _data[i].IsNull ? SqlJson.Null : _data[i].SqlJson;
+ return json;
+ }
+
///
virtual public object GetSqlValue(int i)
{
@@ -2991,6 +3004,16 @@ private T GetFieldValueFromSqlBufferInternal(SqlBuffer data, _SqlMetaData met
return (T)(object)new MemoryStream(value, writable: false);
}
}
+ else if (typeof(T) == typeof(JsonDocument))
+ {
+ MetaType metaType = metaData.metaType;
+ if (metaType.SqlDbType != SqlDbTypeExtensions.Json)
+ {
+ throw SQL.JsonDocumentNotSupportedOnColumnType(metaData.column);
+ }
+ JsonDocument document = JsonDocument.Parse(data.Value as string);
+ return (T)(object)document;
+ }
else
{
if (typeof(INullable).IsAssignableFrom(typeof(T)))
diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParser.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParser.cs
index f8865b5540..02b26a3a1b 100644
--- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParser.cs
+++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParser.cs
@@ -5958,7 +5958,6 @@ private TdsOperationStatus TryReadSqlStringValue(SqlBuffer value, byte type, int
case TdsEnums.SQLVARCHAR:
case TdsEnums.SQLBIGVARCHAR:
case TdsEnums.SQLTEXT:
- case TdsEnums.SQLJSON:
// If bigvarchar(max), we only read the first chunk here,
// expecting the caller to read the rest
if (encoding == null)
@@ -5976,6 +5975,17 @@ private TdsOperationStatus TryReadSqlStringValue(SqlBuffer value, byte type, int
value.SetToString(stringValue);
break;
+ case TdsEnums.SQLJSON:
+ encoding = Encoding.UTF8;
+ string jsonStringValue;
+ result = stateObj.TryReadStringWithEncoding(length, encoding, isPlp, out jsonStringValue);
+ if (result != TdsOperationStatus.Done)
+ {
+ return result;
+ }
+ value.SetToJson(jsonStringValue);
+ break;
+
case TdsEnums.SQLNCHAR:
case TdsEnums.SQLNVARCHAR:
case TdsEnums.SQLNTEXT:
@@ -9598,7 +9608,8 @@ private Task TDSExecuteRPCAddParameter(TdsParserStateObject stateObj, SqlParamet
mt.TDSType != TdsEnums.SQLXMLTYPE &&
mt.TDSType != TdsEnums.SQLIMAGE &&
mt.TDSType != TdsEnums.SQLTEXT &&
- mt.TDSType != TdsEnums.SQLNTEXT, "Type unsupported for encryption");
+ mt.TDSType != TdsEnums.SQLNTEXT &&
+ mt.TDSType != TdsEnums.SQLJSON, "Type unsupported for encryption");
byte[] serializedValue = null;
byte[] encryptedValue = null;
diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft.Data.SqlClient.csproj b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft.Data.SqlClient.csproj
index 9e4fffdfa1..1e1a5a864a 100644
--- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft.Data.SqlClient.csproj
+++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft.Data.SqlClient.csproj
@@ -636,6 +636,9 @@
Microsoft\Data\SqlTypes\SqlTypeWorkarounds.cs
+
+ Microsoft\Data\SqlTypes\SqlJson.cs
+
Resources\ResCategoryAttribute.cs
diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlDataReader.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlDataReader.cs
index 6148a46d3e..0c0b15a330 100644
--- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlDataReader.cs
+++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlDataReader.cs
@@ -13,6 +13,7 @@
using System.IO;
using System.Reflection;
using System.Runtime.CompilerServices;
+using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using System.Xml;
@@ -2890,6 +2891,19 @@ virtual public SqlXml GetSqlXml(int i)
return sx;
}
+ ///
+ /// Retrieves the column at ordinal as a SqlJson.
+ ///
+ ///
+ ///
+ virtual public SqlJson GetSqlJson(int i)
+ {
+ ReadColumn(i);
+ SqlJson json = _data[i].IsNull ? SqlJson.Null : _data[i].SqlJson;
+
+ return json;
+ }
+
///
virtual public object GetSqlValue(int i)
{
@@ -3337,6 +3351,16 @@ private T GetFieldValueFromSqlBufferInternal(SqlBuffer data, _SqlMetaData met
return (T)(object)new MemoryStream(value, writable: false);
}
}
+ else if (typeof(T) == typeof(JsonDocument))
+ {
+ MetaType metaType = metaData.metaType;
+ if (metaType.SqlDbType != SqlDbTypeExtensions.Json)
+ {
+ throw SQL.JsonDocumentNotSupportedOnColumnType(metaData.column);
+ }
+ JsonDocument document = JsonDocument.Parse(data.Value as string);
+ return (T)(object)document;
+ }
else
{
if (typeof(INullable).IsAssignableFrom(typeof(T)))
diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/TdsParser.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/TdsParser.cs
index cc91798a0c..018078a66f 100644
--- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/TdsParser.cs
+++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/TdsParser.cs
@@ -6805,7 +6805,6 @@ private TdsOperationStatus TryReadSqlStringValue(SqlBuffer value, byte type, int
case TdsEnums.SQLVARCHAR:
case TdsEnums.SQLBIGVARCHAR:
case TdsEnums.SQLTEXT:
- case TdsEnums.SQLJSON:
// If bigvarchar(max), we only read the first chunk here,
// expecting the caller to read the rest
if (encoding == null)
@@ -6822,6 +6821,16 @@ private TdsOperationStatus TryReadSqlStringValue(SqlBuffer value, byte type, int
}
value.SetToString(stringValue);
break;
+ case TdsEnums.SQLJSON:
+ encoding = Encoding.UTF8;
+ string jsonStringValue;
+ result = stateObj.TryReadStringWithEncoding(length, encoding, isPlp, out jsonStringValue);
+ if (result != TdsOperationStatus.Done)
+ {
+ return result;
+ }
+ value.SetToJson(jsonStringValue);
+ break;
case TdsEnums.SQLNCHAR:
case TdsEnums.SQLNVARCHAR:
@@ -10349,7 +10358,8 @@ internal Task TdsExecuteRPC(SqlCommand cmd, IList<_SqlRPC> rpcArray, int timeout
mt.TDSType != TdsEnums.SQLXMLTYPE &&
mt.TDSType != TdsEnums.SQLIMAGE &&
mt.TDSType != TdsEnums.SQLTEXT &&
- mt.TDSType != TdsEnums.SQLNTEXT, "Type unsupported for encryption");
+ mt.TDSType != TdsEnums.SQLNTEXT &&
+ mt.TDSType != TdsEnums.SQLJSON, "Type unsupported for encryption");
byte[] serializedValue = null;
byte[] encryptedValue = null;
diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlBuffer.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlBuffer.cs
index f28bf6033b..f3ee06c18f 100644
--- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlBuffer.cs
+++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlBuffer.cs
@@ -36,6 +36,7 @@ internal enum StorageType
DateTime2,
DateTimeOffset,
Time,
+ Json,
}
internal struct DateTimeInfo
@@ -486,7 +487,7 @@ internal string String
{
ThrowIfNull();
- if (StorageType.String == _type)
+ if (StorageType.String == _type || StorageType.Json == _type)
{
return (string)_object;
}
@@ -916,7 +917,8 @@ internal SqlString SqlString
{
get
{
- if (StorageType.String == _type)
+ // String and Json storage type are both strings.
+ if (StorageType.String == _type || StorageType.Json == _type)
{
if (IsNull)
{
@@ -937,6 +939,22 @@ internal SqlString SqlString
}
}
+ internal SqlJson SqlJson
+ {
+ get
+ {
+ if (StorageType.Json == _type)
+ {
+ if (IsNull)
+ {
+ return SqlTypes.SqlJson.Null;
+ }
+ return new SqlJson((string)_object);
+ }
+ return (SqlJson)SqlValue;
+ }
+ }
+
internal object SqlValue
{
get
@@ -969,7 +987,8 @@ internal object SqlValue
return SqlSingle;
case StorageType.String:
return SqlString;
-
+ case StorageType.Json:
+ return SqlJson;
case StorageType.SqlCachedBuffer:
{
SqlCachedBuffer data = (SqlCachedBuffer)(_object);
@@ -1087,6 +1106,8 @@ internal object Value
return DateTimeOffset;
case StorageType.Time:
return Time;
+ case StorageType.Json:
+ return String;
}
return null; // need to return the value as an object of some CLS type
}
@@ -1132,6 +1153,8 @@ internal Type GetTypeFromStorageType(bool isSqlType)
return typeof(SqlGuid);
case StorageType.SqlXml:
return typeof(SqlXml);
+ case StorageType.Json:
+ return typeof(SqlJson);
// Time Date DateTime2 and DateTimeOffset have no direct Sql type to contain them
}
}
@@ -1179,6 +1202,8 @@ internal Type GetTypeFromStorageType(bool isSqlType)
return typeof(DateTime);
case StorageType.DateTimeOffset:
return typeof(DateTimeOffset);
+ case StorageType.Json:
+ return typeof(string);
#if NET6_0_OR_GREATER
case StorageType.Time:
return typeof(TimeOnly);
@@ -1274,6 +1299,14 @@ internal void SetToString(string value)
_isNull = false;
}
+ internal void SetToJson(string value)
+ {
+ Debug.Assert(IsEmpty, "setting value a second time?");
+ _object = value;
+ _type = StorageType.Json;
+ _isNull = false;
+ }
+
internal void SetToDate(ReadOnlySpan bytes)
{
Debug.Assert(IsEmpty, "setting value a second time?");
diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlEnums.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlEnums.cs
index fe7bfc7f9a..dafb0c5ef5 100644
--- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlEnums.cs
+++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlEnums.cs
@@ -16,6 +16,7 @@
using System.IO;
using System.Xml;
using Microsoft.Data.Common;
+using Microsoft.Data.SqlTypes;
using Microsoft.SqlServer.Server;
namespace Microsoft.Data.SqlClient
@@ -364,6 +365,8 @@ private static MetaType GetMetaTypeFromValue(Type dataType, object value, bool i
return s_metaReal;
else if (dataType == typeof(SqlXml))
return MetaXml;
+ else if (dataType == typeof(SqlJson))
+ return s_MetaJson;
else if (dataType == typeof(SqlString))
{
return ((inferLen && !((SqlString)value).IsNull)
diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlParameter.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlParameter.cs
index 65cd5d1e9b..3f79ff7c0b 100644
--- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlParameter.cs
+++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlParameter.cs
@@ -18,6 +18,7 @@
using System.Xml;
using Microsoft.Data.Common;
using Microsoft.Data.SqlClient.Server;
+using Microsoft.Data.SqlTypes;
namespace Microsoft.Data.SqlClient
{
@@ -2247,6 +2248,10 @@ internal static object CoerceValue(object value, MetaType destinationType, out b
{
value = MetaType.GetStringFromXml((XmlReader)(((SqlXml)value).CreateReader()));
}
+ else if (currentType == typeof(SqlJson))
+ {
+ value = (value as SqlJson).Value;
+ }
else if (currentType == typeof(SqlString))
{
typeChanged = false; // Do nothing
diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlUtil.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlUtil.cs
index 154872a8e4..01c777d74b 100644
--- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlUtil.cs
+++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlUtil.cs
@@ -987,6 +987,11 @@ internal static Exception StreamNotSupportOnColumnType(string columnName)
return ADP.InvalidCast(StringsHelper.GetString(Strings.SQL_StreamNotSupportOnColumnType, columnName));
}
+ internal static Exception JsonDocumentNotSupportedOnColumnType(string columnName)
+ {
+ return ADP.InvalidCast(StringsHelper.GetString(Strings.SQL_JsonDocumentNotSupportedOnColumnType, columnName));
+ }
+
internal static Exception StreamNotSupportOnEncryptedColumn(string columnName)
{
return ADP.InvalidOperation(StringsHelper.GetString(Strings.TCE_StreamNotSupportOnEncryptedColumn, columnName, "Stream"));
diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlTypes/SqlJson.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlTypes/SqlJson.cs
new file mode 100644
index 0000000000..95c5311441
--- /dev/null
+++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlTypes/SqlJson.cs
@@ -0,0 +1,118 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+
+using System.Data.SqlTypes;
+using System.Text;
+using System.Text.Json;
+
+#nullable enable
+
+namespace Microsoft.Data.SqlTypes
+{
+ ///
+ /// Represents the Json Data type in SQL Server.
+ ///
+ public class SqlJson : INullable
+ {
+
+ ///
+ /// True if null.
+ ///
+ private bool _isNull;
+
+ private readonly string? _jsonString;
+
+ ///
+ /// Parameterless constructor. Initializes a new instance of the SqlJson class which
+ /// represents a null JSON value.
+ ///
+ public SqlJson()
+ {
+ SetNull();
+ }
+
+ ///
+ /// Takes a as input and initializes a new instance of the SqlJson class.
+ ///
+ ///
+ public SqlJson(string? jsonString)
+ {
+ if (jsonString == null)
+ {
+ SetNull();
+ }
+ else
+ {
+ // TODO: We need to validate the Json before storing it.
+ ValidateJson(jsonString);
+ _jsonString = jsonString;
+ }
+ }
+
+ ///
+ /// Takes a as input and initializes a new instance of the SqlJson class.
+ ///
+ ///
+ public SqlJson(JsonDocument? jsonDoc)
+ {
+ if (jsonDoc == null)
+ {
+ SetNull();
+ }
+ else
+ {
+ _jsonString = jsonDoc.RootElement.GetRawText();
+ }
+ }
+
+ ///
+ public bool IsNull => _isNull;
+
+ ///
+ /// Represents a null instance of the type.
+ ///
+ public static SqlJson Null => new();
+
+ ///
+ /// Gets the string representation of the Json content of this instance.
+ ///
+ public string Value
+ {
+ get
+ {
+ if (IsNull)
+ {
+ throw new SqlNullValueException();
+ }
+ else
+ {
+ return _jsonString!;
+ }
+ }
+ }
+
+ private void SetNull()
+ {
+ _isNull = true;
+ }
+
+ private static void ValidateJson(string jsonString)
+ {
+ // Convert the JSON string to a UTF-8 byte array
+ byte[] jsonBytes = Encoding.UTF8.GetBytes(jsonString);
+
+ // Create a Utf8JsonReader instance
+ var reader = new Utf8JsonReader(jsonBytes, isFinalBlock: true, state: default);
+
+ // Read through the JSON data
+ while (reader.Read())
+ {
+ // The Read method advances the reader to the next token
+ // If the JSON is invalid, an exception will be thrown
+ }
+ // If we reach here, the JSON is valid
+ }
+ }
+}
diff --git a/src/Microsoft.Data.SqlClient/src/Resources/Strings.Designer.cs b/src/Microsoft.Data.SqlClient/src/Resources/Strings.Designer.cs
index a685787e9e..8302a288fe 100644
--- a/src/Microsoft.Data.SqlClient/src/Resources/Strings.Designer.cs
+++ b/src/Microsoft.Data.SqlClient/src/Resources/Strings.Designer.cs
@@ -10484,7 +10484,16 @@ internal static string SQL_StreamNotSupportOnColumnType {
return ResourceManager.GetString("SQL_StreamNotSupportOnColumnType", resourceCulture);
}
}
-
+
+ ///
+ /// Looks up a localized string similar to Invalid attempt to get JsonDocument on column '{0}'. JsonDocument is only supported for columns of type json.
+ ///
+ internal static string SQL_JsonDocumentNotSupportedOnColumnType {
+ get {
+ return ResourceManager.GetString("SQL_JsonDocumentNotSupportedOnColumnType", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to The Stream does not support reading..
///
diff --git a/src/Microsoft.Data.SqlClient/src/Resources/Strings.resx b/src/Microsoft.Data.SqlClient/src/Resources/Strings.resx
index c2dd68b867..facdb0ed04 100644
--- a/src/Microsoft.Data.SqlClient/src/Resources/Strings.resx
+++ b/src/Microsoft.Data.SqlClient/src/Resources/Strings.resx
@@ -4740,4 +4740,7 @@
The certificate provided by the server does not match the certificate provided by the ServerCertificate option.
+
+ Invalid attempt to get JsonDocument on column '{0}'. JsonDocument is only supported for columns of type json.
+
diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/Json/SqlJsonTest.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/Json/SqlJsonTest.cs
new file mode 100644
index 0000000000..03ead359f0
--- /dev/null
+++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/Json/SqlJsonTest.cs
@@ -0,0 +1,151 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Data.SqlTypes;
+using System.Linq;
+using System.Text.Json;
+using Microsoft.Data.SqlTypes;
+using Xunit;
+
+namespace Microsoft.Data.SqlClient.ManualTesting.Tests.Json
+{
+
+ public class SqlJsonTest
+ {
+ [Fact]
+ public void SqlJsonTest_Null()
+ {
+ SqlJson json = new();
+ Assert.True(json.IsNull);
+ Assert.Throws(() => json.Value);
+
+ }
+
+ [Fact]
+ public void SqlJsonTest_NullString()
+ {
+ string nullString = null;
+ SqlJson json = new(nullString);
+ Assert.True(json.IsNull);
+ Assert.Throws(() => json.Value);
+ }
+
+ [Fact]
+ public void SqlJsonTest_NullJsonDocument()
+ {
+ JsonDocument doc = null;
+ SqlJson json = new(doc);
+ Assert.True(json.IsNull);
+ Assert.Throws(() => json.Value);
+ }
+
+ [Fact]
+ public void SqlJsonTest_String()
+ {
+ SqlJson json = new("{\"key\":\"value\"}");
+ Assert.False(json.IsNull);
+ Assert.Equal("{\"key\":\"value\"}", json.Value);
+ }
+
+ [Fact]
+ public void SqlJsonTest_BadString()
+ {
+ Assert.ThrowsAny(()=> new SqlJson("{\"key\":\"value\""));
+ }
+
+ [Fact]
+ public void SqlJsonTest_JsonDocument()
+ {
+ JsonDocument doc = GenerateRandomJson();
+ SqlJson json = new(doc);
+ Assert.False(json.IsNull);
+
+ var outputDocument = JsonDocument.Parse(json.Value);
+ Assert.True(JsonElementsAreEqual(doc.RootElement, outputDocument.RootElement));
+ }
+
+ [Fact]
+ public void SqlJsonTest_NullProperty()
+ {
+ SqlJson json = SqlJson.Null;
+ Assert.True(json.IsNull);
+ Assert.Throws(() => json.Value);
+ }
+
+ static JsonDocument GenerateRandomJson()
+ {
+ var random = new Random();
+
+ var jsonObject = new
+ {
+ id = random.Next(1, 1000),
+ name = $"Name{random.Next(1, 100)}",
+ isActive = random.Next(0, 2) == 1,
+ createdDate = DateTime.Now.AddDays(-random.Next(1, 100)).ToString("yyyy-MM-ddTHH:mm:ssZ"),
+ scores = new int[] { random.Next(1, 100), random.Next(1, 100), random.Next(1, 100) },
+ details = new
+ {
+ age = random.Next(18, 60),
+ city = $"City{random.Next(1, 100)}"
+ }
+ };
+
+ string jsonString = JsonSerializer.Serialize(jsonObject, new JsonSerializerOptions { WriteIndented = true });
+ return JsonDocument.Parse(jsonString);
+ }
+
+ static bool JsonElementsAreEqual(JsonElement element1, JsonElement element2)
+ {
+ if (element1.ValueKind != element2.ValueKind)
+ return false;
+
+ switch (element1.ValueKind)
+ {
+ case JsonValueKind.Object:
+ {
+ JsonElement.ObjectEnumerator obj1 = element1.EnumerateObject();
+ JsonElement.ObjectEnumerator obj2 = element2.EnumerateObject();
+ var dict1 = obj1.ToDictionary(p => p.Name, p => p.Value);
+ var dict2 = obj2.ToDictionary(p => p.Name, p => p.Value);
+
+ if (dict1.Count != dict2.Count)
+ return false;
+
+ foreach (var kvp in dict1)
+ {
+ if (!dict2.TryGetValue(kvp.Key, out var value2))
+ return false;
+
+ if (!JsonElementsAreEqual(kvp.Value, value2))
+ return false;
+ }
+
+ return true;
+ }
+ case JsonValueKind.Array:
+ {
+ var array1 = element1.EnumerateArray();
+ var array2 = element2.EnumerateArray();
+
+ if (array1.Count() != array2.Count())
+ return false;
+
+ return array1.Zip(array2, (e1, e2) => JsonElementsAreEqual(e1, e2)).All(equal => equal);
+ }
+ case JsonValueKind.String:
+ return element1.GetString() == element2.GetString();
+ case JsonValueKind.Number:
+ return element1.GetDecimal() == element2.GetDecimal();
+ case JsonValueKind.True:
+ case JsonValueKind.False:
+ return element1.GetBoolean() == element2.GetBoolean();
+ case JsonValueKind.Null:
+ return true;
+ default:
+ throw new NotSupportedException($"Unsupported JsonValueKind: {element1.ValueKind}");
+ }
+ }
+ }
+}
diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/Microsoft.Data.SqlClient.ManualTesting.Tests.csproj b/src/Microsoft.Data.SqlClient/tests/ManualTests/Microsoft.Data.SqlClient.ManualTesting.Tests.csproj
index 7a3dad589d..61cd5ef327 100644
--- a/src/Microsoft.Data.SqlClient/tests/ManualTests/Microsoft.Data.SqlClient.ManualTesting.Tests.csproj
+++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/Microsoft.Data.SqlClient.ManualTesting.Tests.csproj
@@ -277,6 +277,7 @@
+