Browse Source

Add ToString() to SqlJson (#3427)

feat/vectorSupport
Paul Medynski 1 month ago
committed by GitHub
parent
commit
195de03139
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 21
      .editorconfig
  2. 57
      doc/snippets/Microsoft.Data.SqlTypes/SqlJson.xml
  3. 2
      src/Microsoft.Data.SqlClient/netcore/ref/Microsoft.Data.SqlClient.cs
  4. 2
      src/Microsoft.Data.SqlClient/netfx/ref/Microsoft.Data.SqlClient.cs
  5. 79
      src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlTypes/SqlJson.cs
  6. 151
      src/Microsoft.Data.SqlClient/tests/ManualTests/Json/SqlJsonTest.cs
  7. 1
      src/Microsoft.Data.SqlClient/tests/ManualTests/Microsoft.Data.SqlClient.ManualTesting.Tests.csproj
  8. 157
      src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlTypes/SqlJsonTest.cs
  9. 2
      tools/GenAPI/Microsoft.DotNet.GenAPI/Program.cs

21
.editorconfig

@ -132,7 +132,12 @@ csharp_space_between_method_declaration_parameter_list_parentheses = false
csharp_space_between_parentheses = false
csharp_space_between_square_brackets = false
# Xml project files
# Analyzers
dotnet_code_quality.ca1802.api_surface = private, internal
# CA2000: Dispose objects before losing scope
dotnet_diagnostic.CA2000.severity = suggestion
# CA1063: Implement IDisposable Correctly
dotnet_diagnostic.CA1063.severity = silent
@ -143,6 +148,10 @@ dotnet_diagnostic.CA2100.severity = silent
# CA1416: Validate platform compatibility
dotnet_diagnostic.CA1416.severity = silent
dotnet_code_quality.CA2100.excluded_type_names_with_derived_types = Microsoft.Data.SqlClient.ManualTesting.Tests.*
dotnet_diagnostic.xUnit1031.severity=none
dotnet_diagnostic.xUnit1030.severity=none
[*.{csproj,vcxproj,vcxproj.filters,proj,nativeproj,locproj}]
indent_size = 2
@ -163,13 +172,3 @@ indent_size = 2
end_of_line = lf
[*.{cmd, bat}]
end_of_line = crlf
# Analyzers
dotnet_code_quality.ca1802.api_surface = private, internal
[*.cs]
dotnet_code_quality.CA2100.excluded_type_names_with_derived_types = Microsoft.Data.SqlClient.ManualTesting.Tests.*
dotnet_diagnostic.xUnit1031.severity=none
dotnet_diagnostic.xUnit1030.severity=none

57
doc/snippets/Microsoft.Data.SqlTypes/SqlJson.xml

@ -1,28 +1,69 @@
<?xml version="1.0"?>
<?xml version="1.0" encoding="UTF-8" ?>
<docs>
<members name="SqlJson">
<SqlJson>
<summary>Represents the JSON datatype in SQL Server.</summary>
</SqlJson>
<ctor1>
<summary>Parameterless constructor. Initializes a new instance of the SqlJson class which represents a null JSON value.</summary>
<summary>
Construct a new instance of the SqlJson class which represents a null
JSON value.
</summary>
</ctor1>
<ctor2>
<param name="jsonString"></param>
<summary>Takes a <see cref="string"/> as input and initializes a new instance of the SqlJson class.</summary>
<summary>
Construct a new instance of the SqlJson class with a serialized JSON
<see cref="string"/>. The string is validated by parsing it with
<see cref="System.Text.Json.JsonDocument"/>.
</summary>
<param name="jsonString">
The serialized JSON string to use, or null.
</param>
<throw>
<exception cref="System.Text.Json.JsonException">
If the given string is not valid JSON.
</exception>
</throw>
</ctor2>
<ctor3>
<param name="jsonDoc"></param>
<summary>Takes a <see cref="System.Text.Json.JsonDocument"/> as input and initializes a new instance of the SqlJson class.</summary>
<summary>
Construct a new instance of the SqlJson class with a
<see cref="System.Text.Json.JsonDocument"/>. The serialized JSON string
from the document is saved.
</summary>
<param name="jsonDoc">
The document to use, or null.
</param>
<throw>
<exception cref="System.ObjectDisposedException">
If the given document has been disposed of.
</exception>
</throw>
</ctor3>
<IsNull>
<inheritdoc/>
</IsNull>
<Null>
<summary>Represents a null instance of the <see cref="SqlJson"/> type.</summary>
<summary>
Represents a null instance of the <see cref="SqlJson"/> type. This
instance is equivalent to calling the parameterless constructor, or
calling the other constructors with a null value.
</summary>
</Null>
<Value>
<summary>Gets the string representation of the Json content of this <see cref="SqlJson" /> instance.</summary>
<summary>
Gets the serialized JSON string of this <see cref="SqlJson" /> instance.
</summary>
<throw>
<exception cref="System.Data.SqlTypes.SqlNullValueException">
If the JSON value is null.
</exception>
</throw>
</Value>
<ToString>
<summary>
Returns the serialized JSON string, or null.
</summary>
</ToString>
</members>
</docs>

2
src/Microsoft.Data.SqlClient/netcore/ref/Microsoft.Data.SqlClient.cs

@ -116,6 +116,8 @@ namespace Microsoft.Data.SqlTypes
public static SqlJson Null => throw null;
/// <include file='../../../../doc/snippets/Microsoft.Data.SqlTypes/SqlJson.xml' path='docs/members[@name="SqlJson"]/Value/*' />
public string Value { get { throw null; } }
/// <include file='../../../../doc/snippets/Microsoft.Data.SqlTypes/SqlJson.xml' path='docs/members[@name="SqlJson"]/ToString/*' />
public override string ToString() { throw null; }
}
}
namespace Microsoft.Data.SqlClient

