Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -615,6 +615,9 @@
<Compile Include="$(CommonSourceRoot)Microsoft\Data\SqlTypes\SqlTypeWorkarounds.cs">
<Link>Microsoft\Data\SqlTypes\SqlTypeWorkarounds.cs</Link>
</Compile>
<Compile Include="$(CommonSourceRoot)Microsoft\Data\SqlTypes\SqlJson.cs">
<Link>Microsoft\Data\SqlTypes\SqlJson.cs</Link>
</Compile>
<Compile Include="$(CommonSourceRoot)Resources\ResCategoryAttribute.cs">
<Link>Resources\ResCategoryAttribute.cs</Link>
</Compile>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -636,6 +636,9 @@
<Compile Include="$(CommonSourceRoot)Microsoft\Data\SqlTypes\SqlTypeWorkarounds.cs">
<Link>Microsoft\Data\SqlTypes\SqlTypeWorkarounds.cs</Link>
</Compile>
<Compile Include="$(CommonSourceRoot)Microsoft\Data\SqlTypes\SqlJson.cs">
<Link>Microsoft\Data\SqlTypes\SqlJson.cs</Link>
</Compile>
<Compile Include="$(CommonSourceRoot)Resources\ResCategoryAttribute.cs">
<Link>Resources\ResCategoryAttribute.cs</Link>
</Compile>
Expand Down
118 changes: 118 additions & 0 deletions src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlTypes/SqlJson.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// Represents the Json Data type in SQL Server.
/// </summary>
public class SqlJson : INullable
{

/// <summary>
/// True if null.
/// </summary>
private bool _isNull;

private readonly string? _jsonString;

/// <summary>
/// Parameterless constructor. Initializes a new instance of the SqlJson class which
/// represents a null JSON value.
/// </summary>
public SqlJson()
{
SetNull();
}

/// <summary>
/// Takes a <see cref="string"/> as input and initializes a new instance of the SqlJson class.
/// </summary>
/// <param name="jsonString"></param>
public SqlJson(string? jsonString)
{
if (jsonString == null)
{
SetNull();
}
else
{
// TODO: We need to validate the Json before storing it.
ValidateJson(jsonString);
_jsonString = jsonString;
}
}

/// <summary>
/// Takes a <see cref="JsonDocument"/> as input and initializes a new instance of the SqlJson class.
/// </summary>
/// <param name="jsonDoc"></param>
public SqlJson(JsonDocument? jsonDoc)
{
if (jsonDoc == null)
{
SetNull();
}
else
{
_jsonString = jsonDoc.RootElement.GetRawText();
}
}

/// <inheritdoc/>
public bool IsNull => _isNull;

/// <summary>
/// Represents a null instance of the <see cref="SqlJson"/> type.
/// </summary>
public static SqlJson Null => new();

/// <summary>
/// Gets the string representation of the Json content of this <see cref="SqlJson" /> instance.
/// </summary>
public string Value
{
get
{
if (IsNull)
{
throw new SqlNullValueException();
}
else
{
return _jsonString!;
}
}
}
Comment on lines +82 to +94
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: if you want to start using more modern C# code, this can be a simple expression-bodied method with a conditional operator:

Suggested change
{
get
{
if (IsNull)
{
throw new SqlNullValueException();
}
else
{
return _jsonString!;
}
}
}
=> IsNull ? throw new SqlNullValueException() : _jsonString!;


private void SetNull()
{
_isNull = true;
}

private static void ValidateJson(string jsonString)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this additional parsing of the entire document necessary (with the associated perf overhead)? I'm assuming that SQL Server would throw anyway on any bad input etc...

{
// 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
}
}
}
151 changes: 151 additions & 0 deletions src/Microsoft.Data.SqlClient/tests/ManualTests/Json/SqlJsonTest.cs
Original file line number Diff line number Diff line change
@@ -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<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}");
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,7 @@
<Compile Include="DataCommon\ProxyServer.cs" />
<Compile Include="DataCommon\SqlClientCustomTokenCredential.cs" />
<Compile Include="DataCommon\SystemDataResourceManager.cs" />
<Compile Include="Json\SqlJsonTest.cs" />
<Compile Include="SQL\Common\AsyncDebugScope.cs" />
<Compile Include="SQL\Common\ConnectionPoolWrapper.cs" />
<Compile Include="SQL\Common\InternalConnectionWrapper.cs" />
Expand Down