Skip to content

Commit 50fc45d

Browse files
authored
Bring JSON type out of experimental status (#36442)
* Remove experimental warning * Remove conversion to nvarchar(max) within OPENJSON * Add conversions to nvarchar(max) when comparing json (not natively supported) * Use json type in complex JSON query tests when targeting newer versions of SQL Server * Various test tweaks and cleanup Closes #36024
1 parent 65bb90c commit 50fc45d

19 files changed

+741
-624
lines changed

src/EFCore.SqlServer/Diagnostics/Internal/SqlServerLoggingDefinitions.cs

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -194,12 +194,4 @@ public class SqlServerLoggingDefinitions : RelationalLoggingDefinitions
194194
/// doing so can result in application failures when updating to a new Entity Framework Core release.
195195
/// </summary>
196196
public EventDefinitionBase? LogMissingViewDefinitionRights;
197-
198-
/// <summary>
199-
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
200-
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
201-
/// any release. You should only use it directly in your code with extreme caution and knowing that
202-
/// doing so can result in application failures when updating to a new Entity Framework Core release.
203-
/// </summary>
204-
public EventDefinitionBase? LogJsonTypeExperimental;
205197
}

src/EFCore.SqlServer/Diagnostics/SqlServerEventId.cs

Lines changed: 1 addition & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ private enum Id
3232
ConflictingValueGenerationStrategiesWarning,
3333
DecimalTypeKeyWarning,
3434
SavepointsDisabledBecauseOfMARS,
35-
JsonTypeExperimental,
35+
JsonTypeExperimental, // No longer used
3636

3737
// Scaffolding events
3838
ColumnFound = CoreEventId.ProviderDesignBaseId,
@@ -115,20 +115,6 @@ private static EventId MakeValidationId(Id id)
115115
/// </remarks>
116116
public static readonly EventId ByteIdentityColumnWarning = MakeValidationId(Id.ByteIdentityColumnWarning);
117117

118-
/// <summary>
119-
/// An entity type makes use of the SQL Server native 'json' type. Please note that support for this type in EF Core 9 is
120-
/// experimental and may change in future releases.
121-
/// </summary>
122-
/// <remarks>
123-
/// <para>
124-
/// This event is in the <see cref="DbLoggerCategory.Model.Validation" /> category.
125-
/// </para>
126-
/// <para>
127-
/// This event uses the <see cref="EntityTypeEventData" /> payload when used with a <see cref="DiagnosticSource" />.
128-
/// </para>
129-
/// </remarks>
130-
public static readonly EventId JsonTypeExperimental = MakeValidationId(Id.JsonTypeExperimental);
131-
132118
/// <summary>
133119
/// There are conflicting value generation methods for a property.
134120
/// </summary>

src/EFCore.SqlServer/Extensions/Internal/SqlServerLoggerExtensions.cs

Lines changed: 0 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -124,33 +124,6 @@ private static string ByteIdentityColumnWarning(EventDefinitionBase definition,
124124
p.Property.DeclaringType.DisplayName());
125125
}
126126

127-
/// <summary>
128-
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
129-
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
130-
/// any release. You should only use it directly in your code with extreme caution and knowing that
131-
/// doing so can result in application failures when updating to a new Entity Framework Core release.
132-
/// </summary>
133-
public static void JsonTypeExperimental(
134-
this IDiagnosticsLogger<DbLoggerCategory.Model.Validation> diagnostics,
135-
IEntityType entityType)
136-
{
137-
var definition = SqlServerResources.LogJsonTypeExperimental(diagnostics);
138-
139-
if (diagnostics.ShouldLog(definition))
140-
{
141-
definition.Log(diagnostics, entityType.DisplayName());
142-
}
143-
144-
if (diagnostics.NeedsEventData(definition, out var diagnosticSourceEnabled, out var simpleLogEnabled))
145-
{
146-
var eventData = new EntityTypeEventData(
147-
definition, (d, p)
148-
=> ((EventDefinition<string>)d).GenerateMessage(((EntityTypeEventData)p).EntityType.DisplayName()), entityType);
149-
150-
diagnostics.DispatchEventData(definition, eventData, diagnosticSourceEnabled, simpleLogEnabled);
151-
}
152-
}
153-
154127
/// <summary>
155128
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
156129
/// the same compatibility standards as public APIs. It may be changed or removed without notice in

src/EFCore.SqlServer/Infrastructure/Internal/SqlServerModelValidator.cs