2
src/Microsoft.Data.SqlClient/netfx/ref/Microsoft.Data.SqlClient.cs

@ -2408,5 +2408,7 @@ namespace Microsoft.Data.SqlTypes
public static SqlJson Null => throw null;
/// <include file='../../../../doc/snippets/Microsoft.Data.SqlTypes/SqlJson.xml' path='docs/members[@name="SqlJson"]/Value/*' />
public string Value { get { throw null; } }
/// <include file='../../../../doc/snippets/Microsoft.Data.SqlTypes/SqlJson.xml' path='docs/members[@name="SqlJson"]/ToString/*' />
public override string ToString() { throw null; }
}
}

79
src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlTypes/SqlJson.cs

@ -2,45 +2,47 @@
// 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;
#if NET
using System.Diagnostics.CodeAnalysis;
#endif
using System.Text.Json;
#nullable enable
namespace Microsoft.Data.SqlTypes
{
/// <include file='../../../../doc/snippets/Microsoft.Data.SqlTypes/SqlJson.xml' path='docs/members[@name="SqlJson"]/SqlJson/*' />
/// <include file='../../../../doc/snippets/Microsoft.Data.SqlTypes/SqlJson.xml' path='docs/members[@name="SqlJson"]/SqlJson/*' />
public class SqlJson : INullable
{
/// <summary>
/// True if null.
/// </summary>
private bool _isNull;
private readonly string? _jsonString;
// Our serialized JSON string, or null.
private readonly string? _jsonString = null;
/// <include file='../../../../doc/snippets/Microsoft.Data.SqlTypes/SqlJson.xml' path='docs/members[@name="SqlJson"]/ctor1/*' />
public SqlJson()
{
SetNull();
}
/// <include file='../../../../doc/snippets/Microsoft.Data.SqlTypes/SqlJson.xml' path='docs/members[@name="SqlJson"]/ctor2/*' />
public SqlJson(string? jsonString)
#if NET
public SqlJson([StringSyntax(StringSyntaxAttribute.Json)] string? jsonString)
#else
public SqlJson(string? jsonString)
#endif
{
if (jsonString == null)
{
SetNull();
}
else
{
// TODO: We need to validate the Json before storing it.
ValidateJson(jsonString);
_jsonString = jsonString;
return;
}
// Ask JsonDocument to parse it for validity, or throw.
//
// Note that we do not support trailing commas or comments in the
// JSON.
//
JsonDocument.Parse(jsonString).Dispose();
_jsonString = jsonString;
}
/// <include file='../../../../doc/snippets/Microsoft.Data.SqlTypes/SqlJson.xml' path='docs/members[@name="SqlJson"]/ctor3/*' />
@ -48,16 +50,15 @@ namespace Microsoft.Data.SqlTypes
{
if (jsonDoc == null)
{
SetNull();
}
else
{
_jsonString = jsonDoc.RootElement.GetRawText();
return;
}
// Save the serialized JSON string from the document, or throw.
_jsonString = jsonDoc.RootElement.GetRawText();
}
/// <include file='../../../../doc/snippets/Microsoft.Data.SqlTypes/SqlJson.xml' path='docs/members[@name="SqlJson"]/IsNull/*' />
public bool IsNull => _isNull;
public bool IsNull => _jsonString is null;
/// <include file='../../../../doc/snippets/Microsoft.Data.SqlTypes/SqlJson.xml' path='docs/members[@name="SqlJson"]/Null/*' />
public static SqlJson Null => new();
@ -71,33 +72,15 @@ namespace Microsoft.Data.SqlTypes
{
throw new SqlNullValueException();
}
else
{
return _jsonString!;
}
}
}
private void SetNull()
{
_isNull = true;
return _jsonString!;
}
}
private static void ValidateJson(string jsonString)
/// <include file='../../../../doc/snippets/Microsoft.Data.SqlTypes/SqlJson.xml' path='docs/members[@name="SqlJson"]/ToString/*' />
public override string? ToString()
{
// 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
return _jsonString;
}
}
}

