Skip to content
Merged
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
16 changes: 11 additions & 5 deletions src/EFCore.Relational/Storage/JsonTypeMapping.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,24 +18,24 @@ namespace Microsoft.EntityFrameworkCore.Storage;
/// See <see href="https://aka.ms/efcore-docs-providers">Implementation of database providers and extensions</see>
/// for more information and examples.
/// </remarks>
public abstract class JsonTypeMapping : RelationalTypeMapping
public abstract class StructuralJsonTypeMapping : RelationalTypeMapping
{
/// <summary>
/// Initializes a new instance of the <see cref="JsonTypeMapping" /> class.
/// Initializes a new instance of the <see cref="StructuralJsonTypeMapping" /> class.
/// </summary>
/// <param name="storeType">The name of the database type.</param>
/// <param name="clrType">The .NET type.</param>
/// <param name="dbType">The <see cref="DbType" /> to be used.</param>
protected JsonTypeMapping(string storeType, Type clrType, DbType? dbType)
protected StructuralJsonTypeMapping(string storeType, Type clrType, DbType? dbType)
: base(storeType, clrType, dbType)
{
}

/// <summary>
/// Initializes a new instance of the <see cref="JsonTypeMapping" /> class.
/// Initializes a new instance of the <see cref="StructuralJsonTypeMapping" /> class.
/// </summary>
/// <param name="parameters">Parameter object for <see cref="RelationalTypeMapping" />.</param>
protected JsonTypeMapping(RelationalTypeMappingParameters parameters)
protected StructuralJsonTypeMapping(RelationalTypeMappingParameters parameters)
: base(parameters)
{
}
Expand All @@ -45,3 +45,9 @@ protected override string GenerateNonNullSqlLiteral(object value)
=> throw new InvalidOperationException(
RelationalStrings.MethodNeedsToBeImplementedInTheProvider);
}

