diff --git a/src/EFCore.Relational/Extensions/RelationalDbFunctionsExtensions.cs b/src/EFCore.Relational/Extensions/RelationalDbFunctionsExtensions.cs index f0f6a0e1ea8..e5a95a82700 100644 --- a/src/EFCore.Relational/Extensions/RelationalDbFunctionsExtensions.cs +++ b/src/EFCore.Relational/Extensions/RelationalDbFunctionsExtensions.cs @@ -56,4 +56,17 @@ public static T Greatest( this DbFunctions _, [NotParameterized] params T[] values) => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Greatest))); + + /// + /// Returns a value indicating whether a given JSON path exists within the specified JSON. + /// Usually corresponds to the JSON_PATH_EXISTS SQL function. + /// + /// The instance. + /// The JSON value to check. + /// The JSON path to look for. + public static bool JsonExists( + this DbFunctions _, + object json, + 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 d9952444e5a..c8140c4518d 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 SqlServerJsonFunctionTranslator(sqlExpressionFactory), new SqlServerMathTranslator(sqlExpressionFactory), new SqlServerNewGuidTranslator(sqlExpressionFactory), new SqlServerObjectToStringTranslator(sqlExpressionFactory, typeMappingSource), diff --git a/src/EFCore.SqlServer/Query/Internal/Translators/SqlServerJsonFunctionTranslator.cs b/src/EFCore.SqlServer/Query/Internal/Translators/SqlServerJsonFunctionTranslator.cs new file mode 100644 index 00000000000..948145b1164 --- /dev/null +++ b/src/EFCore.SqlServer/Query/Internal/Translators/SqlServerJsonFunctionTranslator.cs @@ -0,0 +1,39 @@ +// 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; + +// 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 SqlServerJsonFunctionTranslator(ISqlExpressionFactory sqlExpressionFactory) : IMethodCallTranslator +{ + /// + /// 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) + => method.Name == nameof(RelationalDbFunctionsExtensions.JsonExists) + && method.DeclaringType == typeof(RelationalDbFunctionsExtensions) + ? sqlExpressionFactory.Equal( + sqlExpressionFactory.Function( + "JSON_PATH_EXISTS", + [arguments[1], arguments[2]], + nullable: true, + argumentsPropagateNullability: [true, false], + typeof(int)), + sqlExpressionFactory.Constant(1)) + : null; +} diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindDbFunctionsQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindDbFunctionsQuerySqlServerTest.cs index 51f37ee2e49..51ce2f363ca 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindDbFunctionsQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindDbFunctionsQuerySqlServerTest.cs @@ -1348,6 +1348,25 @@ WHERE CAST(DATALENGTH(N'foo') AS int) = 3 } } + [ConditionalFact, SqlServerCondition(SqlServerCondition.SupportsFunctions2022)] + public virtual async Task JsonExists_with_column() + { + await using var context = CreateContext(); + + // Note: Address is not valid JSON, so this will return no results, + // but the purpose is to verify the SQL translation is correct + var result = await context.Customers + .Where(c => EF.Functions.JsonExists(c.Address!, "$.city")) + .ToListAsync(); + + AssertSql( + """ +SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region] +FROM [Customers] AS [c] +WHERE JSON_PATH_EXISTS([c].[Address], N'$.city') = 1 +"""); + } + private void AssertSql(params string[] expected) => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); }