151
src/Microsoft.Data.SqlClient/tests/ManualTests/Json/SqlJsonTest.cs

@ -1,151 +0,0 @@
// 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<SqlNullValueException>(() => json.Value);
}
[Fact]
public void SqlJsonTest_NullString()
{
string nullString = null;
SqlJson json = new(nullString);
Assert.True(json.IsNull);
Assert.Throws<SqlNullValueException>(() => json.Value);
}
[Fact]
public void SqlJsonTest_NullJsonDocument()
{
JsonDocument doc = null;
SqlJson json = new(doc);
Assert.True(json.IsNull);
Assert.Throws<SqlNullValueException>(() => 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<JsonException>(()=> 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<SqlNullValueException>(() => 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}");
}
}
}
}

1
src/Microsoft.Data.SqlClient/tests/ManualTests/Microsoft.Data.SqlClient.ManualTesting.Tests.csproj

@ -281,7 +281,6 @@
<Compile Include="DataCommon\SqlClientCustomTokenCredential.cs" />
<Compile Include="DataCommon\SystemDataResourceManager.cs" />
<Compile Include="Extensions\StreamExtensions.netfx.cs" />
<Compile Include="Json\SqlJsonTest.cs" />
<Compile Include="SQL\Common\AsyncDebugScope.cs" />
<Compile Include="SQL\Common\ConnectionPoolWrapper.cs" />
<Compile Include="SQL\Common\InternalConnectionWrapper.cs" />

157
src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlTypes/SqlJsonTest.cs

