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
Original file line number Diff line number Diff line change
Expand Up @@ -861,6 +861,7 @@ static SqlExpression ProcessJsonQuery(JsonQueryExpression jsonQuery)
/// update function.
/// </param>
/// <returns>A scalar expression ready to be integrated into an UPDATE statement setter.</returns>
[Experimental(EFDiagnostics.ProviderExperimentalApi)] // TODO: This should probably move into the type mappings, #36729
protected virtual bool TrySerializeScalarToJson(
JsonScalarExpression target,
SqlExpression value,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,18 @@ public interface ISqlServerSingletonOptions : ISingletonOptions
/// </summary>
public bool SupportsJsonFunctions { get; }

/// <summary>
/// Whether the <c>JSON_OBJECT()</c> and <c>JSON_ARRAY()</c> functions are supported by the targeted
/// SQL Server engine.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
public bool SupportsJsonObjectArray { get; }

/// <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 @@ -60,6 +60,22 @@ public virtual bool SupportsJsonFunctions
_ => throw new UnreachableException()
};

/// <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 virtual bool SupportsJsonObjectArray
=> EngineType switch
{
SqlServerEngineType.SqlServer => SqlServerCompatibilityLevel >= 160,
SqlServerEngineType.AzureSql => AzureSqlCompatibilityLevel >= 160,
SqlServerEngineType.AzureSynapse => false,
SqlServerEngineType.Unknown => false, // TODO: We shouldn't observe Unknown here, #36477
_ => throw new UnreachableException()
};

/// <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

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions src/EFCore.SqlServer/Properties/SqlServerStrings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,9 @@
<data name="DuplicateKeyMismatchedClustering" xml:space="preserve">
<value>The keys {key1} on '{entityType1}' and {key2} on '{entityType2}' are both mapped to '{table}.{keyName}', but have different clustering configurations.</value>
</data>
<data name="ExecuteUpdateCannotSetJsonPropertyOnOldSqlServer" xml:space="preserve">
<value>'ExecuteUpdate' cannot set a property in a JSON column to an expression containing a column on SQL Server versions before 2022. If you're on SQL Server 2022 and above, your compatibility level may be set to a lower value; consider raising it.</value>
</data>
<data name="IdentityBadType" xml:space="preserve">
<value>Identity value generation cannot be used for the property '{property}' on entity type '{entityType}' because the property type is '{propertyType}'. Identity value generation can only be used with signed integer properties.</value>
</data>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Microsoft.EntityFrameworkCore.Query.SqlExpressions;
using Microsoft.EntityFrameworkCore.SqlServer.Storage.Internal;

namespace Microsoft.EntityFrameworkCore.SqlServer.Query.Internal.SqlExpressions;

/// <summary>
/// An expression that represents a SQL Server <c>JSON_OBJECT()</c> function call in a SQL tree.
/// </summary>
/// <remarks>
/// <para>
/// See <see href="https://learn.microsoft.com/sql/t-sql/functions/json-object-transact-sql">JSON_OBJECT (Transact-SQL)</see>
/// for more information and examples.
/// </para>
/// <para>
/// 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.
/// </para>
/// </remarks>
public sealed class SqlServerJsonObjectExpression : SqlFunctionExpression
{
/// <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 SqlServerJsonObjectExpression(
IReadOnlyList<string> propertyNames,
IReadOnlyList<SqlExpression> propertyValues,
RelationalTypeMapping typeMapping)
: base(
"JSON_OBJECT",
arguments: propertyValues,
nullable: false,
argumentsPropagateNullability: Enumerable.Repeat(false, propertyValues.Count).ToList(),
typeof(string),
typeMapping)
{
if (propertyNames.Count != propertyValues.Count)
{
throw new ArgumentException("The number of property names must match the number of property values.");
}

if (!typeMapping.StoreType.Equals("nvarchar(max)", StringComparison.OrdinalIgnoreCase))
{
throw new ArgumentException("Invalid type mapping for JSON_OBJECT.");
}

PropertyNames = propertyNames;
}

private static ConstructorInfo? _quotingConstructor;

/// <summary>
/// The JSON properties the object consists of.
/// </summary>
public IReadOnlyList<string> PropertyNames { get; }

