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)