@ -0,0 +1,157 @@
// 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.
#nullable enable
using System;
using System.Data.SqlTypes;
using System.Text.Json;
using Microsoft.Data.SqlTypes;
using Xunit;
namespace Microsoft.Data.SqlClient.UnitTests;
public class SqlJsonTest
{
#region Private Fields
private static readonly JsonSerializerOptions _jsonOptions = new()
{
WriteIndented = true
};
#endregion
#region Tests
// Test the static Null property.
[Fact]
public void StaticNull()
{
Assert.True(SqlJson.Null.IsNull);
Assert.Throws<SqlNullValueException>(() => SqlJson.Null.Value);
Assert.Null(SqlJson.Null.ToString());
}
// Test the constructor that takes no arguments.
[Fact]
public void Constructor_NoArgs()
{
SqlJson json = new();
Assert.True(json.IsNull);
Assert.Throws<SqlNullValueException>(() => json.Value);
Assert.Null(json.ToString());
}
// Test the constructors that take a nullable string.
[Fact]
public void Constructor_String_Null()
{
const string? value = null;
SqlJson json = new(value);
Assert.True(json.IsNull);
Assert.Throws<SqlNullValueException>(() => json.Value);
Assert.Null(json.ToString());
}
[Fact]
public void Constructor_String_NotNull()
{
const string value = "{\"key\":\"value\"}";
SqlJson json = new(value);
Assert.False(json.IsNull);
Assert.Equal(value, json.Value);
Assert.Equal(value, json.ToString());
}
[Theory]
// Non-string key.
[InlineData("{key:\"value\"}")]
// Invalid value type.
[InlineData("{\"key\":value}")]
// Missing closing brace.
[InlineData("{\"key\":\"value\"")]
// Trailing comma.
[InlineData("{\"key\":\"value\",}")]
// Comment in JSON.
[InlineData("// comment {\"key\":\"value\"}")]
public void Constructor_String_Invalid(string invalid)
{
Assert.ThrowsAny<JsonException>(() => new SqlJson(invalid));
}
// Test the constructor that takes a nullable JsonDocument.
[Fact]
public void Constructor_JsonDocument_Null()
{
const JsonDocument? doc = null;
SqlJson json = new(doc);
Assert.True(json.IsNull);
Assert.Throws<SqlNullValueException>(() => json.Value);
Assert.Null(json.ToString());
}
[Fact]
public void Constructor_JsonDocument_NotNull()
{
using JsonDocument doc = GenerateRandomJson();
SqlJson json = new(doc);
Assert.False(json.IsNull);
Assert.Equal(doc.RootElement.GetRawText(), json.Value);
Assert.Equal(doc.RootElement.GetRawText(), json.ToString());
}
[Fact]
public void Constructor_JsonDocument_Disposed()
{
using JsonDocument doc = GenerateRandomJson();
doc.Dispose();
Assert.Throws<ObjectDisposedException>(() => new SqlJson(doc));
}
// IsNull, Value, and ToString() are covered by the above tests.
// Test that the Value can be round-tripped through a JsonDocument.
//
// JsonElement.DeepEquals() is only available in .NET 9.0 and later.
#if NET9_0_OR_GREATER
[Fact]
public void RoundTrip()
{
using JsonDocument doc = GenerateRandomJson();
SqlJson json = new(doc);
using var outputDocument = JsonDocument.Parse(json.Value);
Assert.True(JsonElement.DeepEquals(
doc.RootElement, outputDocument.RootElement));
}
#endif
#endregion
#region Helpers
private static JsonDocument GenerateRandomJson()
{
Random random = new();
object 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)}"
}
};
return JsonSerializer.SerializeToDocument(jsonObject, _jsonOptions);
}
#endregion
}

2
tools/GenAPI/Microsoft.DotNet.GenAPI/Program.cs

@ -33,7 +33,7 @@ namespace Microsoft.DotNet.GenAPI
private static int Main(string[] args)
{
var app = new CommandLineApplication
using var app = new CommandLineApplication
{
Name = "GenAPI",
FullName = "A command line tool to generate code for the API surface of an assembly.",

Loading…
Cancel
Save