diff --git a/src/EFCore.SqlServer/Query/Internal/SqlServerMethodCallTranslatorProvider.cs b/src/EFCore.SqlServer/Query/Internal/SqlServerMethodCallTranslatorProvider.cs index 089ead02481..9c960d29c1c 100644 --- a/src/EFCore.SqlServer/Query/Internal/SqlServerMethodCallTranslatorProvider.cs +++ b/src/EFCore.SqlServer/Query/Internal/SqlServerMethodCallTranslatorProvider.cs @@ -1,6 +1,8 @@ // 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.SqlServer.Infrastructure.Internal; + namespace Microsoft.EntityFrameworkCore.SqlServer.Query.Internal; /// @@ -17,7 +19,7 @@ public class SqlServerMethodCallTranslatorProvider : RelationalMethodCallTransla /// 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 SqlServerMethodCallTranslatorProvider(RelationalMethodCallTranslatorProviderDependencies dependencies) + public SqlServerMethodCallTranslatorProvider(RelationalMethodCallTranslatorProviderDependencies dependencies, ISqlServerSingletonOptions sqlServerSingletonOptions) : base(dependencies) { var sqlExpressionFactory = dependencies.SqlExpressionFactory; @@ -37,7 +39,7 @@ public SqlServerMethodCallTranslatorProvider(RelationalMethodCallTranslatorProvi new SqlServerMathTranslator(sqlExpressionFactory), new SqlServerNewGuidTranslator(sqlExpressionFactory), new SqlServerObjectToStringTranslator(sqlExpressionFactory, typeMappingSource), - new SqlServerStringMethodTranslator(sqlExpressionFactory), + new SqlServerStringMethodTranslator(sqlExpressionFactory, sqlServerSingletonOptions), new SqlServerTimeOnlyMethodTranslator(sqlExpressionFactory) ]); } diff --git a/src/EFCore.SqlServer/Query/Internal/Translators/SqlServerStringMethodTranslator.cs b/src/EFCore.SqlServer/Query/Internal/Translators/SqlServerStringMethodTranslator.cs index f35e17a2941..f1663ec86cb 100644 --- a/src/EFCore.SqlServer/Query/Internal/Translators/SqlServerStringMethodTranslator.cs +++ b/src/EFCore.SqlServer/Query/Internal/Translators/SqlServerStringMethodTranslator.cs @@ -1,7 +1,9 @@ // 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; using Microsoft.EntityFrameworkCore.Query.SqlExpressions; +using Microsoft.EntityFrameworkCore.SqlServer.Infrastructure.Internal; using ExpressionExtensions = Microsoft.EntityFrameworkCore.Query.ExpressionExtensions; // ReSharper disable once CheckNamespace @@ -62,6 +64,12 @@ private static readonly MethodInfo TrimEndMethodInfoWithCharArrayArg private static readonly MethodInfo TrimMethodInfoWithCharArrayArg = typeof(string).GetRuntimeMethod(nameof(string.Trim), [typeof(char[])])!; + private static readonly MethodInfo TrimStartMethodInfoWithCharArg + = typeof(string).GetRuntimeMethod(nameof(string.TrimStart), [typeof(char)])!; + + private static readonly MethodInfo TrimEndMethodInfoWithCharArg + = typeof(string).GetRuntimeMethod(nameof(string.TrimEnd), [typeof(char)])!; + private static readonly MethodInfo FirstOrDefaultMethodInfoWithoutArgs = typeof(Enumerable).GetRuntimeMethods().Single( m => m.Name == nameof(Enumerable.FirstOrDefault) @@ -79,15 +87,19 @@ private static readonly MethodInfo PatIndexMethodInfo 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 SqlServerStringMethodTranslator(ISqlExpressionFactory sqlExpressionFactory) + public SqlServerStringMethodTranslator(ISqlExpressionFactory sqlExpressionFactory, ISqlServerSingletonOptions sqlServerSingletonOptions) { _sqlExpressionFactory = sqlExpressionFactory; + + _sqlServerSingletonOptions = sqlServerSingletonOptions; } /// @@ -186,38 +198,26 @@ public SqlServerStringMethodTranslator(ISqlExpressionFactory sqlExpressionFactor instance.TypeMapping); } - if (TrimStartMethodInfoWithoutArgs.Equals(method) - || (TrimStartMethodInfoWithCharArrayArg.Equals(method) - // SqlServer LTRIM does not take arguments - && ((arguments[0] as SqlConstantExpression)?.Value as Array)?.Length == 0)) + // There's single-parameter LTRIM/RTRIM for all versions (trims whitespace), but startin with SQL Server 2022 there's also + // an overload that accepts the characters to trim. + if (method == TrimStartMethodInfoWithoutArgs + || (method == TrimStartMethodInfoWithCharArrayArg && arguments[0] is SqlConstantExpression { Value: char[] { Length: 0 } }) + || (_sqlServerSingletonOptions.CompatibilityLevel >= 160 + && (method == TrimStartMethodInfoWithCharArg || method == TrimStartMethodInfoWithCharArrayArg))) { - return _sqlExpressionFactory.Function( - "LTRIM", - new[] { instance }, - nullable: true, - argumentsPropagateNullability: new[] { true }, - instance.Type, - instance.TypeMapping); + return ProcessTrimStartEnd(instance, arguments, "LTRIM"); } - if (TrimEndMethodInfoWithoutArgs.Equals(method) - || (TrimEndMethodInfoWithCharArrayArg.Equals(method) - // SqlServer RTRIM does not take arguments - && ((arguments[0] as SqlConstantExpression)?.Value as Array)?.Length == 0)) + if (method == TrimEndMethodInfoWithoutArgs + || (method == TrimEndMethodInfoWithCharArrayArg && arguments[0] is SqlConstantExpression { Value: char[] { Length: 0 } }) + || (_sqlServerSingletonOptions.CompatibilityLevel >= 160 + && (method == TrimEndMethodInfoWithCharArg || method == TrimEndMethodInfoWithCharArrayArg))) { - return _sqlExpressionFactory.Function( - "RTRIM", - new[] { instance }, - nullable: true, - argumentsPropagateNullability: new[] { true }, - instance.Type, - instance.TypeMapping); + return ProcessTrimStartEnd(instance, arguments, "RTRIM"); } - if (TrimMethodInfoWithoutArgs.Equals(method) - || (TrimMethodInfoWithCharArrayArg.Equals(method) - // SqlServer LTRIM/RTRIM does not take arguments - && ((arguments[0] as SqlConstantExpression)?.Value as Array)?.Length == 0)) + if (method == TrimMethodInfoWithoutArgs + || (method == TrimMethodInfoWithCharArrayArg && arguments[0] is SqlConstantExpression { Value: char[] { Length: 0 } })) { return _sqlExpressionFactory.Function( "LTRIM", @@ -381,4 +381,26 @@ private SqlExpression TranslateIndexOf( return _sqlExpressionFactory.Subtract(charIndexExpression, offsetExpression); } + + private SqlExpression? ProcessTrimStartEnd(SqlExpression instance, IReadOnlyList arguments, string functionName) + { + SqlConstantExpression? charactersToTrim = null; + if (arguments.Count > 0 && arguments[0] is SqlConstantExpression { Value: var charactersToTrimValue }) + { + charactersToTrim = charactersToTrimValue switch + { + char singleChar => _sqlExpressionFactory.Constant(singleChar.ToString(), instance.TypeMapping), + char[] charArray => _sqlExpressionFactory.Constant(new string(charArray), instance.TypeMapping), + _ => throw new UnreachableException("Invalid parameter type for string.TrimStart/TrimEnd") + }; + } + + return _sqlExpressionFactory.Function( + functionName, + arguments: charactersToTrim is null ? [instance] : [instance, charactersToTrim], + nullable: true, + argumentsPropagateNullability: charactersToTrim is null ? [true] : [true, true], + instance.Type, + instance.TypeMapping); + } } diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindFunctionsQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindFunctionsQuerySqlServerTest.cs index 8511b5b02e5..76af10c51bf 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindFunctionsQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindFunctionsQuerySqlServerTest.cs @@ -2656,20 +2656,30 @@ WHERE LTRIM([c].[ContactTitle]) = N'Owner' """); } + [SqlServerCondition(SqlServerCondition.SupportsFunctions2022)] public override async Task TrimStart_with_char_argument_in_predicate(bool async) { - // String.Trim with parameters. Issue #22927. - await AssertTranslationFailed(() => base.TrimStart_with_char_argument_in_predicate(async)); + await base.TrimStart_with_char_argument_in_predicate(async); - AssertSql(); + 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 LTRIM([c].[ContactTitle], N'O') = N'wner' +"""); } + [SqlServerCondition(SqlServerCondition.SupportsFunctions2022)] public override async Task TrimStart_with_char_array_argument_in_predicate(bool async) { - // String.Trim with parameters. Issue #22927. - await AssertTranslationFailed(() => base.TrimStart_with_char_array_argument_in_predicate(async)); + await base.TrimStart_with_char_array_argument_in_predicate(async); - AssertSql(); + 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 LTRIM([c].[ContactTitle], N'Ow') = N'ner' +"""); } public override async Task TrimEnd_without_arguments_in_predicate(bool async) @@ -2684,20 +2694,30 @@ WHERE RTRIM([c].[ContactTitle]) = N'Owner' """); } + [SqlServerCondition(SqlServerCondition.SupportsFunctions2022)] public override async Task TrimEnd_with_char_argument_in_predicate(bool async) { - // String.Trim with parameters. Issue #22927. - await AssertTranslationFailed(() => base.TrimEnd_with_char_argument_in_predicate(async)); + await base.TrimEnd_with_char_argument_in_predicate(async); - AssertSql(); + 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 RTRIM([c].[ContactTitle], N'r') = N'Owne' +"""); } + [SqlServerCondition(SqlServerCondition.SupportsFunctions2022)] public override async Task TrimEnd_with_char_array_argument_in_predicate(bool async) { - // String.Trim with parameters. Issue #22927. - await AssertTranslationFailed(() => base.TrimEnd_with_char_array_argument_in_predicate(async)); + await base.TrimEnd_with_char_array_argument_in_predicate(async); - AssertSql(); + 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 RTRIM([c].[ContactTitle], N'er') = N'Own' +"""); } public override async Task Trim_without_argument_in_predicate(bool async)