/// <inheritdoc />
public override Expression Quote()
=> New(
_quotingConstructor ??= typeof(SqlServerJsonObjectExpression).GetConstructor(
[
typeof(IReadOnlyList<string>),
typeof(IReadOnlyList<SqlExpression>),
typeof(SqlServerStringTypeMapping),
])!,
NewArrayInit(typeof(string), initializers: PropertyNames.Select(Constant)),
Arguments is null
? Constant(null, typeof(IEnumerable<SqlExpression>))
: NewArrayInit(typeof(SqlExpression), initializers: Arguments.Select(a => a.Quote())),
RelationalExpressionQuotingUtilities.QuoteTypeMapping(TypeMapping));

/// <inheritdoc />
protected override void Print(ExpressionPrinter expressionPrinter)
{
expressionPrinter.Append("JSON_OBJECT(");

for (var i = 0; i < PropertyNames.Count; i++)
{
var name = PropertyNames[i];
var value = Arguments![i];
if (i > 0)
{
expressionPrinter.Append(", ");
}

expressionPrinter.Append(name).Append(": ");
expressionPrinter.Visit(value);
}

expressionPrinter.Append(")");
}

/// <inheritdoc />
public override bool Equals(object? obj)
=> obj != null
&& (ReferenceEquals(this, obj)
|| obj is SqlServerJsonObjectExpression other
&& Equals(other));

private bool Equals(SqlServerJsonObjectExpression other)
=> base.Equals(other) && PropertyNames.SequenceEqual(other.PropertyNames);

/// <inheritdoc />
public override int GetHashCode()
{
var hashCode = new HashCode();
hashCode.Add(base.GetHashCode());

foreach (var name in PropertyNames)
{
hashCode.Add(name);
}

return hashCode.ToHashCode();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using Microsoft.EntityFrameworkCore.SqlServer.Infrastructure.Internal;
using Microsoft.EntityFrameworkCore.SqlServer.Internal;
using Microsoft.EntityFrameworkCore.SqlServer.Metadata.Internal;
using Microsoft.EntityFrameworkCore.SqlServer.Query.Internal.SqlExpressions;
using Microsoft.EntityFrameworkCore.SqlServer.Storage.Internal;

namespace Microsoft.EntityFrameworkCore.SqlServer.Query.Internal;
Expand Down Expand Up @@ -292,6 +293,26 @@ when string.Equals(sqlFunctionExpression.Name, "COALESCE", StringComparison.Ordi
return base.VisitSqlFunction(sqlFunctionExpression);
}

case SqlServerJsonObjectExpression jsonObject:
{
Sql.Append("JSON_OBJECT(");

for (var i = 0; i < jsonObject.PropertyNames.Count; i++)
{
if (i > 0)
{
Sql.Append(", ");
}

Sql.Append("'").Append(jsonObject.PropertyNames[i]).Append("': ");
Visit(jsonObject.Arguments![i]);
}

Sql.Append(")");

return sqlFunctionExpression;
}

// SQL Server 2025 modify method (https://learn.microsoft.com/sql/t-sql/data-types/json-data-type#modify-method)
// We get here only from within UPDATE setters.
// We generate the syntax here manually rather than just using the regular function visitation logic since
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
using Microsoft.EntityFrameworkCore.SqlServer.Infrastructure.Internal;
using Microsoft.EntityFrameworkCore.SqlServer.Internal;
using Microsoft.EntityFrameworkCore.SqlServer.Metadata.Internal;
using Microsoft.EntityFrameworkCore.SqlServer.Query.Internal.SqlExpressions;
using Microsoft.EntityFrameworkCore.SqlServer.Storage.Internal;
using Microsoft.VisualBasic;

namespace Microsoft.EntityFrameworkCore.SqlServer.Query.Internal;
Expand Down Expand Up @@ -570,6 +572,64 @@ protected override bool TryTranslateSetters(
}
#pragma warning restore EF1001 // Internal EF Core API usage.

/// <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 bool TrySerializeScalarToJson(
JsonScalarExpression target,
SqlExpression value,
[NotNullWhen(true)] out SqlExpression? jsonValue)
{
#pragma warning disable EF9002 // TrySerializeScalarToJson is experimental
// The base implementation handles the types natively supported in JSON (int, string, bool), as well
// as constants/parameters.
if (base.TrySerializeScalarToJson(target, value, out jsonValue))
{
return true;
}
#pragma warning restore EF9002

// geometry/geography are "user-defined types" and therefore not supported by JSON_OBJECT(), which we
// use below for serializing arbitrary relational expressions to JSON. Special-case them and serialize
// as WKT.
if (value.TypeMapping?.StoreType is "geometry" or "geography")
{
jsonValue = _sqlExpressionFactory.Function(
instance: value,
"STAsText",
arguments: [],
nullable: true,
instancePropagatesNullability: true,
argumentsPropagateNullability: [],
typeof(string),
_typeMappingSource.FindMapping("nvarchar(max)"));
return true;
}

// We have some arbitrary relational expression that isn't an int/string/bool; it needs to be converted
// to JSON. Do this by generating JSON_VALUE(JSON_OBJECT('v': foo), '$.v') (supported since SQL Server 2022)
if (_sqlServerSingletonOptions.SupportsJsonObjectArray)
{
jsonValue = new JsonScalarExpression(
new SqlServerJsonObjectExpression(
propertyNames: ["v"],
propertyValues: [value],
SqlServerStructuralJsonTypeMapping.NvarcharMaxDefault),
[new("v")],
typeof(string),
_typeMappingSource.FindMapping("nvarchar(max)"),
nullable: value is ColumnExpression column ? column.IsNullable : true);
return true;
}
else
{
throw new InvalidOperationException(SqlServerStrings.ExecuteUpdateCannotSetJsonPropertyOnOldSqlServer);
}
}

