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 @@ +