From 48692bae48366770fde32e2b8b472116f72a47bb Mon Sep 17 00:00:00 2001 From: Mohamed Seada Date: Fri, 27 Dec 2024 21:04:29 +0200 Subject: [PATCH 1/8] Add JsonExists DbFunction - Define SqlServer translation - Define Sqllite translation Fixes dotnet#31136 --- .../RelationalDbFunctionsExtensions.cs | 28 ++++++++ .../SqlServerMethodCallTranslatorProvider.cs | 1 + .../SqlServerJsonFunctionsTranslator.cs | 64 +++++++++++++++++ .../SqliteMethodCallTranslatorProvider.cs | 1 + .../SqliteJsonFunctionsTranslator.cs | 68 +++++++++++++++++++ 5 files changed, 162 insertions(+) create mode 100644 src/EFCore.SqlServer/Query/Internal/Translators/SqlServerJsonFunctionsTranslator.cs create mode 100644 src/EFCore.Sqlite.Core/Query/Internal/Translators/SqliteJsonFunctionsTranslator.cs diff --git a/src/EFCore.Relational/Extensions/RelationalDbFunctionsExtensions.cs b/src/EFCore.Relational/Extensions/RelationalDbFunctionsExtensions.cs index f0f6a0e1ea8..1906fac8f8e 100644 --- a/src/EFCore.Relational/Extensions/RelationalDbFunctionsExtensions.cs +++ b/src/EFCore.Relational/Extensions/RelationalDbFunctionsExtensions.cs @@ -56,4 +56,32 @@ public static T Greatest( this DbFunctions _, [NotParameterized] params T[] values) => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Greatest))); + + /// + /// Checks whether a specified JSON path exists within a JSON string. + /// Typically corresponds to a database function or SQL expression. + /// + /// + /// + /// This method is translated to a database-specific function or expression. + /// The support for this function depends on the database and provider being used. + /// Refer to your database provider's documentation for detailed support information. + /// + /// + /// For more details, see EF Core database providers. + /// + /// + /// The instance. + /// The JSON string or column containing JSON text. + /// The JSON path to check for existence. + /// The type of the JSON expression. + /// + /// A nullable boolean value, if the JSON path exists, if not, and + /// when the JSON string is null. + /// + public static bool? JsonExists( + this DbFunctions _, + T expression, + string path) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(JsonExists))); } diff --git a/src/EFCore.SqlServer/Query/Internal/SqlServerMethodCallTranslatorProvider.cs b/src/EFCore.SqlServer/Query/Internal/SqlServerMethodCallTranslatorProvider.cs index 79a99b8c437..394de2a054e 100644 --- a/src/EFCore.SqlServer/Query/Internal/SqlServerMethodCallTranslatorProvider.cs +++ b/src/EFCore.SqlServer/Query/Internal/SqlServerMethodCallTranslatorProvider.cs @@ -38,6 +38,7 @@ public SqlServerMethodCallTranslatorProvider( new SqlServerFullTextSearchFunctionsTranslator(sqlExpressionFactory), new SqlServerIsDateFunctionTranslator(sqlExpressionFactory), new SqlServerIsNumericFunctionTranslator(sqlExpressionFactory), + new SqlServerJsonFunctionsTranslator(sqlExpressionFactory, sqlServerSingletonOptions), new SqlServerMathTranslator(sqlExpressionFactory), new SqlServerNewGuidTranslator(sqlExpressionFactory), new SqlServerObjectToStringTranslator(sqlExpressionFactory, typeMappingSource), diff --git a/src/EFCore.SqlServer/Query/Internal/Translators/SqlServerJsonFunctionsTranslator.cs b/src/EFCore.SqlServer/Query/Internal/Translators/SqlServerJsonFunctionsTranslator.cs new file mode 100644 index 00000000000..6568ab2a094 --- /dev/null +++ b/src/EFCore.SqlServer/Query/Internal/Translators/SqlServerJsonFunctionsTranslator.cs @@ -0,0 +1,64 @@ +// 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.Infrastructure.Internal; +using Microsoft.EntityFrameworkCore.SqlServer.Storage.Internal; + +// ReSharper disable once CheckNamespace +namespace Microsoft.EntityFrameworkCore.SqlServer.Query.Internal; + +/// +/// 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. +/// +public class SqlServerJsonFunctionsTranslator : IMethodCallTranslator +{ + private static readonly MethodInfo JsonExistsMethodInfo = typeof(RelationalDbFunctionsExtensions) + .GetRuntimeMethod(nameof(RelationalDbFunctionsExtensions.JsonExists), [typeof(DbFunctions), typeof(object), typeof(string)])!; + + private readonly ISqlExpressionFactory _sqlExpressionFactory; + private readonly ISqlServerSingletonOptions _sqlServerSingletonOptions; + + /// + /// 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. + /// + public SqlServerJsonFunctionsTranslator(ISqlExpressionFactory sqlExpressionFactory, ISqlServerSingletonOptions sqlServerSingletonOptions) + { + _sqlExpressionFactory = sqlExpressionFactory; + _sqlServerSingletonOptions = sqlServerSingletonOptions; + } + + /// + /// 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. + /// + public virtual SqlExpression? Translate( + SqlExpression? instance, + MethodInfo method, + IReadOnlyList arguments, + IDiagnosticsLogger logger) + { + if (JsonExistsMethodInfo.Equals(method) + && arguments[0].TypeMapping is SqlServerOwnedJsonTypeMapping or StringTypeMapping + && _sqlServerSingletonOptions.EngineType == SqlServerEngineType.SqlServer + && _sqlServerSingletonOptions.SqlServerCompatibilityLevel >= 160) + { + return _sqlExpressionFactory.Function( + "JSON_PATH_EXISTS", + arguments, + nullable: true, + argumentsPropagateNullability: Statics.TrueArrays[2], + method.ReturnType); + } + + return null; + } +} diff --git a/src/EFCore.Sqlite.Core/Query/Internal/SqliteMethodCallTranslatorProvider.cs b/src/EFCore.Sqlite.Core/Query/Internal/SqliteMethodCallTranslatorProvider.cs index 970873af563..1fab485f481 100644 --- a/src/EFCore.Sqlite.Core/Query/Internal/SqliteMethodCallTranslatorProvider.cs +++ b/src/EFCore.Sqlite.Core/Query/Internal/SqliteMethodCallTranslatorProvider.cs @@ -30,6 +30,7 @@ public SqliteMethodCallTranslatorProvider(RelationalMethodCallTranslatorProvider new SqliteDateTimeMethodTranslator(sqlExpressionFactory), new SqliteGlobMethodTranslator(sqlExpressionFactory), new SqliteHexMethodTranslator(sqlExpressionFactory), + new SqliteJsonFunctionsTranslator(sqlExpressionFactory), new SqliteMathTranslator(sqlExpressionFactory), new SqliteObjectToStringTranslator(sqlExpressionFactory), new SqliteRandomTranslator(sqlExpressionFactory), diff --git a/src/EFCore.Sqlite.Core/Query/Internal/Translators/SqliteJsonFunctionsTranslator.cs b/src/EFCore.Sqlite.Core/Query/Internal/Translators/SqliteJsonFunctionsTranslator.cs new file mode 100644 index 00000000000..712814e4d85 --- /dev/null +++ b/src/EFCore.Sqlite.Core/Query/Internal/Translators/SqliteJsonFunctionsTranslator.cs @@ -0,0 +1,68 @@ +// 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.Sqlite.Storage.Internal; + +// ReSharper disable once CheckNamespace +namespace Microsoft.EntityFrameworkCore.Sqlite.Query.Internal; + +/// +/// 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. +/// +public class SqliteJsonFunctionsTranslator : IMethodCallTranslator +{ + private static readonly MethodInfo JsonExistsMethodInfo = typeof(RelationalDbFunctionsExtensions) + .GetRuntimeMethod(nameof(RelationalDbFunctionsExtensions.JsonExists), [typeof(DbFunctions), typeof(object), typeof(string)])!; + + private readonly ISqlExpressionFactory _sqlExpressionFactory; + + /// + /// 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. + /// + public SqliteJsonFunctionsTranslator(ISqlExpressionFactory sqlExpressionFactory) + { + _sqlExpressionFactory = sqlExpressionFactory; + } + + /// + /// 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. + /// + public virtual SqlExpression? Translate( + SqlExpression? instance, + MethodInfo method, + IReadOnlyList arguments, + IDiagnosticsLogger logger) + { + if (JsonExistsMethodInfo.Equals(method) + && arguments[0].TypeMapping is SqliteJsonTypeMapping or StringTypeMapping) + { + // IIF(arguments_0 IS NULL, NULL, JSON_TYPE(arguments_0, arguments_1) IS NOT NULL) + return _sqlExpressionFactory.Function("IFF", + [ + _sqlExpressionFactory.IsNull(arguments[0]), + _sqlExpressionFactory.Fragment("NULL", method.ReturnType), + _sqlExpressionFactory.IsNotNull( + _sqlExpressionFactory.Function("JSON_TYPE", + arguments, + nullable: true, + argumentsPropagateNullability: Statics.TrueArrays[2], + returnType: typeof(string))) + ], + nullable: true, + argumentsPropagateNullability: Statics.TrueArrays[3], + method.ReturnType); + } + + return null; + } +} From ab9e0a87d11cfbefb06a88d2192bca1b2178c6c5 Mon Sep 17 00:00:00 2001 From: Mohamed Seada Date: Sat, 28 Dec 2024 16:25:39 +0200 Subject: [PATCH 2/8] replace IFF function with CASE expression --- .../Translators/SqliteJsonFunctionsTranslator.cs | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/src/EFCore.Sqlite.Core/Query/Internal/Translators/SqliteJsonFunctionsTranslator.cs b/src/EFCore.Sqlite.Core/Query/Internal/Translators/SqliteJsonFunctionsTranslator.cs index 712814e4d85..a08e7dfc632 100644 --- a/src/EFCore.Sqlite.Core/Query/Internal/Translators/SqliteJsonFunctionsTranslator.cs +++ b/src/EFCore.Sqlite.Core/Query/Internal/Translators/SqliteJsonFunctionsTranslator.cs @@ -46,21 +46,17 @@ public SqliteJsonFunctionsTranslator(ISqlExpressionFactory sqlExpressionFactory) if (JsonExistsMethodInfo.Equals(method) && arguments[0].TypeMapping is SqliteJsonTypeMapping or StringTypeMapping) { - // IIF(arguments_0 IS NULL, NULL, JSON_TYPE(arguments_0, arguments_1) IS NOT NULL) - return _sqlExpressionFactory.Function("IFF", - [ - _sqlExpressionFactory.IsNull(arguments[0]), - _sqlExpressionFactory.Fragment("NULL", method.ReturnType), + return _sqlExpressionFactory.Case( + [new CaseWhenClause( + _sqlExpressionFactory.IsNotNull(arguments[0]), _sqlExpressionFactory.IsNotNull( _sqlExpressionFactory.Function("JSON_TYPE", arguments, nullable: true, argumentsPropagateNullability: Statics.TrueArrays[2], - returnType: typeof(string))) + returnType: typeof(string)))) ], - nullable: true, - argumentsPropagateNullability: Statics.TrueArrays[3], - method.ReturnType); + null); } return null; From d55b43e9f4494ef74f7b34c3073bcd0d06cc6a79 Mon Sep 17 00:00:00 2001 From: Mohamed Seada Date: Sat, 14 Jun 2025 04:07:04 +0300 Subject: [PATCH 3/8] Add JsonExists DbFunction - Fix SqlServer translation - Fix Sqllite translation - Add Tests Drafts Fixes dotnet#31136 --- .../RelationalDbFunctionsExtensions.cs | 5 +- .../SqlServerJsonFunctionsTranslator.cs | 4 +- .../SqliteJsonFunctionsTranslator.cs | 6 +-- .../JsonQueryDbFunctionsRelationalTestBase.cs | 42 +++++++++++++++ .../Query/JsonQueryFixtureBase.cs | 51 ++++++++++++++++++ .../JsonQuery/JsonEntityStringConversion.cs | 17 ++++++ .../TestModels/JsonQuery/JsonQueryContext.cs | 3 ++ .../TestModels/JsonQuery/JsonQueryData.cs | 53 +++++++++++++++++++ .../JsonQuery/JsonStringConversionRoot.cs | 15 ++++++ .../JsonQueryDbFunctionsSqlServerTest.cs | 47 ++++++++++++++++ .../Query/JsonQueryDbFunctionsSqliteTest.cs | 49 +++++++++++++++++ 11 files changed, 284 insertions(+), 8 deletions(-) create mode 100644 test/EFCore.Relational.Specification.Tests/Query/JsonQueryDbFunctionsRelationalTestBase.cs create mode 100644 test/EFCore.Specification.Tests/TestModels/JsonQuery/JsonEntityStringConversion.cs create mode 100644 test/EFCore.Specification.Tests/TestModels/JsonQuery/JsonStringConversionRoot.cs create mode 100644 test/EFCore.SqlServer.FunctionalTests/Query/JsonQueryDbFunctionsSqlServerTest.cs create mode 100644 test/EFCore.Sqlite.FunctionalTests/Query/JsonQueryDbFunctionsSqliteTest.cs diff --git a/src/EFCore.Relational/Extensions/RelationalDbFunctionsExtensions.cs b/src/EFCore.Relational/Extensions/RelationalDbFunctionsExtensions.cs index 1906fac8f8e..f08746121d1 100644 --- a/src/EFCore.Relational/Extensions/RelationalDbFunctionsExtensions.cs +++ b/src/EFCore.Relational/Extensions/RelationalDbFunctionsExtensions.cs @@ -74,14 +74,13 @@ public static T Greatest( /// The instance. /// The JSON string or column containing JSON text. /// The JSON path to check for existence. - /// The type of the JSON expression. /// /// A nullable boolean value, if the JSON path exists, if not, and /// when the JSON string is null. /// - public static bool? JsonExists( + public static bool? JsonExists( this DbFunctions _, - T expression, + object expression, string path) => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(JsonExists))); } diff --git a/src/EFCore.SqlServer/Query/Internal/Translators/SqlServerJsonFunctionsTranslator.cs b/src/EFCore.SqlServer/Query/Internal/Translators/SqlServerJsonFunctionsTranslator.cs index 6568ab2a094..107e9c36655 100644 --- a/src/EFCore.SqlServer/Query/Internal/Translators/SqlServerJsonFunctionsTranslator.cs +++ b/src/EFCore.SqlServer/Query/Internal/Translators/SqlServerJsonFunctionsTranslator.cs @@ -47,13 +47,13 @@ public SqlServerJsonFunctionsTranslator(ISqlExpressionFactory sqlExpressionFacto IDiagnosticsLogger logger) { if (JsonExistsMethodInfo.Equals(method) - && arguments[0].TypeMapping is SqlServerOwnedJsonTypeMapping or StringTypeMapping + && (arguments[1].Type.Equals(typeof(string)) || arguments[1].TypeMapping is SqlServerOwnedJsonTypeMapping or StringTypeMapping) && _sqlServerSingletonOptions.EngineType == SqlServerEngineType.SqlServer && _sqlServerSingletonOptions.SqlServerCompatibilityLevel >= 160) { return _sqlExpressionFactory.Function( "JSON_PATH_EXISTS", - arguments, + [arguments[1], arguments[2]], nullable: true, argumentsPropagateNullability: Statics.TrueArrays[2], method.ReturnType); diff --git a/src/EFCore.Sqlite.Core/Query/Internal/Translators/SqliteJsonFunctionsTranslator.cs b/src/EFCore.Sqlite.Core/Query/Internal/Translators/SqliteJsonFunctionsTranslator.cs index a08e7dfc632..fc99f467164 100644 --- a/src/EFCore.Sqlite.Core/Query/Internal/Translators/SqliteJsonFunctionsTranslator.cs +++ b/src/EFCore.Sqlite.Core/Query/Internal/Translators/SqliteJsonFunctionsTranslator.cs @@ -44,14 +44,14 @@ public SqliteJsonFunctionsTranslator(ISqlExpressionFactory sqlExpressionFactory) IDiagnosticsLogger logger) { if (JsonExistsMethodInfo.Equals(method) - && arguments[0].TypeMapping is SqliteJsonTypeMapping or StringTypeMapping) + && (arguments[1].Type.Equals(typeof(string)) || (arguments[1].TypeMapping is SqliteJsonTypeMapping or StringTypeMapping))) { return _sqlExpressionFactory.Case( [new CaseWhenClause( - _sqlExpressionFactory.IsNotNull(arguments[0]), + _sqlExpressionFactory.IsNotNull(arguments[1]), _sqlExpressionFactory.IsNotNull( _sqlExpressionFactory.Function("JSON_TYPE", - arguments, + [arguments[1], arguments[2]], nullable: true, argumentsPropagateNullability: Statics.TrueArrays[2], returnType: typeof(string)))) diff --git a/test/EFCore.Relational.Specification.Tests/Query/JsonQueryDbFunctionsRelationalTestBase.cs b/test/EFCore.Relational.Specification.Tests/Query/JsonQueryDbFunctionsRelationalTestBase.cs new file mode 100644 index 00000000000..0d0ee5e38eb --- /dev/null +++ b/test/EFCore.Relational.Specification.Tests/Query/JsonQueryDbFunctionsRelationalTestBase.cs @@ -0,0 +1,42 @@ +// 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.TestModels.JsonQuery; + +namespace Microsoft.EntityFrameworkCore.Query; + +public abstract class JsonQueryDbFunctionsRelationalTestBase(TFixture fixture) : QueryTestBase(fixture) + where TFixture : JsonQueryRelationalFixture, new() +{ + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task JsonExists_With_ConstantValue(bool async) + => AssertQuery( + async, + ss => ss.Set() + .Where(x => EF.Functions.JsonExists("{\"Name:\": \"Test\"}", "$.Name") == true)); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task JsonExists_With_StringJsonProperty(bool async) + => AssertQuery( + async, + ss => ss.Set() + .Where(x => EF.Functions.JsonExists(x.StringJsonValue, "$.Name") == true)); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task JsonExists_With_StringConversionJsonProperty(bool async) + => AssertQuery( + async, + ss => ss.Set() + .Where(x => EF.Functions.JsonExists(x.ReferenceRoot, "$.Name") == true)); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task JsonExists_With_OwnedJsonProperty(bool async) + => AssertQuery( + async, + ss => ss.Set() + .Where(x => EF.Functions.JsonExists(x.OwnedReferenceRoot, "$.Name") == true)); +} diff --git a/test/EFCore.Specification.Tests/Query/JsonQueryFixtureBase.cs b/test/EFCore.Specification.Tests/Query/JsonQueryFixtureBase.cs index de2bc86c787..aa9f57f28d3 100644 --- a/test/EFCore.Specification.Tests/Query/JsonQueryFixtureBase.cs +++ b/test/EFCore.Specification.Tests/Query/JsonQueryFixtureBase.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Text.Json; using Microsoft.EntityFrameworkCore.TestModels.JsonQuery; namespace Microsoft.EntityFrameworkCore.Query; @@ -23,6 +24,7 @@ public virtual ISetSource GetExpectedData() { typeof(JsonEntityBasic), e => ((JsonEntityBasic)e)?.Id }, { typeof(JsonEntityBasicForReference), e => ((JsonEntityBasicForReference)e)?.Id }, { typeof(JsonEntityBasicForCollection), e => ((JsonEntityBasicForCollection)e)?.Id }, + { typeof(JsonEntityStringConversion), e => ((JsonEntityStringConversion)e)?.Id }, { typeof(JsonEntityCustomNaming), e => ((JsonEntityCustomNaming)e)?.Id }, { typeof(JsonEntitySingleOwned), e => ((JsonEntitySingleOwned)e)?.Id }, { typeof(JsonEntityInheritanceBase), e => ((JsonEntityInheritanceBase)e)?.Id }, @@ -137,6 +139,29 @@ public virtual ISetSource GetExpectedData() } } }, + { + typeof(JsonEntityStringConversion), (e, a) => + { + Assert.Equal(e == null, a == null); + if (a != null) + { + var ee = (JsonEntityStringConversion)e; + var aa = (JsonEntityStringConversion)a; + + Assert.Equal(ee.Id, aa.Id); + Assert.Equal(ee.Name, aa.Name); + Assert.Equal(ee.StringJsonValue, aa.StringJsonValue); + + AssertJsonStringConversionRoot(ee.ReferenceRoot, aa.ReferenceRoot); + + Assert.Equal(ee.CollectionRoot.Count, aa.CollectionRoot.Count); + for (var i = 0; i < ee.CollectionRoot.Count; i++) + { + AssertJsonStringConversionRoot(ee.CollectionRoot[i], aa.CollectionRoot[i]); + } + } + } + }, { typeof(JsonEntityCustomNaming), (e, a) => { @@ -354,6 +379,15 @@ public static void AssertOwnedBranch(JsonOwnedBranch expected, JsonOwnedBranch a public static void AssertOwnedLeaf(JsonOwnedLeaf expected, JsonOwnedLeaf actual) => Assert.Equal(expected.SomethingSomething, actual.SomethingSomething); + + public static void AssertJsonStringConversionRoot(JsonStringConversionRoot expected, JsonStringConversionRoot actual) + { + Assert.Equal(expected.Name, actual.Name); + Assert.Equal(expected.Names, actual.Names); + Assert.Equal(expected.Number, actual.Number); + Assert.Equal(expected.Numbers, actual.Numbers); + } + public static void AssertCustomNameRoot(JsonOwnedCustomNameRoot expected, JsonOwnedCustomNameRoot actual) { Assert.Equal(expected.Name, actual.Name); @@ -506,6 +540,14 @@ protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext con }); }); + + modelBuilder.Ignore(); + modelBuilder.Entity().Property(x => x.Id).ValueGeneratedNever(); + modelBuilder.Entity().Property(x => x.ReferenceRoot) + .HasConversion(new JsonValueConverter()); + modelBuilder.Entity().Property(x => x.CollectionRoot) + .HasConversion(new JsonValueConverter>(), new GenaricListComparer()); + modelBuilder.Entity().Property(x => x.Id).ValueGeneratedNever(); modelBuilder.Entity().OwnsOne( x => x.OwnedReferenceRoot, b => @@ -664,4 +706,13 @@ public override JsonQueryContext CreateContext() protected override async Task SeedAsync(JsonQueryContext context) => await JsonQueryContext.SeedAsync(context); + + protected class JsonValueConverter() : ValueConverter( + v => JsonSerializer.Serialize(v, (JsonSerializerOptions)null), + v => JsonSerializer.Deserialize(v, (JsonSerializerOptions)null)); + + protected class GenaricListComparer() : ValueComparer>( + (c1, c2) => (c1 == null && c2 == null) || (c1 != null && c2 != null && c1.SequenceEqual(c2)), + c => c == null ? 0 : c.Aggregate(0, (a, v) => HashCode.Combine(a, v.GetHashCode())), + c => c == null ? null : c.ToList()); } diff --git a/test/EFCore.Specification.Tests/TestModels/JsonQuery/JsonEntityStringConversion.cs b/test/EFCore.Specification.Tests/TestModels/JsonQuery/JsonEntityStringConversion.cs new file mode 100644 index 00000000000..6ae44c26a70 --- /dev/null +++ b/test/EFCore.Specification.Tests/TestModels/JsonQuery/JsonEntityStringConversion.cs @@ -0,0 +1,17 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.EntityFrameworkCore.TestModels.JsonQuery; + +#nullable disable + +public class JsonEntityStringConversion +{ + public int Id { get; set; } + public string Name { get; set; } + + public string StringJsonValue { get; set; } + + public JsonStringConversionRoot ReferenceRoot { get; set; } + public List CollectionRoot { get; set; } +} diff --git a/test/EFCore.Specification.Tests/TestModels/JsonQuery/JsonQueryContext.cs b/test/EFCore.Specification.Tests/TestModels/JsonQuery/JsonQueryContext.cs index b6010b6182f..ffa52fba6e8 100644 --- a/test/EFCore.Specification.Tests/TestModels/JsonQuery/JsonQueryContext.cs +++ b/test/EFCore.Specification.Tests/TestModels/JsonQuery/JsonQueryContext.cs @@ -11,6 +11,7 @@ public class JsonQueryContext(DbContextOptions options) : DbContext(options) public DbSet JsonEntitiesBasic { get; set; } public DbSet JsonEntitiesBasicForReference { get; set; } public DbSet JsonEntitiesBasicForCollection { get; set; } + public DbSet JsonEntitiesStringConversion { get; set; } public DbSet JsonEntitiesCustomNaming { get; set; } public DbSet JsonEntitiesSingleOwned { get; set; } public DbSet JsonEntitiesInheritance { get; set; } @@ -25,6 +26,7 @@ public static Task SeedAsync(JsonQueryContext context) var jsonEntitiesBasicForCollection = JsonQueryData.CreateJsonEntitiesBasicForCollection(); JsonQueryData.WireUp(jsonEntitiesBasic, entitiesBasic, jsonEntitiesBasicForReference, jsonEntitiesBasicForCollection); + var jsonEntitiesStringConversion = JsonQueryData.CreateJsonEntitiesStringConversion(); var jsonEntitiesCustomNaming = JsonQueryData.CreateJsonEntitiesCustomNaming(); var jsonEntitiesSingleOwned = JsonQueryData.CreateJsonEntitiesSingleOwned(); var jsonEntitiesInheritance = JsonQueryData.CreateJsonEntitiesInheritance(); @@ -35,6 +37,7 @@ public static Task SeedAsync(JsonQueryContext context) context.EntitiesBasic.AddRange(entitiesBasic); context.JsonEntitiesBasicForReference.AddRange(jsonEntitiesBasicForReference); context.JsonEntitiesBasicForCollection.AddRange(jsonEntitiesBasicForCollection); + context.JsonEntitiesStringConversion.AddRange(jsonEntitiesStringConversion); context.JsonEntitiesCustomNaming.AddRange(jsonEntitiesCustomNaming); context.JsonEntitiesSingleOwned.AddRange(jsonEntitiesSingleOwned); context.JsonEntitiesInheritance.AddRange(jsonEntitiesInheritance); diff --git a/test/EFCore.Specification.Tests/TestModels/JsonQuery/JsonQueryData.cs b/test/EFCore.Specification.Tests/TestModels/JsonQuery/JsonQueryData.cs index 71d19bc0b18..c16b4621acd 100644 --- a/test/EFCore.Specification.Tests/TestModels/JsonQuery/JsonQueryData.cs +++ b/test/EFCore.Specification.Tests/TestModels/JsonQuery/JsonQueryData.cs @@ -17,6 +17,7 @@ public JsonQueryData() JsonEntitiesBasicForCollection = CreateJsonEntitiesBasicForCollection(); WireUp(JsonEntitiesBasic, EntitiesBasic, JsonEntitiesBasicForReference, JsonEntitiesBasicForCollection); + JsonEntitiesStringConversion = CreateJsonEntitiesStringConversion(); JsonEntitiesCustomNaming = CreateJsonEntitiesCustomNaming(); JsonEntitiesSingleOwned = CreateJsonEntitiesSingleOwned(); JsonEntitiesInheritance = CreateJsonEntitiesInheritance(); @@ -28,6 +29,7 @@ public JsonQueryData() public IReadOnlyList JsonEntitiesBasic { get; } public IReadOnlyList JsonEntitiesBasicForReference { get; } public IReadOnlyList JsonEntitiesBasicForCollection { get; } + public IReadOnlyList JsonEntitiesStringConversion { get; } public IReadOnlyList JsonEntitiesCustomNaming { get; set; } public IReadOnlyList JsonEntitiesSingleOwned { get; set; } public IReadOnlyList JsonEntitiesInheritance { get; set; } @@ -387,6 +389,52 @@ public static void WireUp( entitiesBasicForCollection[2].ParentId = jsonEntitiesBasic[0].Id; } + + public static IReadOnlyList CreateJsonEntitiesStringConversion() + { + var e1_r = new JsonStringConversionRoot + { + Name = "e1_r", + Number = 12, + Names = ["e1_r1", "e1_r2"], + Numbers = [-1001, 0, 1001] + }; + + var e1_c1 = new JsonStringConversionRoot + { + Name = "e1_c1", + Number = 12, + Names = ["e1_c11", "e1_c12"], + Numbers = [-1001, 0, 1001] + }; + + var e1_c2 = new JsonStringConversionRoot + { + Name = "e1_c2", + Number = 12, + Names = ["e1_c21", "e1_c22"], + Numbers = [-1001, 0, 1001] + }; + + var entity1 = new JsonEntityStringConversion + { + Id = 1, + Name = "JsonEntityStringConversion1", + StringJsonValue = """ + { + "Name": "e1_s1", + "Number": 12, + "Names": ["e1_c21", "e1_c22"], + "Numbers": [-1001, 0, 1001] + } + """, + ReferenceRoot = e1_r, + CollectionRoot = [e1_c1, e1_c2] + }; + + return new List { entity1 }; + } + public static IReadOnlyList CreateJsonEntitiesCustomNaming() { var e1_r_r = new JsonOwnedCustomNameBranch @@ -1483,6 +1531,11 @@ public IQueryable Set() return (IQueryable)JsonEntitiesBasic.AsQueryable(); } + if (typeof(TEntity) == typeof(JsonEntityStringConversion)) + { + return (IQueryable)JsonEntitiesStringConversion.AsQueryable(); + } + if (typeof(TEntity) == typeof(JsonEntityCustomNaming)) { return (IQueryable)JsonEntitiesCustomNaming.AsQueryable(); diff --git a/test/EFCore.Specification.Tests/TestModels/JsonQuery/JsonStringConversionRoot.cs b/test/EFCore.Specification.Tests/TestModels/JsonQuery/JsonStringConversionRoot.cs new file mode 100644 index 00000000000..346b78d841d --- /dev/null +++ b/test/EFCore.Specification.Tests/TestModels/JsonQuery/JsonStringConversionRoot.cs @@ -0,0 +1,15 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.EntityFrameworkCore.TestModels.JsonQuery; + +#nullable disable + +public class JsonStringConversionRoot +{ + public int Id { get; set; } + public string Name { get; set; } + public int Number { get; set; } + public string[] Names { get; set; } + public int[] Numbers { get; set; } +} diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/JsonQueryDbFunctionsSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/JsonQueryDbFunctionsSqlServerTest.cs new file mode 100644 index 00000000000..8cbb8ba5a8b --- /dev/null +++ b/test/EFCore.SqlServer.FunctionalTests/Query/JsonQueryDbFunctionsSqlServerTest.cs @@ -0,0 +1,47 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Data.SqlClient; +using Microsoft.EntityFrameworkCore.TestModels.JsonQuery; + +namespace Microsoft.EntityFrameworkCore.Query; + +#nullable disable + +public class JsonQueryDbFunctionsSqlServerTest : JsonQueryDbFunctionsRelationalTestBase +{ + public JsonQueryDbFunctionsSqlServerTest(JsonQuerySqlServerFixture fixture, ITestOutputHelper testOutputHelper) + : base(fixture) + { + Fixture.TestSqlLoggerFactory.Clear(); + Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + } + + + public override async Task JsonExists_With_ConstantValue(bool async) + { + await base.JsonExists_With_ConstantValue(async); + // TODO: AssertSql + } + + public override async Task JsonExists_With_StringJsonProperty(bool async) + { + await base.JsonExists_With_StringConversionJsonProperty(async); + // TODO: AssertSql + } + + public override async Task JsonExists_With_StringConversionJsonProperty(bool async) + { + await base.JsonExists_With_StringConversionJsonProperty(async); + // TODO: AssertSql + } + + public override async Task JsonExists_With_OwnedJsonProperty(bool async) + { + await base.JsonExists_With_OwnedJsonProperty(async); + // TODO: AssertSql + } + + private void AssertSql(params string[] expected) + => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); +} diff --git a/test/EFCore.Sqlite.FunctionalTests/Query/JsonQueryDbFunctionsSqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Query/JsonQueryDbFunctionsSqliteTest.cs new file mode 100644 index 00000000000..d77ed24fa2d --- /dev/null +++ b/test/EFCore.Sqlite.FunctionalTests/Query/JsonQueryDbFunctionsSqliteTest.cs @@ -0,0 +1,49 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Data.Sqlite; +using Microsoft.EntityFrameworkCore.Sqlite.Internal; +using Microsoft.EntityFrameworkCore.TestModels.JsonQuery; +using Xunit.Sdk; + +namespace Microsoft.EntityFrameworkCore.Query; + +#nullable disable + +public class JsonQueryDbFunctionsSqliteTest : JsonQueryDbFunctionsRelationalTestBase +{ + public JsonQueryDbFunctionsSqliteTest(JsonQuerySqliteFixture fixture, ITestOutputHelper testOutputHelper) + : base(fixture) + { + Fixture.TestSqlLoggerFactory.Clear(); + Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + } + + + public override async Task JsonExists_With_ConstantValue(bool async) + { + await base.JsonExists_With_ConstantValue(async); + // TODO: AssertSql + } + + public override async Task JsonExists_With_StringJsonProperty(bool async) + { + await base.JsonExists_With_StringConversionJsonProperty(async); + // TODO: AssertSql + } + + public override async Task JsonExists_With_StringConversionJsonProperty(bool async) + { + await base.JsonExists_With_StringConversionJsonProperty(async); + // TODO: AssertSql + } + + public override async Task JsonExists_With_OwnedJsonProperty(bool async) + { + await base.JsonExists_With_OwnedJsonProperty(async); + // TODO: AssertSql + } + + private void AssertSql(params string[] expected) + => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); +} From 17ab03587af3ddb1cd1d5a5e25a8d2090f6eded0 Mon Sep 17 00:00:00 2001 From: Mohamed Seada Date: Sat, 14 Jun 2025 04:35:46 +0300 Subject: [PATCH 4/8] configurer sql server 160 Compatibility Level --- .../RelationalDbFunctionsExtensions.cs | 2 +- .../JsonQueryDbFunctionsSqlServerFixture.cs | 16 ++++++++++++++++ .../Query/JsonQueryDbFunctionsSqlServerTest.cs | 4 ++-- 3 files changed, 19 insertions(+), 3 deletions(-) create mode 100644 test/EFCore.SqlServer.FunctionalTests/Query/JsonQueryDbFunctionsSqlServerFixture.cs diff --git a/src/EFCore.Relational/Extensions/RelationalDbFunctionsExtensions.cs b/src/EFCore.Relational/Extensions/RelationalDbFunctionsExtensions.cs index f08746121d1..dc0083a4d2d 100644 --- a/src/EFCore.Relational/Extensions/RelationalDbFunctionsExtensions.cs +++ b/src/EFCore.Relational/Extensions/RelationalDbFunctionsExtensions.cs @@ -81,6 +81,6 @@ public static T Greatest( public static bool? JsonExists( this DbFunctions _, object expression, - string path) + [NotParameterized] string path) => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(JsonExists))); } diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/JsonQueryDbFunctionsSqlServerFixture.cs b/test/EFCore.SqlServer.FunctionalTests/Query/JsonQueryDbFunctionsSqlServerFixture.cs new file mode 100644 index 00000000000..35ec97865f6 --- /dev/null +++ b/test/EFCore.SqlServer.FunctionalTests/Query/JsonQueryDbFunctionsSqlServerFixture.cs @@ -0,0 +1,16 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.EntityFrameworkCore.Query; + +#nullable disable + +public class JsonQueryDbFunctionsSqlServerFixture: JsonQuerySqlServerFixture +{ + protected override ITestStoreFactory TestStoreFactory + => SqlServerTestStoreFactory.Instance; + + public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder) + => base.AddOptions(builder).UseSqlServer(b => b.UseCompatibilityLevel(160)); + +} diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/JsonQueryDbFunctionsSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/JsonQueryDbFunctionsSqlServerTest.cs index 8cbb8ba5a8b..f6a450146f5 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/JsonQueryDbFunctionsSqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/JsonQueryDbFunctionsSqlServerTest.cs @@ -8,9 +8,9 @@ namespace Microsoft.EntityFrameworkCore.Query; #nullable disable -public class JsonQueryDbFunctionsSqlServerTest : JsonQueryDbFunctionsRelationalTestBase +public class JsonQueryDbFunctionsSqlServerTest : JsonQueryDbFunctionsRelationalTestBase { - public JsonQueryDbFunctionsSqlServerTest(JsonQuerySqlServerFixture fixture, ITestOutputHelper testOutputHelper) + public JsonQueryDbFunctionsSqlServerTest(JsonQueryDbFunctionsSqlServerFixture fixture, ITestOutputHelper testOutputHelper) : base(fixture) { Fixture.TestSqlLoggerFactory.Clear(); From 522b0e47a158d7db64bf64b93c255d7c07d515b1 Mon Sep 17 00:00:00 2001 From: Mohamed Seada Date: Sat, 14 Jun 2025 06:14:43 +0300 Subject: [PATCH 5/8] fix nullable sql server expression return type --- .../Internal/Translators/SqlServerJsonFunctionsTranslator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/EFCore.SqlServer/Query/Internal/Translators/SqlServerJsonFunctionsTranslator.cs b/src/EFCore.SqlServer/Query/Internal/Translators/SqlServerJsonFunctionsTranslator.cs index 107e9c36655..102afe5574b 100644 --- a/src/EFCore.SqlServer/Query/Internal/Translators/SqlServerJsonFunctionsTranslator.cs +++ b/src/EFCore.SqlServer/Query/Internal/Translators/SqlServerJsonFunctionsTranslator.cs @@ -56,7 +56,7 @@ public SqlServerJsonFunctionsTranslator(ISqlExpressionFactory sqlExpressionFacto [arguments[1], arguments[2]], nullable: true, argumentsPropagateNullability: Statics.TrueArrays[2], - method.ReturnType); + typeof(bool)); } return null; From 1e0aa367dc791724c354c70b22f259266ec4c630 Mon Sep 17 00:00:00 2001 From: Mohamed Seada Date: Sat, 14 Jun 2025 12:50:58 +0300 Subject: [PATCH 6/8] update test assertion --- .../JsonQueryDbFunctionsRelationalTestBase.cs | 33 ++++++++++++++----- .../JsonQueryDbFunctionsSqlServerTest.cs | 20 +++++++++-- .../Query/JsonQueryDbFunctionsSqliteTest.cs | 26 +++++++++++++-- 3 files changed, 65 insertions(+), 14 deletions(-) diff --git a/test/EFCore.Relational.Specification.Tests/Query/JsonQueryDbFunctionsRelationalTestBase.cs b/test/EFCore.Relational.Specification.Tests/Query/JsonQueryDbFunctionsRelationalTestBase.cs index 0d0ee5e38eb..41dbbcd6759 100644 --- a/test/EFCore.Relational.Specification.Tests/Query/JsonQueryDbFunctionsRelationalTestBase.cs +++ b/test/EFCore.Relational.Specification.Tests/Query/JsonQueryDbFunctionsRelationalTestBase.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Text.Json; using Microsoft.EntityFrameworkCore.TestModels.JsonQuery; namespace Microsoft.EntityFrameworkCore.Query; @@ -13,30 +14,46 @@ public abstract class JsonQueryDbFunctionsRelationalTestBase(TFixture public virtual Task JsonExists_With_ConstantValue(bool async) => AssertQuery( async, - ss => ss.Set() - .Where(x => EF.Functions.JsonExists("{\"Name:\": \"Test\"}", "$.Name") == true)); + ss => ss.Set().Where(x => EF.Functions.JsonExists("{\"Name\": \"Test\"}", "$.Name") == true), + ss => ss.Set()); [ConditionalTheory] [MemberData(nameof(IsAsyncData))] public virtual Task JsonExists_With_StringJsonProperty(bool async) => AssertQuery( async, - ss => ss.Set() - .Where(x => EF.Functions.JsonExists(x.StringJsonValue, "$.Name") == true)); + ss => ss.Set().Where(x => EF.Functions.JsonExists(x.StringJsonValue, "$.Name") == true), + ss => ss.Set().Where(x => HasJsonProperty(x.StringJsonValue, "Name") == true)); [ConditionalTheory] [MemberData(nameof(IsAsyncData))] public virtual Task JsonExists_With_StringConversionJsonProperty(bool async) => AssertQuery( async, - ss => ss.Set() - .Where(x => EF.Functions.JsonExists(x.ReferenceRoot, "$.Name") == true)); + ss => ss.Set().Where(x => EF.Functions.JsonExists(x.ReferenceRoot, "$.Name") == true), + ss => ss.Set()); [ConditionalTheory] [MemberData(nameof(IsAsyncData))] public virtual Task JsonExists_With_OwnedJsonProperty(bool async) => AssertQuery( async, - ss => ss.Set() - .Where(x => EF.Functions.JsonExists(x.OwnedReferenceRoot, "$.Name") == true)); + ss => ss.Set().Where(x => EF.Functions.JsonExists(x.OwnedReferenceRoot, "$.Name") == true), + ss => ss.Set()); + + private static bool? HasJsonProperty(string jsonString, string propertyName) + { + if (jsonString is null) + return null; + + try + { + using var document = JsonDocument.Parse(jsonString); + return document.RootElement.TryGetProperty(propertyName, out _); + } + catch (JsonException) + { + return false; + } + } } diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/JsonQueryDbFunctionsSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/JsonQueryDbFunctionsSqlServerTest.cs index f6a450146f5..d7c733510ad 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/JsonQueryDbFunctionsSqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/JsonQueryDbFunctionsSqlServerTest.cs @@ -22,24 +22,38 @@ public override async Task JsonExists_With_ConstantValue(bool async) { await base.JsonExists_With_ConstantValue(async); // TODO: AssertSql + + AssertSql(); } public override async Task JsonExists_With_StringJsonProperty(bool async) { - await base.JsonExists_With_StringConversionJsonProperty(async); - // TODO: AssertSql + await base.JsonExists_With_StringJsonProperty(async); + + AssertSql(""" +SELECT [j].[Id], [j].[CollectionRoot], [j].[Name], [j].[ReferenceRoot], [j].[StringJsonValue] +FROM [JsonEntitiesStringConversion] AS [j] +WHERE JSON_PATH_EXISTS([j].[StringJsonValue], N'$.Name') = CAST(1 AS bit) +"""); } public override async Task JsonExists_With_StringConversionJsonProperty(bool async) { await base.JsonExists_With_StringConversionJsonProperty(async); - // TODO: AssertSql + + AssertSql(""" +SELECT [j].[Id], [j].[CollectionRoot], [j].[Name], [j].[ReferenceRoot], [j].[StringJsonValue] +FROM [JsonEntitiesStringConversion] AS [j] +WHERE JSON_PATH_EXISTS([j].[ReferenceRoot], N'$.Name') = CAST(1 AS bit) +"""); } public override async Task JsonExists_With_OwnedJsonProperty(bool async) { await base.JsonExists_With_OwnedJsonProperty(async); // TODO: AssertSql + + AssertSql(); } private void AssertSql(params string[] expected) diff --git a/test/EFCore.Sqlite.FunctionalTests/Query/JsonQueryDbFunctionsSqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Query/JsonQueryDbFunctionsSqliteTest.cs index d77ed24fa2d..a7d98ef658b 100644 --- a/test/EFCore.Sqlite.FunctionalTests/Query/JsonQueryDbFunctionsSqliteTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/Query/JsonQueryDbFunctionsSqliteTest.cs @@ -24,24 +24,44 @@ public override async Task JsonExists_With_ConstantValue(bool async) { await base.JsonExists_With_ConstantValue(async); // TODO: AssertSql + + AssertSql(); } public override async Task JsonExists_With_StringJsonProperty(bool async) { - await base.JsonExists_With_StringConversionJsonProperty(async); - // TODO: AssertSql + await base.JsonExists_With_StringJsonProperty(async); + + AssertSql(""" +SELECT "j"."Id", "j"."CollectionRoot", "j"."Name", "j"."ReferenceRoot", "j"."StringJsonValue" +FROM "JsonEntitiesStringConversion" AS "j" +WHERE CASE + WHEN "j"."StringJsonValue" IS NOT NULL THEN JSON_TYPE("j"."StringJsonValue", '$.Name') IS NOT NULL + ELSE NULL +END +"""); } public override async Task JsonExists_With_StringConversionJsonProperty(bool async) { await base.JsonExists_With_StringConversionJsonProperty(async); - // TODO: AssertSql + + AssertSql(""" +SELECT "j"."Id", "j"."CollectionRoot", "j"."Name", "j"."ReferenceRoot", "j"."StringJsonValue" +FROM "JsonEntitiesStringConversion" AS "j" +WHERE CASE + WHEN "j"."ReferenceRoot" IS NOT NULL THEN JSON_TYPE("j"."StringJsonValue", '$.Name') IS NOT NULL + ELSE NULL +END +"""); } public override async Task JsonExists_With_OwnedJsonProperty(bool async) { await base.JsonExists_With_OwnedJsonProperty(async); // TODO: AssertSql + + AssertSql(); } private void AssertSql(params string[] expected) From 0dcce489c9538473fb5632e8ed8d644a7a8b16f4 Mon Sep 17 00:00:00 2001 From: Mohamed Seada Date: Sat, 14 Jun 2025 13:17:38 +0300 Subject: [PATCH 7/8] update tests --- .../Query/JsonQueryDbFunctionsSqlServerTest.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/JsonQueryDbFunctionsSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/JsonQueryDbFunctionsSqlServerTest.cs index d7c733510ad..9f0d089c425 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/JsonQueryDbFunctionsSqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/JsonQueryDbFunctionsSqlServerTest.cs @@ -21,9 +21,12 @@ public JsonQueryDbFunctionsSqlServerTest(JsonQueryDbFunctionsSqlServerFixture fi public override async Task JsonExists_With_ConstantValue(bool async) { await base.JsonExists_With_ConstantValue(async); - // TODO: AssertSql - AssertSql(); + AssertSql(""" +SELECT [j].[Id], [j].[EntityBasicId], [j].[Name], [j].[OwnedCollectionRoot], [j].[OwnedReferenceRoot] +FROM [JsonEntitiesBasic] AS [j] +WHERE JSON_PATH_EXISTS(N'{"Name": "Test"}', N'$.Name') = CAST(1 AS bit) +"""); } public override async Task JsonExists_With_StringJsonProperty(bool async) From e186ed74caec92eec37a8e2d8e764d115f86b1ac Mon Sep 17 00:00:00 2001 From: Mohamed Seada Date: Sat, 14 Jun 2025 22:37:34 +0300 Subject: [PATCH 8/8] fix sqllite translation and null propagation issue --- .../Translators/SqliteJsonFunctionsTranslator.cs | 2 +- .../Query/JsonQueryDbFunctionsSqliteTest.cs | 11 ++++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/EFCore.Sqlite.Core/Query/Internal/Translators/SqliteJsonFunctionsTranslator.cs b/src/EFCore.Sqlite.Core/Query/Internal/Translators/SqliteJsonFunctionsTranslator.cs index fc99f467164..df2a9ddbc0b 100644 --- a/src/EFCore.Sqlite.Core/Query/Internal/Translators/SqliteJsonFunctionsTranslator.cs +++ b/src/EFCore.Sqlite.Core/Query/Internal/Translators/SqliteJsonFunctionsTranslator.cs @@ -53,7 +53,7 @@ [new CaseWhenClause( _sqlExpressionFactory.Function("JSON_TYPE", [arguments[1], arguments[2]], nullable: true, - argumentsPropagateNullability: Statics.TrueArrays[2], + argumentsPropagateNullability: Statics.FalseArrays[2], returnType: typeof(string)))) ], null); diff --git a/test/EFCore.Sqlite.FunctionalTests/Query/JsonQueryDbFunctionsSqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Query/JsonQueryDbFunctionsSqliteTest.cs index a7d98ef658b..d7a47c957fa 100644 --- a/test/EFCore.Sqlite.FunctionalTests/Query/JsonQueryDbFunctionsSqliteTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/Query/JsonQueryDbFunctionsSqliteTest.cs @@ -23,9 +23,12 @@ public JsonQueryDbFunctionsSqliteTest(JsonQuerySqliteFixture fixture, ITestOutpu public override async Task JsonExists_With_ConstantValue(bool async) { await base.JsonExists_With_ConstantValue(async); - // TODO: AssertSql - AssertSql(); + AssertSql(""" +SELECT "j"."Id", "j"."EntityBasicId", "j"."Name", "j"."OwnedCollectionRoot", "j"."OwnedReferenceRoot" +FROM "JsonEntitiesBasic" AS "j" +WHERE JSON_TYPE('{"Name": "Test"}', '$.Name') IS NOT NULL +"""); } public override async Task JsonExists_With_StringJsonProperty(bool async) @@ -37,7 +40,6 @@ public override async Task JsonExists_With_StringJsonProperty(bool async) FROM "JsonEntitiesStringConversion" AS "j" WHERE CASE WHEN "j"."StringJsonValue" IS NOT NULL THEN JSON_TYPE("j"."StringJsonValue", '$.Name') IS NOT NULL - ELSE NULL END """); } @@ -50,8 +52,7 @@ public override async Task JsonExists_With_StringConversionJsonProperty(bool asy SELECT "j"."Id", "j"."CollectionRoot", "j"."Name", "j"."ReferenceRoot", "j"."StringJsonValue" FROM "JsonEntitiesStringConversion" AS "j" WHERE CASE - WHEN "j"."ReferenceRoot" IS NOT NULL THEN JSON_TYPE("j"."StringJsonValue", '$.Name') IS NOT NULL - ELSE NULL + WHEN "j"."ReferenceRoot" IS NOT NULL THEN JSON_TYPE("j"."ReferenceRoot", '$.Name') IS NOT NULL END """); }