/// <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 @@ -635,7 +635,9 @@ protected override bool TrySerializeScalarToJson(
throw new InvalidOperationException(SqliteStrings.ExecuteUpdateJsonPartialUpdateDoesNotSupportUlong);
}

#pragma warning disable EF9002 // TrySerializeScalarToJson is experimental
return base.TrySerializeScalarToJson(target, value, out jsonValue);
#pragma warning restore EF9002
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,61 +6,49 @@ namespace Microsoft.EntityFrameworkCore.Types.Miscellaneous;
public class BoolTypeTest(BoolTypeTest.BoolTypeFixture fixture)
: TypeTestBase<bool, BoolTypeTest.BoolTypeFixture>(fixture)
{
public class BoolTypeFixture : TypeTestFixture
public class BoolTypeFixture : CosmosTypeFixtureBase<bool>
{
public override bool Value { get; } = true;
public override bool OtherValue { get; } = false;

protected override ITestStoreFactory TestStoreFactory => CosmosTestStoreFactory.Instance;

public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder)
=> base.AddOptions(builder).ConfigureWarnings(c => c.Log(CosmosEventId.NoPartitionKeyDefined));
}
}

public class StringTypeTest(StringTypeTest.StringTypeFixture fixture)
: TypeTestBase<string, StringTypeTest.StringTypeFixture>(fixture)
{
public class StringTypeFixture : TypeTestFixture
public class StringTypeFixture : CosmosTypeFixtureBase<string>
{
public override string Value { get; } = "foo";
public override string OtherValue { get; } = "bar";

protected override ITestStoreFactory TestStoreFactory => CosmosTestStoreFactory.Instance;

public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder)
=> base.AddOptions(builder).ConfigureWarnings(c => c.Log(CosmosEventId.NoPartitionKeyDefined));
}
}

public class GuidTypeTest(GuidTypeTest.GuidTypeFixture fixture)
: TypeTestBase<Guid, GuidTypeTest.GuidTypeFixture>(fixture)
{
public class GuidTypeFixture : TypeTestFixture
public class GuidTypeFixture : CosmosTypeFixtureBase<Guid>
{
public override Guid Value { get; } = new("8f7331d6-cde9-44fb-8611-81fff686f280");
public override Guid OtherValue { get; } = new("ae192c36-9004-49b2-b785-8be10d169627");

protected override ITestStoreFactory TestStoreFactory => CosmosTestStoreFactory.Instance;

public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder)
=> base.AddOptions(builder).ConfigureWarnings(c => c.Log(CosmosEventId.NoPartitionKeyDefined));
}
}

public class ByteArrayTypeTest(ByteArrayTypeTest.ByteArrayTypeFixture fixture)
: TypeTestBase<byte[], ByteArrayTypeTest.ByteArrayTypeFixture>(fixture)
{
public class ByteArrayTypeFixture : TypeTestFixture
public class ByteArrayTypeFixture : CosmosTypeFixtureBase<byte[]>
{
public override byte[] Value { get; } = [1, 2, 3];
public override byte[] OtherValue { get; } = [4, 5, 6, 7];

protected override ITestStoreFactory TestStoreFactory => CosmosTestStoreFactory.Instance;

public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder)
=> base.AddOptions(builder).ConfigureWarnings(c => c.Log(CosmosEventId.NoPartitionKeyDefined));

public override Func<byte[], byte[], bool> Comparer { get; } = (a, b) => a.SequenceEqual(b);
}
}
Loading
Loading