From b842414d976454e9fc02c4c8eca60b3f3d96d34f Mon Sep 17 00:00:00 2001
From: Paul Medynski <31868385+paulmedynski@users.noreply.github.com>
Date: Fri, 29 May 2026 08:58:57 -0300
Subject: [PATCH 1/2] Replace Newtonsoft.Json with System.Text.Json in tests
and samples
- Migrate JsonBulkCopyTest and JsonStreamTest to System.Text.Json
- Add JsonTestHelper with DeepEquals supporting net8.0+ and net9.0+
- Rewrite SqlExceptionTest to serialize key properties via STJ
- Remove JSONSerializationTest (tested Newtonsoft-specific ISerializable)
- Delete legacy AzureKeyVaultProviderLegacyExample_2_0.cs (marked TODO)
- Remove Newtonsoft.Json from all csproj files and Directory.Packages.props
Tracks: https://github.com/dotnet/SqlClient/issues/4322
---
Directory.Packages.props | 1 -
.../AzureKeyVaultProviderLegacyExample_2_0.cs | 380 ------------------
.../Microsoft.Data.SqlClient.Samples.csproj | 1 -
...soft.Data.SqlClient.FunctionalTests.csproj | 2 -
.../tests/FunctionalTests/SqlExceptionTest.cs | 57 +--
...icrosoft.Data.SqlClient.ManualTests.csproj | 3 +-
.../SQL/JsonTest/JsonBulkCopyTest.cs | 20 +-
.../SQL/JsonTest/JsonStreamTest.cs | 18 +-
.../SQL/JsonTest/JsonTestHelper.cs | 81 ++++
9 files changed, 113 insertions(+), 450 deletions(-)
delete mode 100644 doc/samples/AzureKeyVaultProviderLegacyExample_2_0.cs
create mode 100644 src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/JsonTest/JsonTestHelper.cs
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 37b72ca205..801213b749 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -71,7 +71,6 @@
-
diff --git a/doc/samples/AzureKeyVaultProviderLegacyExample_2_0.cs b/doc/samples/AzureKeyVaultProviderLegacyExample_2_0.cs
deleted file mode 100644
index d397de6441..0000000000
--- a/doc/samples/AzureKeyVaultProviderLegacyExample_2_0.cs
+++ /dev/null
@@ -1,380 +0,0 @@
-/**
- * TODO: This sample file should be deleted as the AKV Provider Ctor API is no longer supported with supported versions of AKV provider and MDS.
- * Depends on: Delete documentation and sample reference in MS Docs first: https://learn.microsoft.com/en-us/sql/connect/ado-net/sql/azure-key-vault-example?view=sql-server-ver17#legacy-callback-implementation-design-example-with-v20
- *
-
-//< Snippet1>
-using System;
-using System.Collections.Generic;
-using System.IdentityModel.Tokens.Jwt;
-using System.Linq;
-using System.Net.Http;
-using System.Security.Cryptography;
-using System.Threading;
-using System.Threading.Tasks;
-using Azure.Core;
-using Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider;
-using Newtonsoft.Json;
-using Newtonsoft.Json.Linq;
-
-namespace Microsoft.Data.SqlClient.Samples
-{
- public class AzureKeyVaultProviderLegacyExample_2_0
- {
- const string s_algorithm = "RSA_OAEP";
-
- // ********* Provide details here ***********
- static readonly string s_akvUrl = "https://{KeyVaultName}.vault.azure.net/keys/{Key}/{KeyIdentifier}";
- static readonly string s_clientId = "{Application_Client_ID}";
- static readonly string s_clientSecret = "{Application_Client_Secret}";
- static readonly string s_connectionString = "Server={Server}; Database={database}; Integrated Security=true; Column Encryption Setting=Enabled;";
- // ******************************************
-
- public static void Main()
- {
- // Initialize AKV provider
- SqlColumnEncryptionAzureKeyVaultProvider akvProvider = new SqlColumnEncryptionAzureKeyVaultProvider(new LegacyAuthCallbackTokenCredential());
-
- // Register AKV provider
- SqlConnection.RegisterColumnEncryptionKeyStoreProviders(customProviders: new Dictionary(capacity: 1, comparer: StringComparer.OrdinalIgnoreCase)
- {
- { SqlColumnEncryptionAzureKeyVaultProvider.ProviderName, akvProvider}
- });
- Console.WriteLine("AKV provider Registered");
-
- // Create connection to database
- using (SqlConnection sqlConnection = new SqlConnection(s_connectionString))
- {
- string cmkName = "CMK_WITH_AKV";
- string cekName = "CEK_WITH_AKV";
- string tblName = "AKV_TEST_TABLE";
-
- CustomerRecord customer = new CustomerRecord(1, @"Microsoft", @"Corporation");
-
- try
- {
- sqlConnection.Open();
-
- // Drop Objects if exists
- dropObjects(sqlConnection, cmkName, cekName, tblName);
-
- // Create Column Master Key with AKV Url
- createCMK(sqlConnection, cmkName);
- Console.WriteLine("Column Master Key created.");
-
- // Create Column Encryption Key
- createCEK(sqlConnection, cmkName, cekName, akvProvider);
- Console.WriteLine("Column Encryption Key created.");
-
- // Create Table with Encrypted Columns
- createTbl(sqlConnection, cekName, tblName);
- Console.WriteLine("Table created with Encrypted columns.");
-
- // Insert Customer Record in table
- insertData(sqlConnection, tblName, customer);
- Console.WriteLine("Encryted data inserted.");
-
- // Read data from table
- verifyData(sqlConnection, tblName, customer);
- Console.WriteLine("Data validated successfully.");
- }
- finally
- {
- // Drop table and keys
- dropObjects(sqlConnection, cmkName, cekName, tblName);
- Console.WriteLine("Dropped Table, CEK and CMK");
- }
-
- Console.WriteLine("Completed AKV provider Sample.");
- }
- }
-
- private static void createCMK(SqlConnection sqlConnection, string cmkName)
- {
- string KeyStoreProviderName = SqlColumnEncryptionAzureKeyVaultProvider.ProviderName;
-
- string sql =
- $@"CREATE COLUMN MASTER KEY [{cmkName}]
- WITH (
- KEY_STORE_PROVIDER_NAME = N'{KeyStoreProviderName}',
- KEY_PATH = N'{s_akvUrl}'
- );";
-
- using (SqlCommand command = sqlConnection.CreateCommand())
- {
- command.CommandText = sql;
- command.ExecuteNonQuery();
- }
- }
-
- private static void createCEK(SqlConnection sqlConnection, string cmkName, string cekName, SqlColumnEncryptionAzureKeyVaultProvider sqlColumnEncryptionAzureKeyVaultProvider)
- {
- string sql =
- $@"CREATE COLUMN ENCRYPTION KEY [{cekName}]
- WITH VALUES (
- COLUMN_MASTER_KEY = [{cmkName}],
- ALGORITHM = '{s_algorithm}',
- ENCRYPTED_VALUE = {GetEncryptedValue(sqlColumnEncryptionAzureKeyVaultProvider)}
- )";
-
- using (SqlCommand command = sqlConnection.CreateCommand())
- {
- command.CommandText = sql;
- command.ExecuteNonQuery();
- }
- }
-
- private static string GetEncryptedValue(SqlColumnEncryptionAzureKeyVaultProvider sqlColumnEncryptionAzureKeyVaultProvider)
- {
- byte[] plainTextColumnEncryptionKey = new byte[32];
- RandomNumberGenerator rng = RandomNumberGenerator.Create();
- rng.GetBytes(plainTextColumnEncryptionKey);
-
- byte[] encryptedColumnEncryptionKey = sqlColumnEncryptionAzureKeyVaultProvider.EncryptColumnEncryptionKey(s_akvUrl, s_algorithm, plainTextColumnEncryptionKey);
- string EncryptedValue = string.Concat("0x", BitConverter.ToString(encryptedColumnEncryptionKey).Replace("-", string.Empty));
- return EncryptedValue;
- }
-
- private static void createTbl(SqlConnection sqlConnection, string cekName, string tblName)
- {
- string ColumnEncryptionAlgorithmName = @"AEAD_AES_256_CBC_HMAC_SHA_256";
-
- string sql =
- $@"CREATE TABLE [dbo].[{tblName}]
- (
- [CustomerId] [int] ENCRYPTED WITH (COLUMN_ENCRYPTION_KEY = [{cekName}], ENCRYPTION_TYPE = DETERMINISTIC, ALGORITHM = '{ColumnEncryptionAlgorithmName}'),
- [FirstName] [nvarchar](50) COLLATE Latin1_General_BIN2 ENCRYPTED WITH (COLUMN_ENCRYPTION_KEY = [{cekName}], ENCRYPTION_TYPE = DETERMINISTIC, ALGORITHM = '{ColumnEncryptionAlgorithmName}'),
- [LastName] [nvarchar](50) COLLATE Latin1_General_BIN2 ENCRYPTED WITH (COLUMN_ENCRYPTION_KEY = [{cekName}], ENCRYPTION_TYPE = DETERMINISTIC, ALGORITHM = '{ColumnEncryptionAlgorithmName}')
- )";
-
- using (SqlCommand command = sqlConnection.CreateCommand())
- {
- command.CommandText = sql;
- command.ExecuteNonQuery();
- }
- }
-
- private static void insertData(SqlConnection sqlConnection, string tblName, CustomerRecord customer)
- {
- string insertSql = $"INSERT INTO [{tblName}] (CustomerId, FirstName, LastName) VALUES (@CustomerId, @FirstName, @LastName);";
-
- using (SqlTransaction sqlTransaction = sqlConnection.BeginTransaction())
- using (SqlCommand sqlCommand = new SqlCommand(insertSql,
- connection: sqlConnection, transaction: sqlTransaction,
- columnEncryptionSetting: SqlCommandColumnEncryptionSetting.Enabled))
- {
- sqlCommand.Parameters.AddWithValue(@"CustomerId", customer.Id);
- sqlCommand.Parameters.AddWithValue(@"FirstName", customer.FirstName);
- sqlCommand.Parameters.AddWithValue(@"LastName", customer.LastName);
-
- sqlCommand.ExecuteNonQuery();
- sqlTransaction.Commit();
- }
- }
-
- private static void verifyData(SqlConnection sqlConnection, string tblName, CustomerRecord customer)
- {
- // Test INPUT parameter on an encrypted parameter
- using (SqlCommand sqlCommand = new SqlCommand($"SELECT CustomerId, FirstName, LastName FROM [{tblName}] WHERE FirstName = @firstName",
- sqlConnection))
- {
- SqlParameter customerFirstParam = sqlCommand.Parameters.AddWithValue(@"firstName", @"Microsoft");
- customerFirstParam.Direction = System.Data.ParameterDirection.Input;
- customerFirstParam.ForceColumnEncryption = true;
-
- using (SqlDataReader sqlDataReader = sqlCommand.ExecuteReader())
- {
- ValidateResultSet(sqlDataReader);
- }
- }
- }
-
- private static void ValidateResultSet(SqlDataReader sqlDataReader)
- {
- Console.WriteLine(" * Row available: " + sqlDataReader.HasRows);
-
- while (sqlDataReader.Read())
- {
- if (sqlDataReader.GetInt32(0) == 1)
- {
- Console.WriteLine(" * Employee Id received as sent: " + sqlDataReader.GetInt32(0));
- }
- else
- {
- Console.WriteLine("Employee Id didn't match");
- }
-
- if (sqlDataReader.GetString(1) == @"Microsoft")
- {
- Console.WriteLine(" * Employee Firstname received as sent: " + sqlDataReader.GetString(1));
- }
- else
- {
- Console.WriteLine("Employee FirstName didn't match.");
- }
-
- if (sqlDataReader.GetString(2) == @"Corporation")
- {
- Console.WriteLine(" * Employee LastName received as sent: " + sqlDataReader.GetString(2));
- }
- else
- {
- Console.WriteLine("Employee LastName didn't match.");
- }
- }
- }
-
- private static void dropObjects(SqlConnection sqlConnection, string cmkName, string cekName, string tblName)
- {
- using (SqlCommand cmd = sqlConnection.CreateCommand())
- {
- cmd.CommandText = $@"IF EXISTS (select * from sys.objects where name = '{tblName}') BEGIN DROP TABLE [{tblName}] END";
- cmd.ExecuteNonQuery();
- cmd.CommandText = $@"IF EXISTS (select * from sys.column_encryption_keys where name = '{cekName}') BEGIN DROP COLUMN ENCRYPTION KEY [{cekName}] END";
- cmd.ExecuteNonQuery();
- cmd.CommandText = $@"IF EXISTS (select * from sys.column_master_keys where name = '{cmkName}') BEGIN DROP COLUMN MASTER KEY [{cmkName}] END";
- cmd.ExecuteNonQuery();
- }
- }
-
- private class CustomerRecord
- {
- internal int Id { get; set; }
- internal string FirstName { get; set; }
- internal string LastName { get; set; }
-
- public CustomerRecord(int id, string fName, string lName)
- {
- Id = id;
- FirstName = fName;
- LastName = lName;
- }
- }
-
- private class LegacyAuthCallbackTokenCredential : TokenCredential
- {
- string _authority = "";
- string _resource = "";
- string _akvUrl = "";
-
- public override AccessToken GetToken(TokenRequestContext requestContext, CancellationToken cancellationToken) =>
- AcquireTokenAsync().GetAwaiter().GetResult();
-
- public override async ValueTask GetTokenAsync(TokenRequestContext requestContext, CancellationToken cancellationToken) =>
- await AcquireTokenAsync();
-
- private async Task AcquireTokenAsync()
- {
- // Added to reduce HttpClient calls.
- // For multi-user support, a better design can be implemented as needed.
- if (_akvUrl != s_akvUrl)
- {
- using (HttpClient httpClient = new HttpClient())
- {
- HttpResponseMessage response = await httpClient.GetAsync(s_akvUrl);
- string challenge = response?.Headers.WwwAuthenticate.FirstOrDefault()?.ToString();
- string trimmedChallenge = ValidateChallenge(challenge);
- string[] pairs = trimmedChallenge.Split(new string[] { "," }, StringSplitOptions.RemoveEmptyEntries);
-
- if (pairs != null && pairs.Length > 0)
- {
- for (int i = 0; i < pairs.Length; i++)
- {
- string[] pair = pairs[i]?.Split('=');
-
- if (pair.Length == 2)
- {
- string key = pair[0]?.Trim().Trim(new char[] { '\"' });
- string value = pair[1]?.Trim().Trim(new char[] { '\"' });
-
- if (!string.IsNullOrEmpty(key))
- {
- if (key.Equals("authorization", StringComparison.InvariantCultureIgnoreCase))
- {
- _authority = value;
- }
- else if (key.Equals("resource", StringComparison.InvariantCultureIgnoreCase))
- {
- _resource = value;
- }
- }
- }
- }
- }
- }
- _akvUrl = s_akvUrl;
- }
-
- string strAccessToken = await AzureActiveDirectoryAuthenticationCallback(_authority, _resource);
- DateTime expiryTime = InterceptAccessTokenForExpiry(strAccessToken);
- return new AccessToken(strAccessToken, new DateTimeOffset(expiryTime));
- }
-
- private DateTime InterceptAccessTokenForExpiry(string accessToken)
- {
- if (null == accessToken)
- {
- throw new ArgumentNullException(accessToken);
- }
-
- var jwtHandler = new JwtSecurityTokenHandler();
- var jwtOutput = string.Empty;
-
- // Check Token Format
- if (!jwtHandler.CanReadToken(accessToken))
- throw new FormatException(accessToken);
-
- JwtSecurityToken token = jwtHandler.ReadJwtToken(accessToken);
-
- // Re-serialize the Token Headers to just Key and Values
- var jwtHeader = JsonConvert.SerializeObject(token.Header.Select(h => new { h.Key, h.Value }));
- jwtOutput = $"{{\r\n\"Header\":\r\n{JToken.Parse(jwtHeader)},";
-
- // Re-serialize the Token Claims to just Type and Values
- var jwtPayload = JsonConvert.SerializeObject(token.Claims.Select(c => new { c.Type, c.Value }));
- jwtOutput += $"\r\n\"Payload\":\r\n{JToken.Parse(jwtPayload)}\r\n}}";
-
- // Output the whole thing to pretty JSON object formatted.
- string jToken = JToken.Parse(jwtOutput).ToString(Formatting.Indented);
- JToken payload = JObject.Parse(jToken).GetValue("Payload");
-
- return new DateTime(1970, 1, 1).AddSeconds((long)payload[4]["Value"]);
- }
-
- private static string ValidateChallenge(string challenge)
- {
- string Bearer = "Bearer ";
- if (string.IsNullOrEmpty(challenge))
- throw new ArgumentNullException(nameof(challenge));
-
- string trimmedChallenge = challenge.Trim();
-
- if (!trimmedChallenge.StartsWith(Bearer))
- throw new ArgumentException("Challenge is not Bearer", nameof(challenge));
-
- return trimmedChallenge.Substring(Bearer.Length);
- }
-
- ///
- /// Legacy implementation of Authentication Callback, used by Azure Key Vault provider 1.0.
- /// This can be leveraged to support multi-user authentication support in the same Azure Key Vault Provider.
- ///
- /// Authorization URL
- /// Resource
- ///
- public static async Task AzureActiveDirectoryAuthenticationCallback(string authority, string resource)
- {
- var authContext = new AuthenticationContext(authority);
- ClientCredential clientCred = new ClientCredential(s_clientId, s_clientSecret);
- AuthenticationResult result = await authContext.AcquireTokenAsync(resource, clientCred);
- if (result == null)
- {
- throw new InvalidOperationException($"Failed to retrieve an access token for {resource}");
- }
- return result.AccessToken;
- }
- }
- }
-}
-//
-*/
diff --git a/doc/samples/Microsoft.Data.SqlClient.Samples.csproj b/doc/samples/Microsoft.Data.SqlClient.Samples.csproj
index 44a153f6f2..1887c9b73e 100644
--- a/doc/samples/Microsoft.Data.SqlClient.Samples.csproj
+++ b/doc/samples/Microsoft.Data.SqlClient.Samples.csproj
@@ -23,7 +23,6 @@
-
diff --git a/src/Microsoft.Data.SqlClient/tests/FunctionalTests/Microsoft.Data.SqlClient.FunctionalTests.csproj b/src/Microsoft.Data.SqlClient/tests/FunctionalTests/Microsoft.Data.SqlClient.FunctionalTests.csproj
index 8fe4c2be23..cbb8b9a215 100644
--- a/src/Microsoft.Data.SqlClient/tests/FunctionalTests/Microsoft.Data.SqlClient.FunctionalTests.csproj
+++ b/src/Microsoft.Data.SqlClient/tests/FunctionalTests/Microsoft.Data.SqlClient.FunctionalTests.csproj
@@ -67,7 +67,6 @@
-
@@ -92,7 +91,6 @@
-
diff --git a/src/Microsoft.Data.SqlClient/tests/FunctionalTests/SqlExceptionTest.cs b/src/Microsoft.Data.SqlClient/tests/FunctionalTests/SqlExceptionTest.cs
index 2fe9e63aed..c844978baa 100644
--- a/src/Microsoft.Data.SqlClient/tests/FunctionalTests/SqlExceptionTest.cs
+++ b/src/Microsoft.Data.SqlClient/tests/FunctionalTests/SqlExceptionTest.cs
@@ -5,7 +5,7 @@
using System;
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;
-using Newtonsoft.Json;
+using System.Text.Json;
using Xunit;
namespace Microsoft.Data.SqlClient.Tests
@@ -18,46 +18,27 @@ public class SqlExceptionTest
public void SerializationTest()
{
SqlException e = CreateException();
- string json = JsonConvert.SerializeObject(e);
- var settings = new JsonSerializerSettings();
- var sqlEx = JsonConvert.DeserializeObject(json, settings);
-
- Assert.Equal(e.ClientConnectionId, sqlEx.ClientConnectionId);
- Assert.Equal(e.StackTrace, sqlEx.StackTrace);
- }
+ // Serialize the properties we want to validate round-trip through JSON.
+ // SqlException cannot be directly serialized by System.Text.Json because
+ // Exception.TargetSite (MethodBase) is not supported.
+ string json = JsonSerializer.Serialize(new
+ {
+ e.Message,
+ ClientConnectionId = e.ClientConnectionId.ToString(),
+ e.Number,
+ e.Class,
+ e.State,
+ });
- [Fact]
- public void JSONSerializationTest()
- {
- string clientConnectionId = "90cdab4d-2145-4c24-a354-c8ccff903542";
- string json = @"{"
- + @"""ClassName"":""Microsoft.Data.SqlClient.SqlException"","
- + @"""Message"":""A network-related or instance-specific error occurred while establishing a connection to SQL Server. The server was not found or was not accessible. Verify that the instance name is correct and that SQL Server is configured to allow remote connections. (provider: TCP Provider, error: 40 - Could not open a connection to SQL Server)"","
- + @"""Data"":{""HelpLink.ProdName"":""Microsoft SQL Server"","
- + @"""HelpLink.EvtSrc"":""MSSQLServer"","
- + @"""HelpLink.EvtID"":""0"","
- + @"""HelpLink.BaseHelpUrl"":""http://go.microsoft.com/fwlink"","
- + @"""HelpLink.LinkId"":""20476"","
- + @"""SqlError 1"":""Microsoft.Data.SqlClient.SqlError: A network-related or instance-specific error occurred while establishing a connection to SQL Server. The server was not found or was not accessible. Verify that the instance name is correct and that SQL Server is configured to allow remote connections. (provider: TCP Provider, error: 40 - Could not open a connection to SQL Server)"","
- + @"""$type"":""System.Collections.ListDictionaryInternal, System.Private.CoreLib""},"
- + @"""InnerException"":null,"
- + @"""HelpURL"":null,"
- + @"""StackTraceString"":"" at Microsoft.Data.SqlClient.SqlInternalConnectionTds..ctor(DbConnectionPoolIdentity identity, SqlConnectionOptions connectionOptions, SqlCredential credential, Object providerInfo, String newPassword, SecureString newSecurePassword, Boolean redirectedUserInstance, SqlConnectionOptions userConnectionOptions, SessionData reconnectSessionData, Boolean applyTransientFaultHandling, String accessToken)\\n at Microsoft.Data.SqlClient.SqlConnectionFactory.CreateConnection(SqlConnectionOptions options, DbConnectionPoolKey poolKey, Object poolGroupProviderInfo, DbConnectionPool pool, DbConnection owningConnection, SqlConnectionOptions userOptions)\\n at System.Data.ProviderBase.DbConnectionFactory.CreatePooledConnection(DbConnectionPool pool, DbConnection owningObject, SqlConnectionOptions options, DbConnectionPoolKey poolKey, SqlConnectionOptions userOptions)\\n at System.Data.ProviderBase.DbConnectionPool.CreateObject(DbConnection owningObject, SqlConnectionOptions userOptions, DbConnectionInternal oldConnection)\\n at System.Data.ProviderBase.DbConnectionPool.UserCreateRequest(DbConnection owningObject, SqlConnectionOptions userOptions, DbConnectionInternal oldConnection)\\n at System.Data.ProviderBase.DbConnectionPool.TryGetConnection(DbConnection owningObject, UInt32 waitForMultipleObjectsTimeout, Boolean allowCreate, Boolean onlyOneCheckConnection, SqlConnectionOptions userOptions, DbConnectionInternal& connection)\\n at System.Data.ProviderBase.DbConnectionPool.WaitForPendingOpen()\\n"","
- + @"""RemoteStackTraceString"":null,"
- + @"""RemoteStackIndex"":0,"
- + @"""ExceptionMethod"":null,"
- + @"""HResult"":-2146232060,"
- + @"""Source"":""Core .Net SqlClient Data Provider"","
- + @"""WatsonBuckets"":null,"
- + @"""Errors"":null,"
- + @"""ClientConnectionId"":""90cdab4d-2145-4c24-a354-c8ccff903542"""
- + @"}";
+ using JsonDocument doc = JsonDocument.Parse(json);
+ JsonElement root = doc.RootElement;
- var settings = new JsonSerializerSettings();
- var sqlEx = JsonConvert.DeserializeObject(json, settings);
- Assert.IsType(sqlEx);
- Assert.Equal(clientConnectionId, sqlEx.ClientConnectionId.ToString());
+ Assert.Equal(e.Message, root.GetProperty("Message").GetString());
+ Assert.Equal(e.ClientConnectionId.ToString(), root.GetProperty("ClientConnectionId").GetString());
+ Assert.Equal(e.Number, root.GetProperty("Number").GetInt32());
+ Assert.Equal(e.Class, root.GetProperty("Class").GetByte());
+ Assert.Equal(e.State, root.GetProperty("State").GetByte());
}
private static SqlException CreateException()
diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/Microsoft.Data.SqlClient.ManualTests.csproj b/src/Microsoft.Data.SqlClient/tests/ManualTests/Microsoft.Data.SqlClient.ManualTests.csproj
index 87652921e4..6257988fda 100644
--- a/src/Microsoft.Data.SqlClient/tests/ManualTests/Microsoft.Data.SqlClient.ManualTests.csproj
+++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/Microsoft.Data.SqlClient.ManualTests.csproj
@@ -199,6 +199,7 @@
+
@@ -362,7 +363,6 @@
-
@@ -390,7 +390,6 @@
-
diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/JsonTest/JsonBulkCopyTest.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/JsonTest/JsonBulkCopyTest.cs
index 06b76c7ed4..7395101765 100644
--- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/JsonTest/JsonBulkCopyTest.cs
+++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/JsonTest/JsonBulkCopyTest.cs
@@ -6,12 +6,10 @@
using System.Collections.Generic;
using System.Data;
using System.IO;
+using System.Text.Json;
using System.Threading.Tasks;
-using Newtonsoft.Json.Linq;
-using Newtonsoft.Json;
-using Xunit.Abstractions;
using Xunit;
-using System.Collections;
+using Xunit.Abstractions;
namespace Microsoft.Data.SqlClient.ManualTesting.Tests.SQL.JsonTest
{
@@ -67,7 +65,7 @@ private void GenerateJsonFile(int noOfRecords, string filename)
});
}
- string json = JsonConvert.SerializeObject(records, Formatting.Indented);
+ string json = JsonSerializer.Serialize(records, new JsonSerializerOptions { WriteIndented = true });
File.WriteAllText(filename, json);
Assert.True(File.Exists(filename));
_output.WriteLine("Generated JSON file " + filename);
@@ -75,15 +73,9 @@ private void GenerateJsonFile(int noOfRecords, string filename)
private void CompareJsonFiles()
{
- using (var stream1 = File.OpenText(_generatedJsonFile))
- using (var stream2 = File.OpenText(_outputFile))
- using (var reader1 = new JsonTextReader(stream1))
- using (var reader2 = new JsonTextReader(stream2))
- {
- var jToken1 = JToken.ReadFrom(reader1);
- var jToken2 = JToken.ReadFrom(reader2);
- Assert.True(JToken.DeepEquals(jToken1, jToken2));
- }
+ using JsonDocument doc1 = JsonDocument.Parse(File.ReadAllText(_generatedJsonFile));
+ using JsonDocument doc2 = JsonDocument.Parse(File.ReadAllText(_outputFile));
+ Assert.True(JsonTestHelper.JsonDeepEquals(doc1.RootElement, doc2.RootElement));
}
private void PrintJsonDataToFileAndCompare(SqlConnection connection)
diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/JsonTest/JsonStreamTest.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/JsonTest/JsonStreamTest.cs
index 3cb0f1b534..3722853629 100644
--- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/JsonTest/JsonStreamTest.cs
+++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/JsonTest/JsonStreamTest.cs
@@ -8,11 +8,11 @@
using System.Data;
using System.Linq;
using System.Text;
+using System.Text.Json;
using System.Threading.Tasks;
-using Newtonsoft.Json;
using Xunit;
using Xunit.Abstractions;
-using Newtonsoft.Json.Linq;
+using Microsoft.Data.SqlClient.ManualTesting.Tests.SQL.JsonTest;
using Microsoft.Data.SqlClient.Tests.Common.Fixtures.DatabaseObjects;
@@ -51,7 +51,7 @@ private void GenerateJsonFile(int noOfRecords, string filename)
});
}
- string json = JsonConvert.SerializeObject(records, Formatting.Indented);
+ string json = JsonSerializer.Serialize(records, new JsonSerializerOptions { WriteIndented = true });
File.WriteAllText(filename, json);
Assert.True(File.Exists(filename));
_output.WriteLine("Generated JSON file "+filename);
@@ -59,15 +59,9 @@ private void GenerateJsonFile(int noOfRecords, string filename)
private void CompareJsonFiles()
{
- using (var stream1 = File.OpenText(_jsonFile))
- using (var stream2 = File.OpenText(_outputFile))
- using (var reader1 = new JsonTextReader(stream1))
- using (var reader2 = new JsonTextReader(stream2))
- {
- var jToken1 = JToken.ReadFrom(reader1);
- var jToken2 = JToken.ReadFrom(reader2);
- Assert.True(JToken.DeepEquals(jToken1, jToken2));
- }
+ using JsonDocument doc1 = JsonDocument.Parse(File.ReadAllText(_jsonFile));
+ using JsonDocument doc2 = JsonDocument.Parse(File.ReadAllText(_outputFile));
+ Assert.True(JsonTestHelper.JsonDeepEquals(doc1.RootElement, doc2.RootElement));
}
private void PrintJsonDataToFile(SqlConnection connection, string tableName)
diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/JsonTest/JsonTestHelper.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/JsonTest/JsonTestHelper.cs
new file mode 100644
index 0000000000..fcfacc49b3
--- /dev/null
+++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/JsonTest/JsonTestHelper.cs
@@ -0,0 +1,81 @@
+// 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.Linq;
+using System.Text.Json;
+using System.Text.Json.Nodes;
+
+namespace Microsoft.Data.SqlClient.ManualTesting.Tests.SQL.JsonTest
+{
+ internal static class JsonTestHelper
+ {
+ // Test data is Array → Object → Value (3 levels). Use 64 as a safe ceiling.
+ private const int MaxDepth = 64;
+
+ ///
+ /// Performs a deep structural comparison of two values.
+ /// On .NET 9+ this delegates to ; on earlier
+ /// runtimes it uses a recursive comparison over the element trees.
+ ///
+ internal static bool JsonDeepEquals(JsonElement a, JsonElement b)
+ {
+#if NET9_0_OR_GREATER
+ return JsonNode.DeepEquals(
+ JsonNode.Parse(a.GetRawText()),
+ JsonNode.Parse(b.GetRawText()));
+#else
+ return DeepEqualsCore(a, b, depth: 0);
+#endif
+ }
+
+ private static bool DeepEqualsCore(JsonElement a, JsonElement b, int depth)
+ {
+ if (depth > MaxDepth)
+ {
+ throw new InvalidOperationException($"JSON comparison exceeded maximum depth of {MaxDepth}.");
+ }
+
+ if (a.ValueKind != b.ValueKind)
+ {
+ return false;
+ }
+
+ switch (a.ValueKind)
+ {
+ case JsonValueKind.Object:
+ int countA = a.EnumerateObject().Count();
+ int countB = b.EnumerateObject().Count();
+ if (countA != countB)
+ {
+ return false;
+ }
+ foreach (JsonProperty prop in a.EnumerateObject())
+ {
+ if (!b.TryGetProperty(prop.Name, out JsonElement bValue) ||
+ !DeepEqualsCore(prop.Value, bValue, depth + 1))
+ {
+ return false;
+ }
+ }
+ return true;
+
+ case JsonValueKind.Array:
+ JsonElement.ArrayEnumerator arrA = a.EnumerateArray();
+ JsonElement.ArrayEnumerator arrB = b.EnumerateArray();
+ while (arrA.MoveNext())
+ {
+ if (!arrB.MoveNext() || !DeepEqualsCore(arrA.Current, arrB.Current, depth + 1))
+ {
+ return false;
+ }
+ }
+ return !arrB.MoveNext();
+
+ default:
+ return a.GetRawText() == b.GetRawText();
+ }
+ }
+ }
+}
From 312ca720879842bff1a4cd9fa25b3d456734ac19 Mon Sep 17 00:00:00 2001
From: Paul Medynski <31868385+paulmedynski@users.noreply.github.com>
Date: Fri, 29 May 2026 14:04:02 -0300
Subject: [PATCH 2/2] Fix JSON test comparison to handle Unicode escape
differences
System.Text.Json serializes supplementary-plane characters (e.g. U+29E3D)
as \uD867\uDE3D escape sequences, while SQL Server returns them as literal
UTF-8. The DeepEqualsCore fallback compared GetRawText() which preserves
escaping differences. Add explicit JsonValueKind.String case using
GetString() which decodes both representations to the same .NET string.
---
.../tests/ManualTests/SQL/JsonTest/JsonTestHelper.cs | 3 +++
1 file changed, 3 insertions(+)
diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/JsonTest/JsonTestHelper.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/JsonTest/JsonTestHelper.cs
index fcfacc49b3..238e51074e 100644
--- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/JsonTest/JsonTestHelper.cs
+++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/JsonTest/JsonTestHelper.cs
@@ -73,6 +73,9 @@ private static bool DeepEqualsCore(JsonElement a, JsonElement b, int depth)
}
return !arrB.MoveNext();
+ case JsonValueKind.String:
+ return a.GetString() == b.GetString();
+
default:
return a.GetRawText() == b.GetRawText();
}