Lines changed: 0 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -48,27 +48,6 @@ public override void Validate(IModel model, IDiagnosticsLogger<DbLoggerCategory.
4848
ValidateVectorColumns(model, logger);
4949
ValidateByteIdentityMapping(model, logger);
5050
ValidateTemporalTables(model, logger);
51-
ValidateUseOfJsonType(model, logger);
52-
}
53-
54-
/// <summary>
55-
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
56-
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
57-
/// any release. You should only use it directly in your code with extreme caution and knowing that
58-
/// doing so can result in application failures when updating to a new Entity Framework Core release.
59-
/// </summary>
60-
protected virtual void ValidateUseOfJsonType(
61-
IModel model,
62-
IDiagnosticsLogger<DbLoggerCategory.Model.Validation> logger)
63-
{
64-
foreach (var entityType in model.GetEntityTypes())
65-
{
66-
if (string.Equals(entityType.GetContainerColumnType(), "json", StringComparison.OrdinalIgnoreCase)
67-
|| entityType.GetProperties().Any(p => string.Equals(p.GetColumnType(), "json", StringComparison.OrdinalIgnoreCase)))
68-
{
69-
logger.JsonTypeExperimental(entityType);
70-
}
71-
}
7251
}
7352

7453
/// <summary>

src/EFCore.SqlServer/Properties/SqlServerStrings.Designer.cs

Lines changed: 0 additions & 25 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/EFCore.SqlServer/Properties/SqlServerStrings.resx

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -264,10 +264,6 @@
264264
<value>Found unique constraint on table '{tableName}' with name '{uniqueConstraintName}'.</value>
265265
<comment>Debug SqlServerEventId.UniqueConstraintFound string string</comment>
266266
</data>
267-
<data name="LogJsonTypeExperimental" xml:space="preserve">
268-
<value>The entity type '{entityType}' makes use of the SQL Server native 'json' type. Please note that support for this type in EF Core 9 is experimental and may change in future releases.</value>
269-
<comment>Warning SqlServerEventId.JsonTypeExperimental string</comment>
270-
</data>
271267
<data name="LogMissingSchema" xml:space="preserve">
272268
<value>Unable to find a schema in the database matching the selected schema '{schema}'.</value>
273269
<comment>Warning SqlServerEventId.MissingSchemaWarning string?</comment>

src/EFCore.SqlServer/Query/Internal/SqlServerJsonPostprocessor.cs

Lines changed: 22 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -254,21 +254,28 @@ when _columnsToRewrite.TryGetValue((columnExpression.TableAlias, columnExpressio
254254
jsonScalar.IsNullable);
255255
}
256256

257-
case SqlServerOpenJsonExpression openJsonExpression:
258-
// Currently, OPENJSON does not accept a "json" type, so we must cast the value to a string.
259-
// We do this for both the case where is a string type mapping for a top-level property with the store type
260-
// of "json", and when there is an "element" type mapping to something in the document but is now being used
261-
// with OPENJSON.
262-
return openJsonExpression.JsonExpression.TypeMapping
263-
is SqlServerStringTypeMapping { StoreType: "json" }
264-
or SqlServerStructuralJsonTypeMapping { StoreType: "json" }
265-
? openJsonExpression.Update(
266-
new SqlUnaryExpression(
267-
ExpressionType.Convert,
268-
(SqlExpression)Visit(openJsonExpression.JsonExpression),
269-
typeof(string),
270-
typeMappingSource.FindMapping(typeof(string))!))
271-
: base.Visit(expression);
257+
// The SQL Server json type cannot be compared ("The JSON data type cannot be compared or sorted, except when using the
258+
// IS NULL operator"). So we find comparisons that involve the json type, and apply a conversion to string (nvarchar(max))
259+
// to both sides. We exempt this when one of the sides is a constant null (not required).
260+
case SqlBinaryExpression
261+
{
262+
OperatorType: ExpressionType.Equal or ExpressionType.NotEqual,
263+
Left: var left,
264+
Right: var right
265+
} comparison
266+
when (left.TypeMapping?.StoreType is "json" || right.TypeMapping?.StoreType is "json")
267+
&& left is not SqlConstantExpression { Value: null } && right is not SqlConstantExpression { Value: null }:
268+
{
269+
return comparison.Update(
270+
sqlExpressionFactory.Convert(
271+
left,
272+
typeof(string),
273+
typeMappingSource.FindMapping(typeof(string))),
274+
sqlExpressionFactory.Convert(
275+
right,
276+
typeof(string),
277+
typeMappingSource.FindMapping(typeof(string))));
278+
}
272279

273280
default:
274281
return base.Visit(expression);

test/EFCore.SqlServer.FunctionalTests/Query/AdHocJsonQuerySqlServerJsonTypeTest.cs