/// <summary>
/// Use StructuralJsonTypeMapping instead for type mappings representing JSON structural types as opposed to JSON strings.
/// </summary>
[Obsolete("Use StructuralJsonTypeMapping instead for type mappings representing JSON structural types as opposed to JSON strings.")]
public class JsonTypeMapping;
39 changes: 24 additions & 15 deletions src/EFCore.SqlServer/Query/Internal/SqlServerJsonPostprocessor.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Diagnostics.CodeAnalysis;
using Microsoft.EntityFrameworkCore.Query.SqlExpressions;
using Microsoft.EntityFrameworkCore.SqlServer.Internal;
using Microsoft.EntityFrameworkCore.SqlServer.Storage.Internal;
Expand Down Expand Up @@ -55,8 +54,7 @@ public Expression Process(Expression expression)
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
[return: NotNullIfNotNull(nameof(expression))]
public override Expression? Visit(Expression? expression)
protected override Expression VisitExtension(Expression expression)
{
switch (expression)
{
Expand Down Expand Up @@ -128,7 +126,7 @@ public Expression Process(Expression expression)
{
Check.DebugAssert(newTables is null, "newTables must be null if columnsToRewrite is null");

result = (SelectExpression)base.Visit(result);
result = (SelectExpression)base.VisitExtension(result);
}
else
{
Expand All @@ -154,7 +152,7 @@ public Expression Process(Expression expression)

// Record the OPENJSON expression and its projected column(s), along with the store type we just removed from the WITH
// clause. Then visit the select expression, adding a cast around the matching ColumnExpressions.
result = (SelectExpression)base.Visit(result);
result = (SelectExpression)base.VisitExtension(result);

foreach (var columnsToRewriteKey in columnsToRewrite.Keys)
{
Expand Down Expand Up @@ -270,19 +268,30 @@ when _columnsToRewrite.TryGetValue((columnExpression.TableAlias, columnExpressio
&& left is not SqlConstantExpression { Value: null }
&& right is not SqlConstantExpression { Value: null }:
{
return comparison.Update(
sqlExpressionFactory.Convert(
left,
typeof(string),
typeMappingSource.FindMapping(typeof(string))),
sqlExpressionFactory.Convert(
right,
typeof(string),
typeMappingSource.FindMapping(typeof(string))));
var stringTypeMapping = typeMappingSource.FindMapping(typeof(string))!;

return comparison.Update(ConvertToString(left), ConvertToString(right));

SqlExpression ConvertToString(SqlExpression expression)
{
if (expression.TypeMapping?.StoreType.Equals("json", StringComparison.OrdinalIgnoreCase) == true)
{
// If the expression happens to be a json literal (CAST('...' AS json)), we can just extract the string inside,
// instead of applying an additional CAST around it
return expression is SqlConstantExpression constant
? new SqlConstantExpression(
constant.Value,
typeof(string),
(RelationalTypeMapping)stringTypeMapping.WithComposedConverter(constant.TypeMapping!.Converter))
: sqlExpressionFactory.Convert(expression, typeof(string), stringTypeMapping);
}

return expression;
}
}

default:
return base.Visit(expression);
return base.VisitExtension(expression);
}

static bool IsKeyColumn(SqlExpression sqlExpression, string openJsonTableAlias)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,28 +9,19 @@ namespace Microsoft.EntityFrameworkCore.SqlServer.Query.Internal;
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public class SqlServerQueryTranslationPostprocessor : RelationalQueryTranslationPostprocessor
public class SqlServerQueryTranslationPostprocessor(
QueryTranslationPostprocessorDependencies dependencies,
RelationalQueryTranslationPostprocessorDependencies relationalDependencies,
SqlServerQueryCompilationContext queryCompilationContext)
: RelationalQueryTranslationPostprocessor(dependencies, relationalDependencies, queryCompilationContext)
{
private readonly SqlServerJsonPostprocessor _jsonPostprocessor;
private readonly SqlServerAggregateOverSubqueryPostprocessor _aggregatePostprocessor;
private readonly SqlServerSqlTreePruner _pruner = new();
private readonly SqlServerJsonPostprocessor _jsonPostprocessor = new(
relationalDependencies.TypeMappingSource,
relationalDependencies.SqlExpressionFactory,
queryCompilationContext.SqlAliasManager);

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public SqlServerQueryTranslationPostprocessor(
QueryTranslationPostprocessorDependencies dependencies,
RelationalQueryTranslationPostprocessorDependencies relationalDependencies,
SqlServerQueryCompilationContext queryCompilationContext)
: base(dependencies, relationalDependencies, queryCompilationContext)
{
_jsonPostprocessor = new SqlServerJsonPostprocessor(
relationalDependencies.TypeMappingSource, relationalDependencies.SqlExpressionFactory, queryCompilationContext.SqlAliasManager);
_aggregatePostprocessor = new SqlServerAggregateOverSubqueryPostprocessor(queryCompilationContext.SqlAliasManager);
}
private readonly SqlServerAggregateOverSubqueryPostprocessor _aggregatePostprocessor = new(queryCompilationContext.SqlAliasManager);
private readonly SqlServerSqlTreePruner _pruner = new();

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -608,6 +608,71 @@ IComplexType complexType
false));
}

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
protected override ShapedQueryExpression? TranslateContains(ShapedQueryExpression source, Expression item)
{
// Attempt to translate to JSON_CONTAINS for SQL Server 2025+ (compatibility level 170+).
// JSON_CONTAINS is more efficient than IN (SELECT ... FROM OPENJSON(...)) for primitive collections.
if (_sqlServerSingletonOptions.SupportsJsonType
&& source.QueryExpression is SelectExpression
{
// Primitive collection over OPENJSON (e.g. [p].[Ints])
Tables:
[
SqlServerOpenJsonExpression
{
// JSON_CONTAINS() is only supported over json, not nvarchar
Json: { TypeMapping: SqlServerJsonTypeMapping } json,
Path: null,
ColumnInfos: [{ Name: "value" }]
}
],
Predicate: null,
GroupBy: [],
Having: null,
IsDistinct: false,
Limit: null,
Offset: null
}
&& TranslateExpression(item, applyDefaultTypeMapping: false) is { } translatedItem
// Literal untyped NULL not supported as item by JSON_CONTAINS().
// For any other nullable item, SqlServerNullabilityProcessor will add a null check around the JSON_CONTAINS call.
&& translatedItem is not SqlConstantExpression { Value: null }
// Note: JSON_CONTAINS doesn't allow searching for null items within a JSON collection (returns 0)
// As a result, we only translate to JSON_CONTAINS when we know that either the item is non-nullable or the collection's elements are.
&& (
translatedItem is ColumnExpression { IsNullable: false } or SqlConstantExpression { Value: not null }
|| !translatedItem.Type.IsNullableType()
|| json.Type.GetSequenceType() is var elementClrType && !elementClrType.IsNullableType()))
{
// JSON_CONTAINS returns 1 if found, 0 if not found. It's a search condition expression.
var jsonContains = _sqlExpressionFactory.Equal(
_sqlExpressionFactory.Function(
"JSON_CONTAINS",
[json, translatedItem],
nullable: true,
argumentsPropagateNullability: [false, true],
typeof(int)),
_sqlExpressionFactory.Constant(1));

#pragma warning disable EF1001 // Internal EF Core API usage.
var selectExpression = new SelectExpression(jsonContains, _queryCompilationContext.SqlAliasManager);
return source.Update(
selectExpression,
Expression.Convert(
new ProjectionBindingExpression(selectExpression, new ProjectionMember(), typeof(bool?)),
typeof(bool)));
#pragma warning restore EF1001 // Internal EF Core API usage.
}

return base.TranslateContains(source, item);
}

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
Expand All @@ -623,8 +688,8 @@ IComplexType complexType
{
switch (source.QueryExpression)
{
// index on parameter using a column
// translate via JSON because it is a better translation
// Index on parameter using a column
// Translate via JSON_VALUE() instead of via a VALUES subquery
case SelectExpression
{
Tables: [ValuesExpression { ValuesParameter: { } valuesParameter }],
Expand Down Expand Up @@ -938,15 +1003,7 @@ protected override bool TrySerializeScalarToJson(
// IReadOnlyList<PathSegment> (just like Json{Scalar,Query}Expression), but instead we do the slight hack of packaging it
// as a constant argument; it will be unpacked and handled in SQL generation.
_sqlExpressionFactory.Constant(path, RelationalTypeMapping.NullMapping),

// If an inline JSON object (complex type) is being assigned, it would be rendered here as a simple string:
// [column].modify('$.foo', '{ "x": 8 }')
// Since it's untyped, modify would treat is as a string rather than a JSON object, and insert it as such into
// the enclosing object, escaping all the special JSON characters - that's not what we want.
// We add a cast to JSON to have it interpreted as a JSON object.
value is SqlConstantExpression { TypeMapping.StoreType: "json" }
? _sqlExpressionFactory.Convert(value, value.Type, _typeMappingSource.FindMapping("json")!)
: value
value
],
nullable: true,
instancePropagatesNullability: true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,48 @@ protected virtual SqlExpression VisitSqlServerAggregateFunction(
: aggregateFunctionExpression;
}

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
protected override SqlExpression VisitSqlFunction(
SqlFunctionExpression sqlFunctionExpression,
bool allowOptimizedExpansion,
out bool nullable)
{
if (sqlFunctionExpression is { Name: "JSON_CONTAINS", Arguments: [var collection, var item] } jsonContains)
{
// JSON_CONTAINS() does not allow searching for NULL within a JSON collection (always returns zero when the item is NULL).
// As a result, we do not translate to JSON_CONTAINS() in SqlServerQueryableMethodTranslatingExpressionVisitor unless we know that
// either the item or the collection's elements are non-nullable.
// When the item argument is nullable, we add a null check around JSON_CONTAINS():
// CASE WHEN @item IS NULL THEN NULL ELSE JSON_CONTAINS(collection, @item) END
item = Visit(item, out var itemNullable);
collection = Visit(collection, out var collectionNullable);

sqlFunctionExpression = jsonContains.Update(instance: null, arguments: [collection, item]);

if (itemNullable && !UseRelationalNulls)
{
nullable = true;
return Dependencies.SqlExpressionFactory.Case(
[
new CaseWhenClause(
Dependencies.SqlExpressionFactory.IsNull(item),
Dependencies.SqlExpressionFactory.Constant(null, typeof(bool?), jsonContains.TypeMapping))
],
jsonContains);
}

nullable = itemNullable || collectionNullable;
return sqlFunctionExpression;
}

return base.VisitSqlFunction(sqlFunctionExpression, allowOptimizedExpansion, out nullable);
}

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,14 +101,15 @@ protected virtual SqlServerOpenJsonExpression ApplyTypeMappingsOnOpenJsonExpress
elementTypeMapping = e;
}

if (parameterTypeMapping is not SqlServerStringTypeMapping { ElementTypeMapping: not null })
if (parameterTypeMapping is not SqlServerStringTypeMapping { ElementTypeMapping: not null }
and not SqlServerJsonTypeMapping { ElementTypeMapping: not null })
{
throw new UnreachableException("A SqlServerStringTypeMapping collection type mapping could not be found");
throw new UnreachableException("A string/JSON collection type mapping was not found");
}

return openJsonExpression.Update(
parameterExpression.ApplyTypeMapping(parameterTypeMapping),
path: null,
[new SqlServerOpenJsonExpression.ColumnInfo("value", elementTypeMapping, [])]);
[new SqlServerOpenJsonExpression.ColumnInfo("value", elementTypeMapping, Path: [])]);
}
}
Loading
Loading