Lines changed: 43 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -2,37 +2,56 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
#nullable disable
5+
56
using Microsoft.Data.SqlClient;
7+
using Xunit.Sdk;
68

79
namespace Microsoft.EntityFrameworkCore.Query;
810

911
[SqlServerCondition(SqlServerCondition.SupportsJsonType)]
1012
public class AdHocJsonQuerySqlServerJsonTypeTest(NonSharedFixture fixture) : AdHocJsonQuerySqlServerTestBase(fixture)
1113
{
12-
public override async Task Missing_navigation_works_with_deduplication(bool async)
13-
{
14-
// TODO:SQLJSON Returns empty (invalid) JSON (See BadJson.cs)
15-
if (async)
16-
{
17-
Assert.Equal(
18-
"Unable to cast object of type 'System.DBNull' to type 'System.String'.",
19-
(await Assert.ThrowsAsync<InvalidCastException>(() => base.Missing_navigation_works_with_deduplication(true))).Message);
20-
}
21-
else
22-
{
23-
Assert.Equal(
24-
RelationalStrings.JsonEmptyString,
25-
(await Assert.ThrowsAsync<InvalidOperationException>(() => base.Missing_navigation_works_with_deduplication(false)))
26-
.Message);
27-
}
28-
}
29-
30-
public override async Task Contains_on_nested_collection_with_init_only_navigation()
31-
// TODO:SQLJSON (See JsonTypeToFunction.cs)
32-
=> Assert.Equal(
33-
"OpenJson support not yet supported for JSON native data type.",
34-
(await Assert.ThrowsAsync<SqlException>(
35-
() => base.Contains_on_nested_collection_with_init_only_navigation())).Message);
14+
#region BadJsonProperties
15+
16+
// When using the SQL Server JSON data type, insertion of the bad data fails thanks to SQL Server validation,
17+
// unlike with tests mapping to nvarchar(max) where the bad JSON data is inserted correctly and then read.
18+
19+
public override Task Bad_json_properties_duplicated_navigations(bool noTracking)
20+
=> Task.CompletedTask;
21+
22+
public override Task Bad_json_properties_duplicated_scalars(bool noTracking)
23+
=> Task.CompletedTask;
24+
25+
public override Task Bad_json_properties_empty_navigations(bool noTracking)
26+
=> Task.CompletedTask;
27+
28+
public override Task Bad_json_properties_empty_scalars(bool noTracking)
29+
=> Task.CompletedTask;
30+
31+
public override Task Bad_json_properties_null_navigations(bool noTracking)
32+
=> Task.CompletedTask;
33+
34+
public override Task Bad_json_properties_null_scalars(bool noTracking)
35+
=> Task.CompletedTask;
36+
37+
#endregion BadJsonProperties
38+
39+
// SQL Server 2025 (CTP 2.1) does not support casting JSON scalar strings to json
40+
// (CAST('8' AS json) and CAST('null' AS json) fail with "JSON text is not properly formatted").
41+
public override Task Project_entity_with_json_null_values()
42+
=> Assert.ThrowsAsync<SqlException>(base.Project_entity_with_json_null_values);
43+
44+
// SQL Server 2025 (CTP 2.1) does not support casting JSON scalar strings to json
45+
// (CAST('8' AS json) and CAST('null' AS json) fail with "JSON text is not properly formatted").
46+
// The base implementation expects a different exception.
47+
public override Task Try_project_collection_but_JSON_is_entity()
48+
=> Assert.ThrowsAsync<ThrowsException>(base.Try_project_collection_but_JSON_is_entity);
49+
50+
// SQL Server 2025 (CTP 2.1) does not support casting JSON scalar strings to json
51+
// (CAST('8' AS json) and CAST('null' AS json) fail with "JSON text is not properly formatted").
52+
// The base implementation expects a different exception.
53+
public override Task Try_project_reference_but_JSON_is_collection()
54+
=> Assert.ThrowsAsync<ThrowsException>(base.Try_project_reference_but_JSON_is_collection);
3655

3756
protected override string StoreName
3857
=> "AdHocJsonQueryJsonTypeTest";

test/EFCore.SqlServer.FunctionalTests/Query/AdHocJsonQuerySqlServerTest.cs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,4 @@
33

44
namespace Microsoft.EntityFrameworkCore.Query;
55

6-
public class AdHocJsonQuerySqlServerTest(NonSharedFixture fixture) : AdHocJsonQuerySqlServerTestBase(fixture)
7-
{
8-
}
6+
public class AdHocJsonQuerySqlServerTest(NonSharedFixture fixture) : AdHocJsonQuerySqlServerTestBase(fixture);

0 commit comments

Comments
 (0)