diff --git a/src/EFCore.Relational/Query/QuerySqlGenerator.cs b/src/EFCore.Relational/Query/QuerySqlGenerator.cs
index a2d0e963fa4..b5981f189a5 100644
--- a/src/EFCore.Relational/Query/QuerySqlGenerator.cs
+++ b/src/EFCore.Relational/Query/QuerySqlGenerator.cs
@@ -736,6 +736,7 @@ protected virtual void GenerateLike(LikeExpression likeExpression, bool negated)
}
_relationalCommandBuilder.Append(" LIKE ");
+
Visit(likeExpression.Pattern);
if (likeExpression.EscapeChar != null)
diff --git a/src/EFCore.Relational/Query/RelationalSqlTranslatingExpressionVisitor.cs b/src/EFCore.Relational/Query/RelationalSqlTranslatingExpressionVisitor.cs
index 88716980b18..46309019c68 100644
--- a/src/EFCore.Relational/Query/RelationalSqlTranslatingExpressionVisitor.cs
+++ b/src/EFCore.Relational/Query/RelationalSqlTranslatingExpressionVisitor.cs
@@ -1029,7 +1029,7 @@ protected override Expression VisitNewArray(NewArrayExpression newArrayExpressio
///
protected override Expression VisitParameter(ParameterExpression parameterExpression)
=> parameterExpression.Name?.StartsWith(QueryCompilationContext.QueryParameterPrefix, StringComparison.Ordinal) == true
- ? new SqlParameterExpression(parameterExpression, null)
+ ? new SqlParameterExpression(parameterExpression.Name, parameterExpression.Type, null)
: throw new InvalidOperationException(CoreStrings.TranslationFailed(parameterExpression.Print()));
///
diff --git a/src/EFCore.Relational/Query/SqlExpressions/SqlParameterExpression.cs b/src/EFCore.Relational/Query/SqlExpressions/SqlParameterExpression.cs
index 8540a8646ac..d53f50bbae6 100644
--- a/src/EFCore.Relational/Query/SqlExpressions/SqlParameterExpression.cs
+++ b/src/EFCore.Relational/Query/SqlExpressions/SqlParameterExpression.cs
@@ -6,32 +6,30 @@ namespace Microsoft.EntityFrameworkCore.Query.SqlExpressions;
///
/// An expression that represents a parameter in a SQL tree.
///
-///
-/// This is a simple wrapper around a in the SQL tree.
-/// Instances of this type cannot be constructed by application or database provider code. If this is a problem for your
-/// application or provider, then please file an issue at
-/// github.com/dotnet/efcore.
-///
public sealed class SqlParameterExpression : SqlExpression
{
- private readonly ParameterExpression _parameterExpression;
- private readonly string _name;
-
- internal SqlParameterExpression(ParameterExpression parameterExpression, RelationalTypeMapping? typeMapping)
- : base(parameterExpression.Type.UnwrapNullableType(), typeMapping)
+ ///
+ /// Creates a new instance of the class.
+ ///
+ /// The parameter name.
+ /// The of the expression.
+ /// The associated with the expression.
+ public SqlParameterExpression(string name, Type type, RelationalTypeMapping? typeMapping)
+ : this(name, type.UnwrapNullableType(), type.IsNullableType(), typeMapping)
{
- Check.DebugAssert(parameterExpression.Name != null, "Parameter must have name.");
+ }
- _parameterExpression = parameterExpression;
- _name = parameterExpression.Name;
- IsNullable = parameterExpression.Type.IsNullableType();
+ private SqlParameterExpression(string name, Type type, bool nullable, RelationalTypeMapping? typeMapping)
+ : base(type, typeMapping)
+ {
+ Name = name;
+ IsNullable = nullable;
}
///
/// The name of the parameter.
///
- public string Name
- => _name;
+ public string Name { get; }
///
/// The bool value indicating if this parameter can have null values.
@@ -44,7 +42,7 @@ public string Name
/// A relational type mapping to apply.
/// A new expression which has supplied type mapping.
public SqlExpression ApplyTypeMapping(RelationalTypeMapping? typeMapping)
- => new SqlParameterExpression(_parameterExpression, typeMapping);
+ => new SqlParameterExpression(Name, Type, IsNullable, typeMapping);
///
protected override Expression VisitChildren(ExpressionVisitor visitor)
@@ -52,7 +50,7 @@ protected override Expression VisitChildren(ExpressionVisitor visitor)
///
protected override void Print(ExpressionPrinter expressionPrinter)
- => expressionPrinter.Append("@" + _parameterExpression.Name);
+ => expressionPrinter.Append("@" + Name);
///
public override bool Equals(object? obj)
diff --git a/src/EFCore.Relational/Query/SqlNullabilityProcessor.cs b/src/EFCore.Relational/Query/SqlNullabilityProcessor.cs
index 90b037fbf93..72dc533b909 100644
--- a/src/EFCore.Relational/Query/SqlNullabilityProcessor.cs
+++ b/src/EFCore.Relational/Query/SqlNullabilityProcessor.cs
@@ -161,7 +161,7 @@ protected virtual TableExpressionBase Visit(TableExpressionBase tableExpressionB
var newTable = Visit(innerJoinExpression.Table);
var newJoinPredicate = ProcessJoinPredicate(innerJoinExpression.JoinPredicate);
- return TryGetBoolConstantValue(newJoinPredicate) == true
+ return IsTrue(newJoinPredicate)
? new CrossJoinExpression(newTable)
: innerJoinExpression.Update(newTable, newJoinPredicate);
}
@@ -301,7 +301,7 @@ protected virtual SelectExpression Visit(SelectExpression selectExpression)
var predicate = Visit(selectExpression.Predicate, allowOptimizedExpansion: true, out _);
changed |= predicate != selectExpression.Predicate;
- if (TryGetBoolConstantValue(predicate) == true)
+ if (IsTrue(predicate))
{
predicate = null;
changed = true;
@@ -333,7 +333,7 @@ protected virtual SelectExpression Visit(SelectExpression selectExpression)
var having = Visit(selectExpression.Having, allowOptimizedExpansion: true, out _);
changed |= having != selectExpression.Having;
- if (TryGetBoolConstantValue(having) == true)
+ if (IsTrue(having))
{
having = null;
changed = true;
@@ -519,20 +519,17 @@ protected virtual SqlExpression VisitCase(CaseExpression caseExpression, bool al
var test = Visit(
whenClause.Test, allowOptimizedExpansion: testIsCondition, preserveColumnNullabilityInformation: true, out _);
- if (TryGetBoolConstantValue(test) is bool testConstantBool)
+ if (IsTrue(test))
{
- if (testConstantBool)
- {
- testEvaluatesToTrue = true;
- }
- else
- {
- // if test evaluates to 'false' we can remove the WhenClause
- RestoreNonNullableColumnsList(currentNonNullableColumnsCount);
- RestoreNullValueColumnsList(currentNullValueColumnsCount);
+ testEvaluatesToTrue = true;
+ }
+ else if (IsFalse(test))
+ {
+ // if test evaluates to 'false' we can remove the WhenClause
+ RestoreNonNullableColumnsList(currentNonNullableColumnsCount);
+ RestoreNullValueColumnsList(currentNullValueColumnsCount);
- continue;
- }
+ continue;
}
var newResult = Visit(whenClause.Result, out var resultNullable);
@@ -570,7 +567,7 @@ protected virtual SqlExpression VisitCase(CaseExpression caseExpression, bool al
// if there is only one When clause and it's test evaluates to 'true' AND there is no else block, simply return the result
return elseResult == null
&& whenClauses.Count == 1
- && TryGetBoolConstantValue(whenClauses[0].Test) == true
+ && IsTrue(whenClauses[0].Test)
? whenClauses[0].Result
: caseExpression.Update(operand, whenClauses, elseResult);
}
@@ -635,7 +632,7 @@ protected virtual SqlExpression VisitExists(
// if subquery has predicate which evaluates to false, we can simply return false
// if the exists is negated we need to return true instead
- return TryGetBoolConstantValue(subquery.Predicate) == false
+ return IsFalse(subquery.Predicate)
? _sqlExpressionFactory.Constant(false, existsExpression.TypeMapping)
: existsExpression.Update(subquery);
}
@@ -658,7 +655,7 @@ protected virtual SqlExpression VisitIn(InExpression inExpression, bool allowOpt
var subquery = Visit(inExpression.Subquery);
// a IN (SELECT * FROM table WHERE false) => false
- if (TryGetBoolConstantValue(subquery.Predicate) == false)
+ if (IsFalse(subquery.Predicate))
{
nullable = false;
@@ -967,9 +964,64 @@ protected virtual SqlExpression VisitLike(LikeExpression likeExpression, bool al
var pattern = Visit(likeExpression.Pattern, out var patternNullable);
var escapeChar = Visit(likeExpression.EscapeChar, out var escapeCharNullable);
- nullable = matchNullable || patternNullable || escapeCharNullable;
+ SqlExpression result = likeExpression.Update(match, pattern, escapeChar);
+
+ if (UseRelationalNulls)
+ {
+ nullable = matchNullable || patternNullable || escapeCharNullable;
+
+ return result;
+ }
+
+ nullable = false;
+
+ // The null semantics behavior we implement for LIKE is that it only returns true when both sides are non-null and match; any other
+ // input returns false:
+ // foo LIKE f% -> true
+ // foo LIKE null -> false
+ // null LIKE f% -> false
+ // null LIKE null -> false
+
+ if (IsNull(match) || IsNull(pattern) || IsNull(escapeChar))
+ {
+ return _sqlExpressionFactory.Constant(false, likeExpression.TypeMapping);
+ }
+
+ // A constant match-all pattern (%) returns true for all cases, except where the match string is null:
+ // nullable_foo LIKE % -> foo IS NOT NULL
+ // non_nullable_foo LIKE % -> true
+ if (pattern is SqlConstantExpression { Value: "%" })
+ {
+ return matchNullable
+ ? _sqlExpressionFactory.IsNotNull(match)
+ : _sqlExpressionFactory.Constant(true, likeExpression.TypeMapping);
+ }
- return likeExpression.Update(match, pattern, escapeChar);
+ if (!allowOptimizedExpansion)
+ {
+ if (matchNullable)
+ {
+ result = _sqlExpressionFactory.AndAlso(result, GenerateNotNullCheck(match));
+ }
+
+ if (patternNullable)
+ {
+ result = _sqlExpressionFactory.AndAlso(result, GenerateNotNullCheck(pattern));
+ }
+
+ if (escapeChar is not null && escapeCharNullable)
+ {
+ result = _sqlExpressionFactory.AndAlso(result, GenerateNotNullCheck(escapeChar));
+ }
+ }
+
+ return result;
+
+ SqlExpression GenerateNotNullCheck(SqlExpression operand)
+ => OptimizeNonNullableNotExpression(
+ _sqlExpressionFactory.Not(
+ ProcessNullNotNull(
+ _sqlExpressionFactory.IsNull(operand), operandNullable: true)));
}
///
@@ -1395,8 +1447,28 @@ protected virtual SqlExpression VisitJsonScalar(
///
protected virtual bool PreferExistsToComplexIn => false;
- private static bool? TryGetBoolConstantValue(SqlExpression? expression)
- => expression is SqlConstantExpression { Value: bool boolValue } ? boolValue : null;
+ // Note that we can check parameter values for null since we cache by the parameter nullability; but we cannot do the same for bool.
+ private bool IsNull(SqlExpression? expression)
+ => expression is SqlConstantExpression { Value: null }
+ || expression is SqlParameterExpression { Name: string parameterName } && ParameterValues[parameterName] is null;
+
+ private bool IsTrue(SqlExpression? expression)
+ => expression is SqlConstantExpression { Value: true };
+
+ private bool IsFalse(SqlExpression? expression)
+ => expression is SqlConstantExpression { Value: false };
+
+ private bool TryGetBool(SqlExpression? expression, out bool value)
+ {
+ if (expression is SqlConstantExpression { Value: bool b })
+ {
+ value = b;
+ return true;
+ }
+
+ value = false;
+ return false;
+ }
private void RestoreNonNullableColumnsList(int counter)
{
@@ -1486,7 +1558,7 @@ private SqlExpression OptimizeComparison(
return result;
}
- if (TryGetBoolConstantValue(right) is bool rightBoolValue
+ if (TryGetBool(right, out var rightBoolValue)
&& !leftNullable
&& left.TypeMapping!.Converter == null)
{
@@ -1502,7 +1574,7 @@ private SqlExpression OptimizeComparison(
: left;
}
- if (TryGetBoolConstantValue(left) is bool leftBoolValue
+ if (TryGetBool(left, out var leftBoolValue)
&& !rightNullable
&& right.TypeMapping!.Converter == null)
{
@@ -2069,10 +2141,6 @@ private SqlExpression ProcessNullNotNull(SqlUnaryExpression sqlUnaryExpression,
private static bool IsLogicalNot(SqlUnaryExpression? sqlUnaryExpression)
=> sqlUnaryExpression is { OperatorType: ExpressionType.Not } && sqlUnaryExpression.Type == typeof(bool);
- private bool IsNull(SqlExpression expression)
- => expression is SqlConstantExpression { Value: null }
- || expression is SqlParameterExpression { Name: string parameterName } && ParameterValues[parameterName] is null;
-
// ?a == ?b -> [(a == b) && (a != null && b != null)] || (a == null && b == null))
//
// a | b | F1 = a == b | F2 = (a != null && b != null) | F3 = F1 && F2 |
diff --git a/src/EFCore.SqlServer/Query/Internal/SqlServerSqlTranslatingExpressionVisitor.cs b/src/EFCore.SqlServer/Query/Internal/SqlServerSqlTranslatingExpressionVisitor.cs
index e3b99f42894..3df235d1959 100644
--- a/src/EFCore.SqlServer/Query/Internal/SqlServerSqlTranslatingExpressionVisitor.cs
+++ b/src/EFCore.SqlServer/Query/Internal/SqlServerSqlTranslatingExpressionVisitor.cs
@@ -1,7 +1,10 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
+using System.Diagnostics.CodeAnalysis;
+using System.Text;
using Microsoft.EntityFrameworkCore.Query.SqlExpressions;
+using ExpressionExtensions = Microsoft.EntityFrameworkCore.Query.ExpressionExtensions;
namespace Microsoft.EntityFrameworkCore.SqlServer.Query.Internal;
@@ -13,6 +16,9 @@ namespace Microsoft.EntityFrameworkCore.SqlServer.Query.Internal;
///
public class SqlServerSqlTranslatingExpressionVisitor : RelationalSqlTranslatingExpressionVisitor
{
+ private readonly QueryCompilationContext _queryCompilationContext;
+ private readonly ISqlExpressionFactory _sqlExpressionFactory;
+
private static readonly HashSet DateTimeDataTypes
= new()
{
@@ -43,6 +49,21 @@ private static readonly HashSet ArithmeticOperatorTypes
ExpressionType.Modulo
};
+ private static readonly MethodInfo StringStartsWithMethodInfo
+ = typeof(string).GetRuntimeMethod(nameof(string.StartsWith), new[] { typeof(string) })!;
+
+ private static readonly MethodInfo StringEndsWithMethodInfo
+ = typeof(string).GetRuntimeMethod(nameof(string.EndsWith), new[] { typeof(string) })!;
+
+ private static readonly MethodInfo StringContainsMethodInfo
+ = typeof(string).GetRuntimeMethod(nameof(string.Contains), new[] { typeof(string) })!;
+
+ private static readonly MethodInfo EscapeLikePatternParameterMethod =
+ typeof(SqlServerSqlTranslatingExpressionVisitor).GetTypeInfo().GetDeclaredMethod(nameof(ConstructLikePatternParameter))!;
+
+ private const char LikeEscapeChar = '\\';
+ private const string LikeEscapeString = "\\";
+
///
/// 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
@@ -55,6 +76,8 @@ public SqlServerSqlTranslatingExpressionVisitor(
QueryableMethodTranslatingExpressionVisitor queryableMethodTranslatingExpressionVisitor)
: base(dependencies, queryCompilationContext, queryableMethodTranslatingExpressionVisitor)
{
+ _queryCompilationContext = queryCompilationContext;
+ _sqlExpressionFactory = dependencies.SqlExpressionFactory;
}
///
@@ -142,17 +165,264 @@ protected override Expression VisitUnary(UnaryExpression unaryExpression)
///
protected override Expression VisitMethodCall(MethodCallExpression methodCallExpression)
{
- if (methodCallExpression is { Method: { IsGenericMethod: true } } genericMethodCall
- && genericMethodCall.Method.GetGenericMethodDefinition() == EnumerableMethods.ElementAt
- && genericMethodCall.Arguments[0].Type == typeof(byte[]))
+ var method = methodCallExpression.Method;
+
+ if (method.IsGenericMethod
+ && method.GetGenericMethodDefinition() == EnumerableMethods.ElementAt
+ && methodCallExpression.Arguments[0].Type == typeof(byte[]))
{
return TranslateByteArrayElementAccess(
- genericMethodCall.Arguments[0],
- genericMethodCall.Arguments[1],
+ methodCallExpression.Arguments[0],
+ methodCallExpression.Arguments[1],
methodCallExpression.Type);
}
+ if (method == StringStartsWithMethodInfo
+ && TryTranslateStartsEndsWithContains(
+ methodCallExpression.Object!, methodCallExpression.Arguments[0], StartsEndsWithContains.StartsWith, out var translation1))
+ {
+ return translation1;
+ }
+
+ if (method == StringEndsWithMethodInfo
+ && TryTranslateStartsEndsWithContains(
+ methodCallExpression.Object!, methodCallExpression.Arguments[0], StartsEndsWithContains.EndsWith, out var translation2))
+ {
+ return translation2;
+ }
+
+ if (method == StringContainsMethodInfo
+ && TryTranslateStartsEndsWithContains(
+ methodCallExpression.Object!, methodCallExpression.Arguments[0], StartsEndsWithContains.Contains, out var translation3))
+ {
+ return translation3;
+ }
+
return base.VisitMethodCall(methodCallExpression);
+
+ bool TryTranslateStartsEndsWithContains(
+ Expression instance, Expression pattern, StartsEndsWithContains methodType, [NotNullWhen(true)] out SqlExpression? translation)
+ {
+ if (Visit(instance) is not SqlExpression translatedInstance
+ || Visit(pattern) is not SqlExpression translatedPattern)
+ {
+ translation = null;
+ return false;
+ }
+
+ var stringTypeMapping = ExpressionExtensions.InferTypeMapping(translatedInstance, translatedPattern);
+
+ translatedInstance = _sqlExpressionFactory.ApplyTypeMapping(translatedInstance, stringTypeMapping);
+ translatedPattern = _sqlExpressionFactory.ApplyTypeMapping(translatedPattern, stringTypeMapping);
+
+ switch (translatedPattern)
+ {
+ case SqlConstantExpression patternConstant:
+ {
+ // The pattern is constant. Aside from null and empty string, we escape all special characters (%, _, \) and send a
+ // simple LIKE
+ translation = patternConstant.Value switch
+ {
+ null => _sqlExpressionFactory.Like(translatedInstance, _sqlExpressionFactory.Constant(null, stringTypeMapping)),
+
+ // In .NET, all strings start with/end with/contain the empty string, but SQL LIKE return false for empty patterns.
+ // Return % which always matches instead.
+ // Note that we don't just return a true constant, since null strings shouldn't match even an empty string
+ // (but SqlNullabilityProcess will convert this to a true constant if the instance is non-nullable)
+ "" => _sqlExpressionFactory.Like(translatedInstance, _sqlExpressionFactory.Constant("%")),
+
+ string s => s.Any(IsLikeWildChar)
+ ? _sqlExpressionFactory.Like(
+ translatedInstance,
+ _sqlExpressionFactory.Constant(
+ methodType switch
+ {
+ StartsEndsWithContains.StartsWith => EscapeLikePattern(s) + '%',
+ StartsEndsWithContains.EndsWith => '%' + EscapeLikePattern(s),
+ StartsEndsWithContains.Contains => $"%{EscapeLikePattern(s)}%",
+
+ _ => throw new ArgumentOutOfRangeException(nameof(methodType), methodType, null)
+ }),
+ _sqlExpressionFactory.Constant(LikeEscapeString))
+ : _sqlExpressionFactory.Like(
+ translatedInstance,
+ _sqlExpressionFactory.Constant(
+ methodType switch
+ {
+ StartsEndsWithContains.StartsWith => s + '%',
+ StartsEndsWithContains.EndsWith => '%' + s,
+ StartsEndsWithContains.Contains => $"%{s}%",
+
+ _ => throw new ArgumentOutOfRangeException(nameof(methodType), methodType, null)
+ })),
+
+ _ => throw new UnreachableException()
+ };
+
+ return true;
+ }
+
+ case SqlParameterExpression patternParameter
+ when patternParameter.Name.StartsWith(QueryCompilationContext.QueryParameterPrefix, StringComparison.Ordinal):
+ {
+ // The pattern is a parameter, register a runtime parameter that will contain the rewritten LIKE pattern, where
+ // all special characters have been escaped.
+ var lambda = Expression.Lambda(
+ Expression.Call(
+ EscapeLikePatternParameterMethod,
+ QueryCompilationContext.QueryContextParameter,
+ Expression.Constant(patternParameter.Name),
+ Expression.Constant(methodType)),
+ QueryCompilationContext.QueryContextParameter);
+
+ var escapedPatternParameter =
+ _queryCompilationContext.RegisterRuntimeParameter(patternParameter.Name + "_rewritten", lambda);
+
+ translation = _sqlExpressionFactory.Like(
+ translatedInstance,
+ new SqlParameterExpression(escapedPatternParameter.Name!, escapedPatternParameter.Type, stringTypeMapping),
+ _sqlExpressionFactory.Constant(LikeEscapeString));
+
+ return true;
+ }
+
+ default:
+ // The pattern is a column or a complex expression; the possible special characters in the pattern cannot be escaped,
+ // preventing us from translating to LIKE.
+ translation = methodType switch
+ {
+ // For StartsWith/EndsWith, use LEFT or RIGHT instead to extract substring and compare:
+ // WHERE instance IS NOT NULL AND pattern IS NOT NULL AND LEFT(instance, LEN(pattern)) = pattern
+ // This is less efficient than LIKE (i.e. StartsWith does an index scan instead of seek), but we have no choice.
+ // Note that we compensate for the case where both the instance and the pattern are null (null.StartsWith(null)); a
+ // simple equality would yield true in that case, but we want false. We technically
+ StartsEndsWithContains.StartsWith or StartsEndsWithContains.EndsWith
+ => _sqlExpressionFactory.AndAlso(
+ _sqlExpressionFactory.IsNotNull(translatedInstance),
+ _sqlExpressionFactory.AndAlso(
+ _sqlExpressionFactory.IsNotNull(translatedPattern),
+ _sqlExpressionFactory.Equal(
+ _sqlExpressionFactory.Function(
+ methodType is StartsEndsWithContains.StartsWith ? "LEFT" : "RIGHT",
+ new[]
+ {
+ translatedInstance,
+ _sqlExpressionFactory.Function(
+ "LEN",
+ new[] { translatedPattern },
+ nullable: true,
+ argumentsPropagateNullability: new[] { true },
+ typeof(int))
+ },
+ nullable: true,
+ argumentsPropagateNullability: new[] { true, true },
+ typeof(string),
+ stringTypeMapping),
+ translatedPattern))),
+
+ // For Contains, just use CHARINDEX and check if the result is greater than 0.
+ // Add a check to return null when the pattern is an empty string (and the string isn't null)
+ StartsEndsWithContains.Contains
+ => _sqlExpressionFactory.AndAlso(
+ _sqlExpressionFactory.IsNotNull(translatedInstance),
+ _sqlExpressionFactory.AndAlso(
+ _sqlExpressionFactory.IsNotNull(translatedPattern),
+ _sqlExpressionFactory.OrElse(
+ _sqlExpressionFactory.GreaterThan(
+ _sqlExpressionFactory.Function(
+ "CHARINDEX",
+ new[] { translatedPattern, translatedInstance },
+ nullable: true,
+ argumentsPropagateNullability: new[] { true, true },
+ typeof(int)),
+ _sqlExpressionFactory.Constant(0)),
+ _sqlExpressionFactory.Like(
+ translatedPattern,
+ _sqlExpressionFactory.Constant(string.Empty, stringTypeMapping))))),
+
+ _ => throw new UnreachableException()
+ };
+
+ return true;
+ }
+ }
+ }
+
+ private static string? ConstructLikePatternParameter(
+ QueryContext queryContext, string baseParameterName, StartsEndsWithContains methodType)
+ => queryContext.ParameterValues[baseParameterName] switch
+ {
+ null => null,
+
+ // In .NET, all strings start/end with the empty string, but SQL LIKE return false for empty patterns.
+ // Return % which always matches instead.
+ "" => "%",
+
+ string s => methodType switch
+ {
+ StartsEndsWithContains.StartsWith => EscapeLikePattern(s) + '%',
+ StartsEndsWithContains.EndsWith => '%' + EscapeLikePattern(s),
+ StartsEndsWithContains.Contains => $"%{EscapeLikePattern(s)}%",
+ _ => throw new ArgumentOutOfRangeException(nameof(methodType), methodType, null)
+ },
+
+ _ => throw new UnreachableException()
+ };
+
+ private enum StartsEndsWithContains
+ {
+ StartsWith,
+ EndsWith,
+ Contains
+ }
+
+ ///
+ /// 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.
+ ///
+ private static bool IsLikeWildChar(char c)
+ => c is '%' or '_' or '['; // See https://docs.microsoft.com/en-us/sql/t-sql/language-elements/like-transact-sql
+
+ ///
+ /// 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.
+ ///
+ private static string EscapeLikePattern(string pattern)
+ {
+ int i;
+ for (i = 0; i < pattern.Length; i++)
+ {
+ var c = pattern[i];
+ if (IsLikeWildChar(c) || c == LikeEscapeChar)
+ {
+ break;
+ }
+ }
+
+ if (i == pattern.Length) // No special characters were detected, just return the original pattern string
+ {
+ return pattern;
+ }
+
+ var builder = new StringBuilder(pattern, 0, i, pattern.Length + 10);
+
+ for (; i < pattern.Length; i++)
+ {
+ var c = pattern[i];
+ if (IsLikeWildChar(c)
+ || c == LikeEscapeChar)
+ {
+ builder.Append(LikeEscapeChar);
+ }
+
+ builder.Append(c);
+ }
+
+ return builder.ToString();
}
private Expression TranslateByteArrayElementAccess(Expression array, Expression index, Type resultType)
diff --git a/src/EFCore.SqlServer/Query/Internal/SqlServerStringMethodTranslator.cs b/src/EFCore.SqlServer/Query/Internal/SqlServerStringMethodTranslator.cs
index 306648dec52..317bccf1604 100644
--- a/src/EFCore.SqlServer/Query/Internal/SqlServerStringMethodTranslator.cs
+++ b/src/EFCore.SqlServer/Query/Internal/SqlServerStringMethodTranslator.cs
@@ -62,15 +62,6 @@ private static readonly MethodInfo TrimEndMethodInfoWithCharArrayArg
private static readonly MethodInfo TrimMethodInfoWithCharArrayArg
= typeof(string).GetRuntimeMethod(nameof(string.Trim), new[] { typeof(char[]) })!;
- private static readonly MethodInfo StartsWithMethodInfo
- = typeof(string).GetRuntimeMethod(nameof(string.StartsWith), new[] { typeof(string) })!;
-
- private static readonly MethodInfo ContainsMethodInfo
- = typeof(string).GetRuntimeMethod(nameof(string.Contains), new[] { typeof(string) })!;
-
- private static readonly MethodInfo EndsWithMethodInfo
- = typeof(string).GetRuntimeMethod(nameof(string.EndsWith), new[] { typeof(string) })!;
-
private static readonly MethodInfo FirstOrDefaultMethodInfoWithoutArgs
= typeof(Enumerable).GetRuntimeMethods().Single(
m => m.Name == nameof(Enumerable.FirstOrDefault)
@@ -83,9 +74,6 @@ private static readonly MethodInfo LastOrDefaultMethodInfoWithoutArgs
private readonly ISqlExpressionFactory _sqlExpressionFactory;
- private const char LikeEscapeChar = '\\';
- private const string LikeEscapeString = "\\";
-
///
/// 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
@@ -243,59 +231,6 @@ public SqlServerStringMethodTranslator(ISqlExpressionFactory sqlExpressionFactor
instance.Type,
instance.TypeMapping);
}
-
- if (ContainsMethodInfo.Equals(method))
- {
- var pattern = arguments[0];
- var stringTypeMapping = ExpressionExtensions.InferTypeMapping(instance, pattern);
- instance = _sqlExpressionFactory.ApplyTypeMapping(instance, stringTypeMapping);
- pattern = _sqlExpressionFactory.ApplyTypeMapping(pattern, stringTypeMapping);
-
- if (pattern is SqlConstantExpression constantPattern)
- {
- if (!(constantPattern.Value is string patternValue))
- {
- return _sqlExpressionFactory.Like(
- instance,
- _sqlExpressionFactory.Constant(null, stringTypeMapping));
- }
-
- if (patternValue.Length == 0)
- {
- return _sqlExpressionFactory.Constant(true);
- }
-
- return patternValue.Any(IsLikeWildChar)
- ? _sqlExpressionFactory.Like(
- instance,
- _sqlExpressionFactory.Constant($"%{EscapeLikePattern(patternValue)}%"),
- _sqlExpressionFactory.Constant(LikeEscapeString))
- : _sqlExpressionFactory.Like(instance, _sqlExpressionFactory.Constant($"%{patternValue}%"));
- }
-
- return _sqlExpressionFactory.OrElse(
- _sqlExpressionFactory.Like(
- pattern,
- _sqlExpressionFactory.Constant(string.Empty, stringTypeMapping)),
- _sqlExpressionFactory.GreaterThan(
- _sqlExpressionFactory.Function(
- "CHARINDEX",
- new[] { pattern, instance },
- nullable: true,
- argumentsPropagateNullability: new[] { true, true },
- typeof(int)),
- _sqlExpressionFactory.Constant(0)));
- }
-
- if (StartsWithMethodInfo.Equals(method))
- {
- return TranslateStartsEndsWith(instance, arguments[0], true);
- }
-
- if (EndsWithMethodInfo.Equals(method))
- {
- return TranslateStartsEndsWith(instance, arguments[0], false);
- }
}
if (IsNullOrEmptyMethodInfo.Equals(method))
@@ -355,80 +290,6 @@ public SqlServerStringMethodTranslator(ISqlExpressionFactory sqlExpressionFactor
return null;
}
- private SqlExpression TranslateStartsEndsWith(SqlExpression instance, SqlExpression pattern, bool startsWith)
- {
- var stringTypeMapping = ExpressionExtensions.InferTypeMapping(instance, pattern);
-
- instance = _sqlExpressionFactory.ApplyTypeMapping(instance, stringTypeMapping);
- pattern = _sqlExpressionFactory.ApplyTypeMapping(pattern, stringTypeMapping);
-
- if (pattern is SqlConstantExpression constantExpression)
- {
- // The pattern is constant. Aside from null or empty, we escape all special characters (%, _, \)
- // in C# and send a simple LIKE
- if (!(constantExpression.Value is string patternValue))
- {
- return _sqlExpressionFactory.Like(
- instance,
- _sqlExpressionFactory.Constant(null, stringTypeMapping));
- }
-
- return patternValue.Any(IsLikeWildChar)
- ? _sqlExpressionFactory.Like(
- instance,
- _sqlExpressionFactory.Constant(
- startsWith
- ? EscapeLikePattern(patternValue) + '%'
- : '%' + EscapeLikePattern(patternValue)),
- _sqlExpressionFactory.Constant(LikeEscapeString))
- : _sqlExpressionFactory.Like(
- instance,
- _sqlExpressionFactory.Constant(startsWith ? patternValue + '%' : '%' + patternValue));
- }
-
- // The pattern is non-constant, we use LEFT or RIGHT to extract substring and compare.
- if (startsWith)
- {
- return _sqlExpressionFactory.Equal(
- _sqlExpressionFactory.Function(
- "LEFT",
- new[]
- {
- instance,
- _sqlExpressionFactory.Function(
- "LEN",
- new[] { pattern },
- nullable: true,
- argumentsPropagateNullability: new[] { true },
- typeof(int))
- },
- nullable: true,
- argumentsPropagateNullability: new[] { true, true },
- typeof(string),
- stringTypeMapping),
- pattern);
- }
-
- return _sqlExpressionFactory.Equal(
- _sqlExpressionFactory.Function(
- "RIGHT",
- new[]
- {
- instance,
- _sqlExpressionFactory.Function(
- "LEN",
- new[] { pattern },
- nullable: true,
- argumentsPropagateNullability: new[] { true },
- typeof(int))
- },
- nullable: true,
- argumentsPropagateNullability: new[] { true, true },
- typeof(string),
- stringTypeMapping),
- pattern);
- }
-
private SqlExpression TranslateIndexOf(
SqlExpression instance,
MethodInfo method,
@@ -497,26 +358,4 @@ private SqlExpression TranslateIndexOf(
},
charIndexExpression);
}
-
- // See https://docs.microsoft.com/en-us/sql/t-sql/language-elements/like-transact-sql
- private static bool IsLikeWildChar(char c)
- => c is '%' or '_' or '[';
-
- private static string EscapeLikePattern(string pattern)
- {
- var builder = new StringBuilder();
- for (var i = 0; i < pattern.Length; i++)
- {
- var c = pattern[i];
- if (IsLikeWildChar(c)
- || c == LikeEscapeChar)
- {
- builder.Append(LikeEscapeChar);
- }
-
- builder.Append(c);
- }
-
- return builder.ToString();
- }
}
diff --git a/src/EFCore.Sqlite.Core/Query/Internal/SqliteSqlTranslatingExpressionVisitor.cs b/src/EFCore.Sqlite.Core/Query/Internal/SqliteSqlTranslatingExpressionVisitor.cs
index 813912eec71..29f1c421110 100644
--- a/src/EFCore.Sqlite.Core/Query/Internal/SqliteSqlTranslatingExpressionVisitor.cs
+++ b/src/EFCore.Sqlite.Core/Query/Internal/SqliteSqlTranslatingExpressionVisitor.cs
@@ -2,7 +2,9 @@
// The .NET Foundation licenses this file to you under the MIT license.
using System.Diagnostics.CodeAnalysis;
+using System.Text;
using Microsoft.EntityFrameworkCore.Query.SqlExpressions;
+using ExpressionExtensions = Microsoft.EntityFrameworkCore.Query.ExpressionExtensions;
namespace Microsoft.EntityFrameworkCore.Sqlite.Query.Internal;
@@ -14,6 +16,21 @@ namespace Microsoft.EntityFrameworkCore.Sqlite.Query.Internal;
///
public class SqliteSqlTranslatingExpressionVisitor : RelationalSqlTranslatingExpressionVisitor
{
+ private readonly QueryCompilationContext _queryCompilationContext;
+ private readonly ISqlExpressionFactory _sqlExpressionFactory;
+
+ private static readonly MethodInfo StringStartsWithMethodInfo
+ = typeof(string).GetRuntimeMethod(nameof(string.StartsWith), new[] { typeof(string) })!;
+
+ private static readonly MethodInfo StringEndsWithMethodInfo
+ = typeof(string).GetRuntimeMethod(nameof(string.EndsWith), new[] { typeof(string) })!;
+
+ private static readonly MethodInfo EscapeLikePatternParameterMethod =
+ typeof(SqliteSqlTranslatingExpressionVisitor).GetTypeInfo().GetDeclaredMethod(nameof(ConstructLikePatternParameter))!;
+
+ private const char LikeEscapeChar = '\\';
+ private const string LikeEscapeString = "\\";
+
private static readonly IReadOnlyDictionary> RestrictedBinaryExpressions
= new Dictionary>
{
@@ -91,6 +108,8 @@ public SqliteSqlTranslatingExpressionVisitor(
QueryableMethodTranslatingExpressionVisitor queryableMethodTranslatingExpressionVisitor)
: base(dependencies, queryCompilationContext, queryableMethodTranslatingExpressionVisitor)
{
+ _queryCompilationContext = queryCompilationContext;
+ _sqlExpressionFactory = dependencies.SqlExpressionFactory;
}
///
@@ -226,6 +245,213 @@ protected override Expression VisitBinary(BinaryExpression binaryExpression)
return visitedExpression;
}
+ ///
+ protected override Expression VisitMethodCall(MethodCallExpression methodCallExpression)
+ {
+ var method = methodCallExpression.Method;
+
+ if (method == StringStartsWithMethodInfo
+ && TryTranslateStartsEndsWith(
+ methodCallExpression.Object!, methodCallExpression.Arguments[0], startsWith: true, out var translation1))
+ {
+ return translation1;
+ }
+
+ if (method == StringEndsWithMethodInfo
+ && TryTranslateStartsEndsWith(
+ methodCallExpression.Object!, methodCallExpression.Arguments[0], startsWith: false, out var translation2))
+ {
+ return translation2;
+ }
+
+ return base.VisitMethodCall(methodCallExpression);
+
+ bool TryTranslateStartsEndsWith(
+ Expression instance,
+ Expression pattern,
+ bool startsWith,
+ [NotNullWhen(true)] out SqlExpression? translation)
+ {
+ if (Visit(instance) is not SqlExpression translatedInstance
+ || Visit(pattern) is not SqlExpression translatedPattern)
+ {
+ translation = null;
+ return false;
+ }
+
+ var stringTypeMapping = ExpressionExtensions.InferTypeMapping(translatedInstance, translatedPattern);
+
+ translatedInstance = _sqlExpressionFactory.ApplyTypeMapping(translatedInstance, stringTypeMapping);
+ translatedPattern = _sqlExpressionFactory.ApplyTypeMapping(translatedPattern, stringTypeMapping);
+
+ switch (translatedPattern)
+ {
+ case SqlConstantExpression patternConstant:
+ {
+ // The pattern is constant. Aside from null and empty string, we escape all special characters (%, _, \) and send a
+ // simple LIKE
+ translation = patternConstant.Value switch
+ {
+ null => _sqlExpressionFactory.Like(translatedInstance, _sqlExpressionFactory.Constant(null, stringTypeMapping)),
+
+ // In .NET, all strings start with/end with/contain the empty string, but SQL LIKE return false for empty patterns.
+ // Return % which always matches instead.
+ // Note that we don't just return a true constant, since null strings shouldn't match even an empty string
+ // (but SqlNullabilityProcess will convert this to a true constant if the instance is non-nullable)
+ "" => _sqlExpressionFactory.Like(translatedInstance, _sqlExpressionFactory.Constant("%")),
+
+ string s => s.Any(IsLikeWildChar)
+ ? _sqlExpressionFactory.Like(
+ translatedInstance,
+ _sqlExpressionFactory.Constant(startsWith ? EscapeLikePattern(s) + '%' : '%' + EscapeLikePattern(s)),
+ _sqlExpressionFactory.Constant(LikeEscapeString))
+ : _sqlExpressionFactory.Like(
+ translatedInstance,
+ _sqlExpressionFactory.Constant(startsWith ? s + '%' : '%' + s)),
+
+ _ => throw new UnreachableException()
+ };
+
+ return true;
+ }
+
+ case SqlParameterExpression patternParameter
+ when patternParameter.Name.StartsWith(QueryCompilationContext.QueryParameterPrefix, StringComparison.Ordinal):
+ {
+ // The pattern is a parameter, register a runtime parameter that will contain the rewritten LIKE pattern, where
+ // all special characters have been escaped.
+ var lambda = Expression.Lambda(
+ Expression.Call(
+ EscapeLikePatternParameterMethod,
+ QueryCompilationContext.QueryContextParameter,
+ Expression.Constant(patternParameter.Name),
+ Expression.Constant(startsWith)),
+ QueryCompilationContext.QueryContextParameter);
+
+ var escapedPatternParameter =
+ _queryCompilationContext.RegisterRuntimeParameter(patternParameter.Name + "_rewritten", lambda);
+
+ translation = _sqlExpressionFactory.Like(
+ translatedInstance,
+ new SqlParameterExpression(escapedPatternParameter.Name!, escapedPatternParameter.Type, stringTypeMapping),
+ _sqlExpressionFactory.Constant(LikeEscapeString));
+
+ return true;
+ }
+
+ default:
+ // The pattern is a column or a complex expression; the possible special characters in the pattern cannot be escaped,
+ // preventing us from translating to LIKE.
+ if (startsWith)
+ {
+ // Generate: WHERE instance IS NOT NULL AND pattern IS NOT NULL AND (substr(instance, 1, length(pattern)) = pattern OR pattern = '')
+ // Note that the empty string pattern needs special handling, since in .NET it returns true for all non-null
+ // instances, but substr(instance, 0) returns the entire string in SQLite.
+ // Note that we compensate for the case where both the instance and the pattern are null (null.StartsWith(null)); a
+ // simple equality would yield true in that case, but we want false. We technically
+ translation = _sqlExpressionFactory.AndAlso(
+ _sqlExpressionFactory.IsNotNull(translatedInstance),
+ _sqlExpressionFactory.AndAlso(
+ _sqlExpressionFactory.IsNotNull(translatedPattern),
+ _sqlExpressionFactory.OrElse(
+ _sqlExpressionFactory.Equal(
+ _sqlExpressionFactory.Function(
+ "substr",
+ new[]
+ {
+ translatedInstance,
+ _sqlExpressionFactory.Constant(1),
+ _sqlExpressionFactory.Function(
+ "length",
+ new[] { translatedPattern },
+ nullable: true,
+ argumentsPropagateNullability: new[] { true },
+ typeof(int))
+ },
+ nullable: true,
+ argumentsPropagateNullability: new[] { true, false, true },
+ typeof(string),
+ stringTypeMapping),
+ translatedPattern),
+ _sqlExpressionFactory.Equal(translatedPattern, _sqlExpressionFactory.Constant(string.Empty)))));
+ }
+ else
+ {
+ // Generate: WHERE instance IS NOT NULL AND pattern IS NOT NULL AND (substr(instance, -length(pattern)) = pattern OR pattern = '')
+ // Note that the empty string pattern needs special handling, since in .NET it returns true for all non-null
+ // instances, but substr(instance, 0) returns the entire string in SQLite.
+ // Note that we compensate for the case where both the instance and the pattern are null (null.StartsWith(null)); a
+ // simple equality would yield true in that case, but we want false. We technically
+ translation =
+ _sqlExpressionFactory.AndAlso(
+ _sqlExpressionFactory.IsNotNull(translatedInstance),
+ _sqlExpressionFactory.AndAlso(
+ _sqlExpressionFactory.IsNotNull(translatedPattern),
+ _sqlExpressionFactory.OrElse(
+ _sqlExpressionFactory.Equal(
+ _sqlExpressionFactory.Function(
+ "substr",
+ new[]
+ {
+ translatedInstance,
+ _sqlExpressionFactory.Negate(
+ _sqlExpressionFactory.Function(
+ "length",
+ new[] { translatedPattern },
+ nullable: true,
+ argumentsPropagateNullability: new[] { true },
+ typeof(int)))
+ },
+ nullable: true,
+ argumentsPropagateNullability: new[] { true, true },
+ typeof(string),
+ stringTypeMapping),
+ translatedPattern),
+ _sqlExpressionFactory.Equal(translatedPattern, _sqlExpressionFactory.Constant(string.Empty)))));
+ }
+
+ return true;
+ }
+ }
+ }
+
+ private static string? ConstructLikePatternParameter(
+ QueryContext queryContext, string baseParameterName, bool startsWith)
+ => queryContext.ParameterValues[baseParameterName] switch
+ {
+ null => null,
+
+ // In .NET, all strings start/end with the empty string, but SQL LIKE return false for empty patterns.
+ // Return % which always matches instead.
+ "" => "%",
+
+ string s => startsWith ? EscapeLikePattern(s) + '%' : '%' + EscapeLikePattern(s),
+
+ _ => throw new UnreachableException()
+ };
+
+ // See https://www.sqlite.org/lang_expr.html
+ private static bool IsLikeWildChar(char c)
+ => c is '%' or '_';
+
+ private static string EscapeLikePattern(string pattern)
+ {
+ var builder = new StringBuilder();
+ for (var i = 0; i < pattern.Length; i++)
+ {
+ var c = pattern[i];
+ if (IsLikeWildChar(c)
+ || c == LikeEscapeChar)
+ {
+ builder.Append(LikeEscapeChar);
+ }
+
+ builder.Append(c);
+ }
+
+ return builder.ToString();
+ }
+
[return: NotNullIfNotNull(nameof(expression))]
private static Type? GetProviderType(SqlExpression? expression)
=> expression == null
diff --git a/src/EFCore.Sqlite.Core/Query/Internal/SqliteStringMethodTranslator.cs b/src/EFCore.Sqlite.Core/Query/Internal/SqliteStringMethodTranslator.cs
index 840cabb2d30..70819ab0583 100644
--- a/src/EFCore.Sqlite.Core/Query/Internal/SqliteStringMethodTranslator.cs
+++ b/src/EFCore.Sqlite.Core/Query/Internal/SqliteStringMethodTranslator.cs
@@ -65,15 +65,9 @@ private static readonly MethodInfo TrimEndMethodInfoWithCharArrayArg
private static readonly MethodInfo TrimMethodInfoWithCharArrayArg
= typeof(string).GetRuntimeMethod(nameof(string.Trim), new[] { typeof(char[]) })!;
- private static readonly MethodInfo StartsWithMethodInfo
- = typeof(string).GetRuntimeMethod(nameof(string.StartsWith), new[] { typeof(string) })!;
-
private static readonly MethodInfo ContainsMethodInfo
= typeof(string).GetRuntimeMethod(nameof(string.Contains), new[] { typeof(string) })!;
- private static readonly MethodInfo EndsWithMethodInfo
- = typeof(string).GetRuntimeMethod(nameof(string.EndsWith), new[] { typeof(string) })!;
-
private static readonly MethodInfo FirstOrDefaultMethodInfoWithoutArgs
= typeof(Enumerable).GetRuntimeMethods().Single(
m => m.Name == nameof(Enumerable.FirstOrDefault)
@@ -85,7 +79,6 @@ private static readonly MethodInfo LastOrDefaultMethodInfoWithoutArgs
&& m.GetParameters().Length == 1).MakeGenericMethod(typeof(char));
private readonly ISqlExpressionFactory _sqlExpressionFactory;
- private const char LikeEscapeChar = '\\';
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
@@ -214,28 +207,20 @@ public SqliteStringMethodTranslator(ISqlExpressionFactory sqlExpressionFactory)
instance = _sqlExpressionFactory.ApplyTypeMapping(instance, stringTypeMapping);
pattern = _sqlExpressionFactory.ApplyTypeMapping(pattern, stringTypeMapping);
- return _sqlExpressionFactory.OrElse(
- _sqlExpressionFactory.Equal(
- pattern,
- _sqlExpressionFactory.Constant(string.Empty, stringTypeMapping)),
- _sqlExpressionFactory.GreaterThan(
- _sqlExpressionFactory.Function(
- "instr",
- new[] { instance, pattern },
- nullable: true,
- argumentsPropagateNullability: new[] { true, true },
- typeof(int)),
- _sqlExpressionFactory.Constant(0)));
- }
-
- if (StartsWithMethodInfo.Equals(method))
- {
- return TranslateStartsEndsWith(instance, arguments[0], true);
- }
-
- if (EndsWithMethodInfo.Equals(method))
- {
- return TranslateStartsEndsWith(instance, arguments[0], false);
+ // Note: we add IS NOT NULL checks here since we don't do null semantics/compensation for comparison (greater-than)
+ return
+ _sqlExpressionFactory.AndAlso(
+ _sqlExpressionFactory.IsNotNull(instance),
+ _sqlExpressionFactory.AndAlso(
+ _sqlExpressionFactory.IsNotNull(pattern),
+ _sqlExpressionFactory.GreaterThan(
+ _sqlExpressionFactory.Function(
+ "instr",
+ new[] { instance, pattern },
+ nullable: true,
+ argumentsPropagateNullability: new[] { true, true },
+ typeof(int)),
+ _sqlExpressionFactory.Constant(0))));
}
}
@@ -291,124 +276,6 @@ public SqliteStringMethodTranslator(ISqlExpressionFactory sqlExpressionFactory)
return null;
}
- private SqlExpression TranslateStartsEndsWith(SqlExpression instance, SqlExpression pattern, bool startsWith)
- {
- var stringTypeMapping = ExpressionExtensions.InferTypeMapping(instance, pattern);
-
- instance = _sqlExpressionFactory.ApplyTypeMapping(instance, stringTypeMapping);
- pattern = _sqlExpressionFactory.ApplyTypeMapping(pattern, stringTypeMapping);
-
- if (pattern is SqlConstantExpression constantExpression)
- {
- // The pattern is constant. Aside from null or empty, we escape all special characters (%, _, \)
- // in C# and send a simple LIKE
- if (!(constantExpression.Value is string constantString))
- {
- return _sqlExpressionFactory.Like(instance, _sqlExpressionFactory.Constant(null, stringTypeMapping));
- }
-
- if (constantString.Length == 0)
- {
- return _sqlExpressionFactory.Constant(true);
- }
-
- return constantString.Any(c => IsLikeWildChar(c))
- ? _sqlExpressionFactory.Like(
- instance,
- _sqlExpressionFactory.Constant(
- startsWith
- ? EscapeLikePattern(constantString) + '%'
- : '%' + EscapeLikePattern(constantString)),
- _sqlExpressionFactory.Constant(
- LikeEscapeChar.ToString())) // SQL Server has no char mapping, avoid value conversion warning)
- : _sqlExpressionFactory.Like(
- instance,
- _sqlExpressionFactory.Constant(startsWith ? constantString + '%' : '%' + constantString));
- }
-
- // The pattern is non-constant, we use LEFT or RIGHT to extract substring and compare.
- // For StartsWith we also first run a LIKE to quickly filter out most non-matching results (sargable, but imprecise
- // because of wildcards).
- if (startsWith)
- {
- return _sqlExpressionFactory.OrElse(
- _sqlExpressionFactory.AndAlso(
- _sqlExpressionFactory.Like(
- instance,
- _sqlExpressionFactory.Add(
- pattern,
- _sqlExpressionFactory.Constant("%"))),
- _sqlExpressionFactory.Equal(
- _sqlExpressionFactory.Function(
- "substr",
- new[]
- {
- instance,
- _sqlExpressionFactory.Constant(1),
- _sqlExpressionFactory.Function(
- "length",
- new[] { pattern },
- nullable: true,
- argumentsPropagateNullability: new[] { true },
- typeof(int))
- },
- nullable: true,
- argumentsPropagateNullability: new[] { true, false, true },
- typeof(string),
- stringTypeMapping),
- pattern)),
- _sqlExpressionFactory.Equal(
- pattern,
- _sqlExpressionFactory.Constant(string.Empty)));
- }
-
- return _sqlExpressionFactory.OrElse(
- _sqlExpressionFactory.Equal(
- _sqlExpressionFactory.Function(
- "substr",
- new[]
- {
- instance,
- _sqlExpressionFactory.Negate(
- _sqlExpressionFactory.Function(
- "length",
- new[] { pattern },
- nullable: true,
- argumentsPropagateNullability: new[] { true },
- typeof(int)))
- },
- nullable: true,
- argumentsPropagateNullability: new[] { true, true },
- typeof(string),
- stringTypeMapping),
- pattern),
- _sqlExpressionFactory.Equal(
- pattern,
- _sqlExpressionFactory.Constant(string.Empty)));
- }
-
- // See https://www.sqlite.org/lang_expr.html
- private static bool IsLikeWildChar(char c)
- => c is '%' or '_';
-
- private static string EscapeLikePattern(string pattern)
- {
- var builder = new StringBuilder();
- for (var i = 0; i < pattern.Length; i++)
- {
- var c = pattern[i];
- if (IsLikeWildChar(c)
- || c == LikeEscapeChar)
- {
- builder.Append(LikeEscapeChar);
- }
-
- builder.Append(c);
- }
-
- return builder.ToString();
- }
-
private SqlExpression? ProcessTrimMethod(SqlExpression instance, IReadOnlyList arguments, string functionName)
{
var typeMapping = instance.TypeMapping;
diff --git a/src/EFCore/DbFunctionsExtensions.cs b/src/EFCore/DbFunctionsExtensions.cs
index 8a3b831a673..86b9d15f0f9 100644
--- a/src/EFCore/DbFunctionsExtensions.cs
+++ b/src/EFCore/DbFunctionsExtensions.cs
@@ -31,10 +31,7 @@ public static class DbFunctionsExtensions
/// The string that is to be matched.
/// The pattern which may involve wildcards %,_,[,],^.
/// if there is a match.
- public static bool Like(
- this DbFunctions _,
- string matchExpression,
- string pattern)
+ public static bool Like(this DbFunctions _, string? matchExpression, string? pattern)
=> throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Like)));
///
@@ -59,11 +56,7 @@ public static bool Like(
/// if they are not used as wildcards.
///
/// if there is a match.
- public static bool Like(
- this DbFunctions _,
- string matchExpression,
- string pattern,
- string escapeCharacter)
+ public static bool Like(this DbFunctions _, string? matchExpression, string? pattern, string? escapeCharacter)
=> throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Like)));
///
diff --git a/src/EFCore/Query/Internal/QueryOptimizingExpressionVisitor.cs b/src/EFCore/Query/Internal/QueryOptimizingExpressionVisitor.cs
index 4e7deb8ab01..6ca20e19952 100644
--- a/src/EFCore/Query/Internal/QueryOptimizingExpressionVisitor.cs
+++ b/src/EFCore/Query/Internal/QueryOptimizingExpressionVisitor.cs
@@ -38,14 +38,6 @@ public class QueryOptimizingExpressionVisitor : ExpressionVisitor
private static readonly MethodInfo StringCompareWithoutComparisonMethod =
typeof(string).GetRuntimeMethod(nameof(string.Compare), new[] { typeof(string), typeof(string) })!;
- private static readonly MethodInfo StartsWithMethodInfo =
- typeof(string).GetRuntimeMethod(nameof(string.StartsWith), new[] { typeof(string) })!;
-
- private static readonly MethodInfo EndsWithMethodInfo =
- typeof(string).GetRuntimeMethod(nameof(string.EndsWith), new[] { typeof(string) })!;
-
- private static readonly Expression ConstantNullString = Expression.Constant(null, typeof(string));
-
///
/// 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
@@ -180,33 +172,6 @@ protected override MemberAssignment VisitMemberAssignment(MemberAssignment membe
///
protected override Expression VisitMethodCall(MethodCallExpression methodCallExpression)
{
- if (Equals(StartsWithMethodInfo, methodCallExpression.Method)
- || Equals(EndsWithMethodInfo, methodCallExpression.Method))
- {
- if (methodCallExpression.Arguments[0] is ConstantExpression { Value: "" })
- {
- // every string starts/ends with empty string.
- return Expression.Constant(true);
- }
-
- var newObject = Visit(methodCallExpression.Object)!;
- var newArgument = Visit(methodCallExpression.Arguments[0]);
-
- var result = Expression.AndAlso(
- Expression.NotEqual(newObject, ConstantNullString),
- Expression.AndAlso(
- Expression.NotEqual(newArgument, ConstantNullString),
- methodCallExpression.Update(newObject, new[] { newArgument })));
-
- return newArgument is ConstantExpression
- ? result
- : Expression.OrElse(
- Expression.Equal(
- newArgument,
- Expression.Constant(string.Empty)),
- result);
- }
-
// Normalize x.Any(i => i == foo) to x.Contains(foo)
// And x.All(i => i != foo) to !x.Contains(foo)
if (methodCallExpression.Method.IsGenericMethod
@@ -335,38 +300,7 @@ protected override Expression VisitNewArray(NewArrayExpression newArrayExpressio
/// doing so can result in application failures when updating to a new Entity Framework Core release.
///
protected override Expression VisitUnary(UnaryExpression unaryExpression)
- {
- if (unaryExpression is { NodeType: ExpressionType.Not, Operand: MethodCallExpression innerMethodCall }
- && (Equals(StartsWithMethodInfo, innerMethodCall.Method)
- || Equals(EndsWithMethodInfo, innerMethodCall.Method)))
- {
- if (innerMethodCall.Arguments[0] is ConstantExpression { Value: "" })
- {
- // every string starts/ends with empty string.
- return Expression.Constant(false);
- }
-
- var newObject = Visit(innerMethodCall.Object)!;
- var newArgument = Visit(innerMethodCall.Arguments[0]);
-
- var result = Expression.AndAlso(
- Expression.NotEqual(newObject, ConstantNullString),
- Expression.AndAlso(
- Expression.NotEqual(newArgument, ConstantNullString),
- Expression.Not(innerMethodCall.Update(newObject, new[] { newArgument }))));
-
- return newArgument is ConstantExpression
- ? result
- : Expression.AndAlso(
- Expression.NotEqual(
- newArgument,
- Expression.Constant(string.Empty)),
- result);
- }
-
- return unaryExpression.Update(
- Visit(unaryExpression.Operand));
- }
+ => unaryExpression.Update(Visit(unaryExpression.Operand));
private static Expression MatchExpressionType(Expression expression, Type typeToMatch)
=> expression.Type != typeToMatch
diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindFunctionsQueryCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindFunctionsQueryCosmosTest.cs
index 6ff277f6f90..fd80af23244 100644
--- a/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindFunctionsQueryCosmosTest.cs
+++ b/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindFunctionsQueryCosmosTest.cs
@@ -30,7 +30,21 @@ public override async Task String_StartsWith_Literal(bool async)
"""
SELECT c
FROM root c
-WHERE ((c["Discriminator"] = "Customer") AND ((c["ContactName"] != null) AND (("M" != null) AND STARTSWITH(c["ContactName"], "M"))))
+WHERE ((c["Discriminator"] = "Customer") AND STARTSWITH(c["ContactName"], "M"))
+""");
+ }
+
+ public override async Task String_StartsWith_Parameter(bool async)
+ {
+ await base.String_StartsWith_Parameter(async);
+
+ AssertSql(
+"""
+@__pattern_0='M'
+
+SELECT c
+FROM root c
+WHERE ((c["Discriminator"] = "Customer") AND STARTSWITH(c["ContactName"], @__pattern_0))
""");
}
@@ -42,8 +56,9 @@ public override async Task String_StartsWith_Identity(bool async)
"""
SELECT c
FROM root c
-WHERE ((c["Discriminator"] = "Customer") AND ((c["ContactName"] = "") OR ((c["ContactName"] != null) AND ((c["ContactName"] != null) AND STARTSWITH(c["ContactName"], c["ContactName"])))))
+WHERE ((c["Discriminator"] = "Customer") AND STARTSWITH(c["ContactName"], c["ContactName"]))
""");
+
}
public override async Task String_StartsWith_Column(bool async)
@@ -54,7 +69,7 @@ public override async Task String_StartsWith_Column(bool async)
"""
SELECT c
FROM root c
-WHERE ((c["Discriminator"] = "Customer") AND ((c["ContactName"] = "") OR ((c["ContactName"] != null) AND ((c["ContactName"] != null) AND STARTSWITH(c["ContactName"], c["ContactName"])))))
+WHERE ((c["Discriminator"] = "Customer") AND STARTSWITH(c["ContactName"], c["ContactName"]))
""");
}
@@ -66,7 +81,7 @@ public override async Task String_StartsWith_MethodCall(bool async)
"""
SELECT c
FROM root c
-WHERE ((c["Discriminator"] = "Customer") AND ((c["ContactName"] != null) AND (("M" != null) AND STARTSWITH(c["ContactName"], "M"))))
+WHERE ((c["Discriminator"] = "Customer") AND STARTSWITH(c["ContactName"], "M"))
""");
}
@@ -78,7 +93,21 @@ public override async Task String_EndsWith_Literal(bool async)
"""
SELECT c
FROM root c
-WHERE ((c["Discriminator"] = "Customer") AND ((c["ContactName"] != null) AND (("b" != null) AND ENDSWITH(c["ContactName"], "b"))))
+WHERE ((c["Discriminator"] = "Customer") AND ENDSWITH(c["ContactName"], "b"))
+""");
+ }
+
+ public override async Task String_EndsWith_Parameter(bool async)
+ {
+ await base.String_EndsWith_Parameter(async);
+
+ AssertSql(
+"""
+@__pattern_0='b'
+
+SELECT c
+FROM root c
+WHERE ((c["Discriminator"] = "Customer") AND ENDSWITH(c["ContactName"], @__pattern_0))
""");
}
@@ -90,7 +119,7 @@ public override async Task String_EndsWith_Identity(bool async)
"""
SELECT c
FROM root c
-WHERE ((c["Discriminator"] = "Customer") AND ((c["ContactName"] = "") OR ((c["ContactName"] != null) AND ((c["ContactName"] != null) AND ENDSWITH(c["ContactName"], c["ContactName"])))))
+WHERE ((c["Discriminator"] = "Customer") AND ENDSWITH(c["ContactName"], c["ContactName"]))
""");
}
@@ -102,7 +131,7 @@ public override async Task String_EndsWith_Column(bool async)
"""
SELECT c
FROM root c
-WHERE ((c["Discriminator"] = "Customer") AND ((c["ContactName"] = "") OR ((c["ContactName"] != null) AND ((c["ContactName"] != null) AND ENDSWITH(c["ContactName"], c["ContactName"])))))
+WHERE ((c["Discriminator"] = "Customer") AND ENDSWITH(c["ContactName"], c["ContactName"]))
""");
}
@@ -114,7 +143,7 @@ public override async Task String_EndsWith_MethodCall(bool async)
"""
SELECT c
FROM root c
-WHERE ((c["Discriminator"] = "Customer") AND ((c["ContactName"] != null) AND (("m" != null) AND ENDSWITH(c["ContactName"], "m"))))
+WHERE ((c["Discriminator"] = "Customer") AND ENDSWITH(c["ContactName"], "m"))
""");
}
diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindMiscellaneousQueryCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindMiscellaneousQueryCosmosTest.cs
index ed5ae1017ef..86988aa34a1 100644
--- a/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindMiscellaneousQueryCosmosTest.cs
+++ b/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindMiscellaneousQueryCosmosTest.cs
@@ -1429,7 +1429,7 @@ await Assert.ThrowsAsync(
"""
SELECT c["City"]
FROM root c
-WHERE ((c["Discriminator"] = "Customer") AND ((c["CustomerID"] != null) AND (("A" != null) AND STARTSWITH(c["CustomerID"], "A"))))
+WHERE ((c["Discriminator"] = "Customer") AND STARTSWITH(c["CustomerID"], "A"))
ORDER BY c["Country"], c["City"]
""");
}
@@ -2966,7 +2966,7 @@ public override async Task Comparing_to_fixed_string_parameter(bool async)
SELECT c["CustomerID"]
FROM root c
-WHERE ((c["Discriminator"] = "Customer") AND ((@__prefix_0 = "") OR ((c["CustomerID"] != null) AND ((@__prefix_0 != null) AND STARTSWITH(c["CustomerID"], @__prefix_0)))))
+WHERE ((c["Discriminator"] = "Customer") AND STARTSWITH(c["CustomerID"], @__prefix_0))
""");
}
@@ -2994,7 +2994,7 @@ public override async Task Comparing_entity_to_null_using_Equals(bool async)
"""
SELECT c["CustomerID"]
FROM root c
-WHERE (((c["Discriminator"] = "Customer") AND ((c["CustomerID"] != null) AND (("A" != null) AND STARTSWITH(c["CustomerID"], "A")))) AND NOT((c["CustomerID"] = null)))
+WHERE (((c["Discriminator"] = "Customer") AND STARTSWITH(c["CustomerID"], "A")) AND NOT((c["CustomerID"] = null)))
ORDER BY c["CustomerID"]
""");
}
@@ -3059,7 +3059,7 @@ public override async Task Compare_collection_navigation_with_itself(bool async)
"""
SELECT c["CustomerID"]
FROM root c
-WHERE (((c["Discriminator"] = "Customer") AND ((c["CustomerID"] != null) AND (("A" != null) AND STARTSWITH(c["CustomerID"], "A")))) AND (c["CustomerID"] = c["CustomerID"]))
+WHERE (((c["Discriminator"] = "Customer") AND STARTSWITH(c["CustomerID"], "A")) AND (c["CustomerID"] = c["CustomerID"]))
""");
}
@@ -3095,7 +3095,7 @@ public override async Task OrderBy_ThenBy_same_column_different_direction(bool a
"""
SELECT c["CustomerID"]
FROM root c
-WHERE ((c["Discriminator"] = "Customer") AND ((c["CustomerID"] != null) AND (("A" != null) AND STARTSWITH(c["CustomerID"], "A"))))
+WHERE ((c["Discriminator"] = "Customer") AND STARTSWITH(c["CustomerID"], "A"))
ORDER BY c["CustomerID"]
""");
}
@@ -3108,7 +3108,7 @@ public override async Task OrderBy_OrderBy_same_column_different_direction(bool
"""
SELECT c["CustomerID"]
FROM root c
-WHERE ((c["Discriminator"] = "Customer") AND ((c["CustomerID"] != null) AND (("A" != null) AND STARTSWITH(c["CustomerID"], "A"))))
+WHERE ((c["Discriminator"] = "Customer") AND STARTSWITH(c["CustomerID"], "A"))
ORDER BY c["CustomerID"] DESC
""");
}
@@ -4238,7 +4238,7 @@ public override async Task MemberInitExpression_NewExpression_is_funcletized_eve
"""
SELECT c["CustomerID"]
FROM root c
-WHERE ((c["Discriminator"] = "Customer") AND ((c["CustomerID"] != null) AND (("A" != null) AND STARTSWITH(c["CustomerID"], "A"))))
+WHERE ((c["Discriminator"] = "Customer") AND STARTSWITH(c["CustomerID"], "A"))
""");
}
diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindSelectQueryCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindSelectQueryCosmosTest.cs
index bfc4e24ba47..c7187c13161 100644
--- a/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindSelectQueryCosmosTest.cs
+++ b/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindSelectQueryCosmosTest.cs
@@ -119,7 +119,7 @@ public override async Task Projection_of_entity_type_into_object_array(bool asyn
"""
SELECT c
FROM root c
-WHERE ((c["Discriminator"] = "Customer") AND ((c["CustomerID"] != null) AND (("A" != null) AND STARTSWITH(c["CustomerID"], "A"))))
+WHERE ((c["Discriminator"] = "Customer") AND STARTSWITH(c["CustomerID"], "A"))
ORDER BY c["CustomerID"]
""");
}
@@ -426,7 +426,7 @@ public override async Task New_date_time_in_anonymous_type_works(bool async)
"""
SELECT 1
FROM root c
-WHERE ((c["Discriminator"] = "Customer") AND ((c["CustomerID"] != null) AND (("A" != null) AND STARTSWITH(c["CustomerID"], "A"))))
+WHERE ((c["Discriminator"] = "Customer") AND STARTSWITH(c["CustomerID"], "A"))
""");
}
@@ -903,7 +903,7 @@ public override async Task Client_method_in_projection_requiring_materialization
"""
SELECT c
FROM root c
-WHERE ((c["Discriminator"] = "Customer") AND ((c["CustomerID"] != null) AND (("A" != null) AND STARTSWITH(c["CustomerID"], "A"))))
+WHERE ((c["Discriminator"] = "Customer") AND STARTSWITH(c["CustomerID"], "A"))
""");
}
@@ -915,7 +915,7 @@ public override async Task Client_method_in_projection_requiring_materialization
"""
SELECT c
FROM root c
-WHERE ((c["Discriminator"] = "Customer") AND ((c["CustomerID"] != null) AND (("A" != null) AND STARTSWITH(c["CustomerID"], "A"))))
+WHERE ((c["Discriminator"] = "Customer") AND STARTSWITH(c["CustomerID"], "A"))
""");
}
@@ -1292,7 +1292,7 @@ public override async Task Projection_take_predicate_projection(bool async)
SELECT VALUE {"Aggregate" : ((c["CustomerID"] || " ") || c["City"])}
FROM root c
-WHERE ((c["Discriminator"] = "Customer") AND ((c["CustomerID"] != null) AND (("A" != null) AND STARTSWITH(c["CustomerID"], "A"))))
+WHERE ((c["Discriminator"] = "Customer") AND STARTSWITH(c["CustomerID"], "A"))
ORDER BY c["CustomerID"]
OFFSET 0 LIMIT @__p_0
""");
@@ -1632,7 +1632,7 @@ public override async Task VisitLambda_should_not_be_visited_trivially(bool asyn
"""
SELECT c
FROM root c
-WHERE ((c["Discriminator"] = "Order") AND ((c["CustomerID"] != null) AND (("A" != null) AND STARTSWITH(c["CustomerID"], "A"))))
+WHERE ((c["Discriminator"] = "Order") AND STARTSWITH(c["CustomerID"], "A"))
""");
}
@@ -1824,7 +1824,7 @@ public override async Task Using_enumerable_parameter_in_projection(bool async)
"""
SELECT c["CustomerID"]
FROM root c
-WHERE ((c["Discriminator"] = "Customer") AND ((c["CustomerID"] != null) AND (("F" != null) AND STARTSWITH(c["CustomerID"], "F"))))
+WHERE ((c["Discriminator"] = "Customer") AND STARTSWITH(c["CustomerID"], "F"))
""");
}
diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindWhereQueryCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindWhereQueryCosmosTest.cs
index fae640c3f12..8525a6da14f 100644
--- a/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindWhereQueryCosmosTest.cs
+++ b/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindWhereQueryCosmosTest.cs
@@ -1667,7 +1667,7 @@ public override async Task Where_comparison_to_nullable_bool(bool async)
"""
SELECT c
FROM root c
-WHERE ((c["Discriminator"] = "Customer") AND (((c["CustomerID"] != null) AND (("KI" != null) AND ENDSWITH(c["CustomerID"], "KI"))) = true))
+WHERE ((c["Discriminator"] = "Customer") AND (ENDSWITH(c["CustomerID"], "KI") = true))
""");
}
diff --git a/test/EFCore.InMemory.FunctionalTests/Query/GearsOfWarQueryInMemoryTest.cs b/test/EFCore.InMemory.FunctionalTests/Query/GearsOfWarQueryInMemoryTest.cs
index ce9292645ed..1ad6264d09c 100644
--- a/test/EFCore.InMemory.FunctionalTests/Query/GearsOfWarQueryInMemoryTest.cs
+++ b/test/EFCore.InMemory.FunctionalTests/Query/GearsOfWarQueryInMemoryTest.cs
@@ -29,6 +29,30 @@ public override async Task
.Null_semantics_is_correctly_applied_for_function_comparisons_that_take_arguments_from_optional_navigation_complex(
async))).Message);
+ public override async Task Group_by_on_StartsWith_with_null_parameter_as_argument(bool async)
+ => Assert.Equal(
+ "Value cannot be null. (Parameter 'value')",
+ (await Assert.ThrowsAsync(
+ () => base.Group_by_on_StartsWith_with_null_parameter_as_argument(async))).Message);
+
+ public override async Task Group_by_with_having_StartsWith_with_null_parameter_as_argument(bool async)
+ => Assert.Equal(
+ "Value cannot be null. (Parameter 'value')",
+ (await Assert.ThrowsAsync(
+ () => base.Group_by_with_having_StartsWith_with_null_parameter_as_argument(async))).Message);
+
+ public override async Task OrderBy_StartsWith_with_null_parameter_as_argument(bool async)
+ => Assert.Equal(
+ "Value cannot be null. (Parameter 'value')",
+ (await Assert.ThrowsAsync(
+ () => base.OrderBy_StartsWith_with_null_parameter_as_argument(async))).Message);
+
+ public override async Task Select_StartsWith_with_null_parameter_as_argument(bool async)
+ => Assert.Equal(
+ "Value cannot be null. (Parameter 'value')",
+ (await Assert.ThrowsAsync(
+ () => base.Select_StartsWith_with_null_parameter_as_argument(async))).Message);
+
public override async Task Projecting_entity_as_well_as_correlated_collection_followed_by_Distinct(bool async)
// Distinct. Issue #24325.
=> Assert.Equal(
diff --git a/test/EFCore.Relational.Specification.Tests/Query/NorthwindFunctionsQueryRelationalTestBase.cs b/test/EFCore.Relational.Specification.Tests/Query/NorthwindFunctionsQueryRelationalTestBase.cs
index 0666285093e..0c55bc50430 100644
--- a/test/EFCore.Relational.Specification.Tests/Query/NorthwindFunctionsQueryRelationalTestBase.cs
+++ b/test/EFCore.Relational.Specification.Tests/Query/NorthwindFunctionsQueryRelationalTestBase.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.TestModels.Northwind;
+
namespace Microsoft.EntityFrameworkCore.Query;
public abstract class NorthwindFunctionsQueryRelationalTestBase : NorthwindFunctionsQueryTestBase
diff --git a/test/EFCore.Relational.Specification.Tests/Query/NullSemanticsQueryTestBase.cs b/test/EFCore.Relational.Specification.Tests/Query/NullSemanticsQueryTestBase.cs
index cac161b3732..7f6c4f6357f 100644
--- a/test/EFCore.Relational.Specification.Tests/Query/NullSemanticsQueryTestBase.cs
+++ b/test/EFCore.Relational.Specification.Tests/Query/NullSemanticsQueryTestBase.cs
@@ -2044,6 +2044,75 @@ join e2 in ss.Set() on e1.Id equals e2.Id
select (e1.NullableIntA ?? (e1.NullableIntB ?? (e2.NullableIntC ?? e2.NullableIntB)))
?? e1.NullableIntC ?? (e2.NullableIntA ?? e2.NullableIntC ?? e1.NullableIntA));
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual async Task Like(bool async)
+ {
+ await AssertQueryScalar(async,
+ ss => ss.Set().Where(e => EF.Functions.Like(e.StringA, e.StringB)).Select(e => e.Id),
+ ss => ss.Set().Where(e => LikeLite(e.StringA, e.StringB)).Select(e => e.Id));
+
+ await AssertQueryScalar(async,
+ ss => ss.Set().Where(e => EF.Functions.Like(e.StringA, e.NullableStringB)).Select(e => e.Id),
+ ss => ss.Set().Where(e => LikeLite(e.StringA, e.NullableStringB)).Select(e => e.Id));
+
+ await AssertQueryScalar(async,
+ ss => ss.Set().Where(e => EF.Functions.Like(e.NullableStringA, e.StringB)).Select(e => e.Id),
+ ss => ss.Set().Where(e => LikeLite(e.NullableStringA, e.StringB)).Select(e => e.Id));
+
+ await AssertQueryScalar(async,
+ ss => ss.Set().Where(e => EF.Functions.Like(e.NullableStringA, e.NullableStringB)).Select(e => e.Id),
+ ss => ss.Set().Where(e => LikeLite(e.NullableStringA, e.NullableStringB)).Select(e => e.Id));
+ }
+
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual async Task Like_negated(bool async)
+ {
+ await AssertQueryScalar(async,
+ ss => ss.Set().Where(e => !EF.Functions.Like(e.StringA, e.StringB)).Select(e => e.Id),
+ ss => ss.Set().Where(e => !LikeLite(e.StringA, e.StringB)).Select(e => e.Id));
+
+ await AssertQueryScalar(async,
+ ss => ss.Set().Where(e => !EF.Functions.Like(e.StringA, e.NullableStringB)).Select(e => e.Id),
+ ss => ss.Set().Where(e => !LikeLite(e.StringA, e.NullableStringB)).Select(e => e.Id));
+
+ await AssertQueryScalar(async,
+ ss => ss.Set().Where(e => !EF.Functions.Like(e.NullableStringA, e.StringB)).Select(e => e.Id),
+ ss => ss.Set().Where(e => !LikeLite(e.NullableStringA, e.StringB)).Select(e => e.Id));
+
+ await AssertQueryScalar(async,
+ ss => ss.Set().Where(e => !EF.Functions.Like(e.NullableStringA, e.NullableStringB)).Select(e => e.Id),
+ ss => ss.Set().Where(e => !LikeLite(e.NullableStringA, e.NullableStringB)).Select(e => e.Id));
+ }
+
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual async Task Like_with_escape_char(bool async)
+ {
+ await AssertQueryScalar(async,
+ ss => ss.Set().Where(e => EF.Functions.Like(e.StringA, e.StringB, "\\")).Select(e => e.Id),
+ ss => ss.Set().Where(e => LikeLite(e.StringA, e.StringB)).Select(e => e.Id));
+
+ await AssertQueryScalar(async,
+ ss => ss.Set().Where(e => !EF.Functions.Like(e.StringA, e.StringB, "\\")).Select(e => e.Id),
+ ss => ss.Set().Where(e => !LikeLite(e.StringA, e.StringB)).Select(e => e.Id));
+
+ await AssertQueryScalar(async,
+ ss => ss.Set().Where(e => EF.Functions.Like(e.StringA, e.StringB, null)).Select(e => e.Id),
+ ss => ss.Set().Where(e => false).Select(e => e.Id));
+
+ await AssertQueryScalar(async,
+ ss => ss.Set().Where(e => !EF.Functions.Like(e.StringA, e.StringB, null)).Select(e => e.Id),
+ ss => ss.Set().Where(e => true).Select(e => e.Id));
+ }
+
+ // We can't client-evaluate Like (for the expected results).
+ // However, since the test data has no LIKE wildcards, it effectively functions like equality - except that 'null like null' returns
+ // false instead of true. So we have this "lite" implementation which doesn't support wildcards.
+ private bool LikeLite(string s, string pattern)
+ => s == pattern && s is not null && pattern is not null;
+
private string NormalizeDelimitersInRawString(string sql)
=> Fixture.TestStore.NormalizeDelimitersInRawString(sql);
diff --git a/test/EFCore.Specification.Tests/Query/ComplexNavigationsQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/ComplexNavigationsQueryTestBase.cs
index 529d3213ae0..35afecee25a 100644
--- a/test/EFCore.Specification.Tests/Query/ComplexNavigationsQueryTestBase.cs
+++ b/test/EFCore.Specification.Tests/Query/ComplexNavigationsQueryTestBase.cs
@@ -310,6 +310,9 @@ public virtual Task Method_call_on_optional_navigation_translates_to_null_condit
async,
ss => from e1 in ss.Set()
where e1.OneToOne_Optional_FK1.Name.StartsWith(e1.OneToOne_Optional_FK1.Name)
+ select e1,
+ ss => from e1 in ss.Set()
+ where e1.OneToOne_Optional_FK1.Name.MaybeScalar(x => x.StartsWith(e1.OneToOne_Optional_FK1.Name)) == true
select e1);
[ConditionalTheory]
diff --git a/test/EFCore.Specification.Tests/Query/FunkyDataQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/FunkyDataQueryTestBase.cs
index 6596ad8a00a..2d8d4b9bd9e 100644
--- a/test/EFCore.Specification.Tests/Query/FunkyDataQueryTestBase.cs
+++ b/test/EFCore.Specification.Tests/Query/FunkyDataQueryTestBase.cs
@@ -23,11 +23,13 @@ public virtual async Task String_contains_on_argument_with_wildcard_constant(boo
{
await AssertQuery(
async,
- ss => ss.Set().Where(c => c.FirstName.Contains("%B")).Select(c => c.FirstName));
+ ss => ss.Set().Where(c => c.FirstName.Contains("%B")).Select(c => c.FirstName),
+ ss => ss.Set().Where(c => c.FirstName.MaybeScalar(x => x.Contains("%B")) == true).Select(c => c.FirstName));
await AssertQuery(
async,
- ss => ss.Set().Where(c => c.FirstName.Contains("a_")).Select(c => c.FirstName));
+ ss => ss.Set().Where(c => c.FirstName.Contains("a_")).Select(c => c.FirstName),
+ ss => ss.Set().Where(c => c.FirstName.MaybeScalar(x => x.Contains("a_")) == true).Select(c => c.FirstName));
await AssertQuery(
async,
@@ -37,28 +39,28 @@ await AssertQuery(
await AssertQuery(
async,
ss => ss.Set().Where(c => c.FirstName.Contains("")).Select(c => c.FirstName),
- ss => ss.Set().Select(c => c.FirstName));
+ ss => ss.Set().Where(c => c.FirstName != null).Select(c => c.FirstName));
await AssertQuery(
async,
- ss => ss.Set().Where(c => c.FirstName.Contains("_Ba_")).Select(c => c.FirstName));
+ ss => ss.Set().Where(c => c.FirstName.Contains("_Ba_")).Select(c => c.FirstName),
+ ss => ss.Set().Where(c => c.FirstName.MaybeScalar(x => x.Contains("_Ba_")) == true).Select(c => c.FirstName));
await AssertQuery(
async,
ss => ss.Set().Where(c => !c.FirstName.Contains("%B%a%r")).Select(c => c.FirstName),
- ss => ss.Set().Where(c => !c.FirstName.MaybeScalar(x => x.Contains("%B%a%r")) == true)
+ ss => ss.Set().Where(c => c.FirstName.MaybeScalar(x => x.Contains("%B%a%r")) != true)
.Select(c => c.FirstName));
await AssertQuery(
async,
ss => ss.Set().Where(c => !c.FirstName.Contains("")).Select(c => c.FirstName),
- ss => ss.Set().Where(c => !c.FirstName.MaybeScalar(x => x.Contains("")) == true)
- .Select(c => c.FirstName));
+ ss => ss.Set().Where(c => c.FirstName == null).Select(c => c.FirstName));
await AssertQuery(
async,
ss => ss.Set().Where(c => !c.FirstName.Contains(null)).Select(c => c.FirstName),
- ss => ss.Set().Where(c => false).Select(c => c.FirstName));
+ ss => ss.Set().Where(c => true).Select(c => c.FirstName));
}
[ConditionalTheory]
@@ -68,12 +70,14 @@ public virtual async Task String_contains_on_argument_with_wildcard_parameter(bo
var prm1 = "%B";
await AssertQuery(
async,
- ss => ss.Set().Where(c => c.FirstName.Contains(prm1)).Select(c => c.FirstName));
+ ss => ss.Set().Where(c => c.FirstName.Contains(prm1)).Select(c => c.FirstName),
+ ss => ss.Set().Where(c => c.FirstName.MaybeScalar(x => x.Contains(prm1)) == true).Select(c => c.FirstName));
var prm2 = "a_";
await AssertQuery(
async,
- ss => ss.Set().Where(c => c.FirstName.Contains(prm2)).Select(c => c.FirstName));
+ ss => ss.Set().Where(c => c.FirstName.Contains(prm2)).Select(c => c.FirstName),
+ ss => ss.Set().Where(c => c.FirstName.MaybeScalar(x => x.Contains(prm2)) == true).Select(c => c.FirstName));
var prm3 = (string)null;
await AssertQuery(
@@ -85,31 +89,32 @@ await AssertQuery(
await AssertQuery(
async,
ss => ss.Set().Where(c => c.FirstName.Contains(prm4)).Select(c => c.FirstName),
- ss => ss.Set().Select(c => c.FirstName));
+ ss => ss.Set().Where(c => c.FirstName != null).Select(c => c.FirstName));
var prm5 = "_Ba_";
await AssertQuery(
async,
- ss => ss.Set().Where(c => c.FirstName.Contains(prm5)).Select(c => c.FirstName));
+ ss => ss.Set().Where(c => c.FirstName.Contains(prm5)).Select(c => c.FirstName),
+ ss => ss.Set().Where(c => c.FirstName.MaybeScalar(x => x.Contains(prm5)) == true).Select(c => c.FirstName));
var prm6 = "%B%a%r";
await AssertQuery(
async,
ss => ss.Set().Where(c => !c.FirstName.Contains(prm6)).Select(c => c.FirstName),
- ss => ss.Set().Where(c => !c.FirstName.MaybeScalar(x => x.Contains(prm6)) == true)
+ ss => ss.Set().Where(c => c.FirstName.MaybeScalar(x => x.Contains(prm6)) != true)
.Select(c => c.FirstName));
var prm7 = "";
await AssertQuery(
async,
ss => ss.Set().Where(c => !c.FirstName.Contains(prm7)).Select(c => c.FirstName),
- ss => ss.Set().Where(c => false).Select(c => c.FirstName));
+ ss => ss.Set().Where(c => c.FirstName == null).Select(c => c.FirstName));
var prm8 = (string)null;
await AssertQuery(
async,
ss => ss.Set().Where(c => !c.FirstName.Contains(prm8)).Select(c => c.FirstName),
- ss => ss.Set().Where(c => false).Select(c => c.FirstName));
+ ss => ss.Set().Where(c => true).Select(c => c.FirstName));
}
[ConditionalTheory]
@@ -122,7 +127,7 @@ public virtual Task String_contains_on_argument_with_wildcard_column(bool async)
.Where(r => r.fn.Contains(r.ln)),
ss => ss.Set().Select(c => c.FirstName)
.SelectMany(c => ss.Set().Select(c2 => c2.LastName), (fn, ln) => new { fn, ln })
- .Where(r => r.ln == "" || r.fn.Contains(r.ln)),
+ .Where(r => r.fn.MaybeScalar(x => r.ln.MaybeScalar(xx => x.Contains(xx))) == true),
elementSorter: e => (e.fn, e.ln),
elementAsserter: (e, a) =>
{
@@ -140,7 +145,8 @@ public virtual Task String_contains_on_argument_with_wildcard_column_negated(boo
.Where(r => !r.fn.Contains(r.ln)),
ss => ss.Set().Select(c => c.FirstName)
.SelectMany(c => ss.Set().Select(c2 => c2.LastName), (fn, ln) => new { fn, ln })
- .Where(r => r.ln != "" && !r.fn.MaybeScalar(x => r.ln.MaybeScalar(xx => x.Contains(xx))) == true));
+ .Where(r => r.fn.MaybeScalar(x => r.ln.MaybeScalar(xx => x.Contains(xx))) != true));
+ // .Where(r => r.ln != "" && !r.fn.MaybeScalar(x => r.ln.MaybeScalar(xx => x.Contains(xx))) == true));
[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
@@ -148,11 +154,13 @@ public virtual async Task String_starts_with_on_argument_with_wildcard_constant(
{
await AssertQuery(
async,
- ss => ss.Set().Where(c => c.FirstName.StartsWith("%B")).Select(c => c.FirstName));
+ ss => ss.Set().Where(c => c.FirstName.StartsWith("%B")).Select(c => c.FirstName),
+ ss => ss.Set().Where(c => c.FirstName.MaybeScalar(x => x.StartsWith("%B")) == true).Select(c => c.FirstName));
await AssertQuery(
async,
- ss => ss.Set().Where(c => c.FirstName.StartsWith("a_")).Select(c => c.FirstName));
+ ss => ss.Set().Where(c => c.FirstName.StartsWith("a_")).Select(c => c.FirstName),
+ ss => ss.Set().Where(c => c.FirstName.MaybeScalar(x => x.StartsWith("a_")) == true).Select(c => c.FirstName));
await AssertQuery(
async,
@@ -162,28 +170,29 @@ await AssertQuery(
await AssertQuery(
async,
ss => ss.Set().Where(c => c.FirstName.StartsWith("")).Select(c => c.FirstName),
- ss => ss.Set().Select(c => c.FirstName));
+ ss => ss.Set().Where(c => c.FirstName != null).Select(c => c.FirstName));
await AssertQuery(
async,
- ss => ss.Set().Where(c => c.FirstName.StartsWith("_Ba_")).Select(c => c.FirstName));
+ ss => ss.Set().Where(c => c.FirstName.StartsWith("_Ba_")).Select(c => c.FirstName),
+ ss => ss.Set().Where(c => c.FirstName.MaybeScalar(x => x.StartsWith("_Ba_")) == true).Select(c => c.FirstName));
await AssertQuery(
async,
ss => ss.Set().Where(c => !c.FirstName.StartsWith("%B%a%r")).Select(c => c.FirstName),
- ss => ss.Set().Where(c => !c.FirstName.MaybeScalar(x => x.StartsWith("%B%a%r")) == true)
+ ss => ss.Set().Where(c => c.FirstName.MaybeScalar(x => x.StartsWith("%B%a%r")) != true)
.Select(c => c.FirstName));
await AssertQuery(
async,
ss => ss.Set().Where(c => !c.FirstName.StartsWith("")).Select(c => c.FirstName),
- ss => ss.Set().Where(c => !c.FirstName.MaybeScalar(x => x.StartsWith("")) == true)
+ ss => ss.Set().Where(c => c.FirstName == null)
.Select(c => c.FirstName));
await AssertQuery(
async,
ss => ss.Set().Where(c => !c.FirstName.StartsWith(null)).Select(c => c.FirstName),
- ss => ss.Set().Where(c => false).Select(c => c.FirstName));
+ ss => ss.Set().Where(c => true).Select(c => c.FirstName));
}
[ConditionalTheory]
@@ -193,12 +202,14 @@ public virtual async Task String_starts_with_on_argument_with_wildcard_parameter
var prm1 = "%B";
await AssertQuery(
async,
- ss => ss.Set().Where(c => c.FirstName.StartsWith(prm1)).Select(c => c.FirstName));
+ ss => ss.Set().Where(c => c.FirstName.StartsWith(prm1)).Select(c => c.FirstName),
+ ss => ss.Set().Where(c => c.FirstName.MaybeScalar(x => x.StartsWith(prm1)) == true).Select(c => c.FirstName));
var prm2 = "a_";
await AssertQuery(
async,
- ss => ss.Set().Where(c => c.FirstName.StartsWith(prm2)).Select(c => c.FirstName));
+ ss => ss.Set().Where(c => c.FirstName.StartsWith(prm2)).Select(c => c.FirstName),
+ ss => ss.Set().Where(c => c.FirstName.MaybeScalar(x => x.StartsWith(prm2)) == true).Select(c => c.FirstName));
var prm3 = (string)null;
await AssertQuery(
@@ -210,31 +221,32 @@ await AssertQuery(
await AssertQuery(
async,
ss => ss.Set().Where(c => c.FirstName.StartsWith(prm4)).Select(c => c.FirstName),
- ss => ss.Set().Select(c => c.FirstName));
+ ss => ss.Set().Where(c => c.FirstName != null).Select(c => c.FirstName));
var prm5 = "_Ba_";
await AssertQuery(
async,
- ss => ss.Set().Where(c => c.FirstName.StartsWith(prm5)).Select(c => c.FirstName));
+ ss => ss.Set().Where(c => c.FirstName.StartsWith(prm5)).Select(c => c.FirstName),
+ ss => ss.Set().Where(c => c.FirstName.MaybeScalar(x => x.StartsWith(prm5)) == true).Select(c => c.FirstName));
var prm6 = "%B%a%r";
await AssertQuery(
async,
ss => ss.Set().Where(c => !c.FirstName.StartsWith(prm6)).Select(c => c.FirstName),
- ss => ss.Set().Where(c => !c.FirstName.MaybeScalar(x => x.StartsWith(prm6)) == true)
+ ss => ss.Set().Where(c => c.FirstName.MaybeScalar(x => x.StartsWith(prm6)) != true)
.Select(c => c.FirstName));
var prm7 = "";
await AssertQuery(
async,
ss => ss.Set().Where(c => !c.FirstName.StartsWith(prm7)).Select(c => c.FirstName),
- ss => ss.Set().Where(c => false).Select(c => c.FirstName));
+ ss => ss.Set().Where(c => c.FirstName == null).Select(c => c.FirstName));
var prm8 = (string)null;
await AssertQuery(
async,
ss => ss.Set().Where(c => !c.FirstName.StartsWith(prm8)).Select(c => c.FirstName),
- ss => ss.Set().Where(c => false).Select(c => c.FirstName));
+ ss => ss.Set().Where(c => true).Select(c => c.FirstName));
}
[ConditionalTheory]
@@ -243,34 +255,41 @@ public virtual async Task String_starts_with_on_argument_with_bracket(bool async
{
await AssertQuery(
async,
- ss => ss.Set().Where(c => c.FirstName.StartsWith("[")));
+ ss => ss.Set().Where(c => c.FirstName.StartsWith("[")),
+ ss => ss.Set().Where(c => c.FirstName.MaybeScalar(x => x.StartsWith("[")) == true));
await AssertQuery(
async,
- ss => ss.Set().Where(c => c.FirstName.StartsWith("B[")));
+ ss => ss.Set().Where(c => c.FirstName.StartsWith("B[")),
+ ss => ss.Set().Where(c => c.FirstName.MaybeScalar(x => x.StartsWith("B[")) == true));
await AssertQuery(
async,
- ss => ss.Set().Where(c => c.FirstName.StartsWith("B[[a^")));
+ ss => ss.Set().Where(c => c.FirstName.StartsWith("B[[a^")),
+ ss => ss.Set().Where(c => c.FirstName.MaybeScalar(x => x.StartsWith("B[[a^")) == true));
var prm1 = "[";
await AssertQuery(
async,
- ss => ss.Set().Where(c => c.FirstName.StartsWith(prm1)));
+ ss => ss.Set().Where(c => c.FirstName.StartsWith(prm1)),
+ ss => ss.Set().Where(c => c.FirstName.MaybeScalar(x => x.StartsWith(prm1)) == true));
var prm2 = "B[";
await AssertQuery(
async,
- ss => ss.Set().Where(c => c.FirstName.StartsWith(prm2)));
+ ss => ss.Set().Where(c => c.FirstName.StartsWith(prm2)),
+ ss => ss.Set().Where(c => c.FirstName.MaybeScalar(x => x.StartsWith(prm2)) == true));
var prm3 = "B[[a^";
await AssertQuery(
async,
- ss => ss.Set().Where(c => c.FirstName.StartsWith(prm3)));
+ ss => ss.Set().Where(c => c.FirstName.StartsWith(prm3)),
+ ss => ss.Set().Where(c => c.FirstName.MaybeScalar(x => x.StartsWith(prm3)) == true));
await AssertQuery(
async,
- ss => ss.Set().Where(c => c.FirstName.StartsWith(c.LastName)));
+ ss => ss.Set().Where(c => c.FirstName.StartsWith(c.LastName)),
+ ss => ss.Set().Where(c => c.FirstName.MaybeScalar(x => c.LastName.MaybeScalar(xx => x.StartsWith(xx))) == true));
}
[ConditionalTheory]
@@ -283,7 +302,7 @@ public virtual Task String_starts_with_on_argument_with_wildcard_column(bool asy
.Where(r => r.fn.StartsWith(r.ln)),
ss => ss.Set().Select(c => c.FirstName)
.SelectMany(c => ss.Set().Select(c2 => c2.LastName), (fn, ln) => new { fn, ln })
- .Where(r => r.ln == "" || r.fn.StartsWith(r.ln)),
+ .Where(r => r.fn.MaybeScalar(x => r.ln.MaybeScalar(xx => x.StartsWith(xx))) == true),
elementSorter: e => (e.fn, e.ln),
elementAsserter: (e, a) =>
{
@@ -301,7 +320,7 @@ public virtual Task String_starts_with_on_argument_with_wildcard_column_negated(
.Where(r => !r.fn.StartsWith(r.ln)),
ss => ss.Set().Select(c => c.FirstName)
.SelectMany(c => ss.Set().Select(c2 => c2.LastName), (fn, ln) => new { fn, ln })
- .Where(r => r.ln != "" && !r.fn.MaybeScalar(x => r.ln.MaybeScalar(xx => x.StartsWith(xx))) == true));
+ .Where(r => !(r.fn.MaybeScalar(x => r.ln.MaybeScalar(xx => x.StartsWith(xx))) == true)));
[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
@@ -309,11 +328,13 @@ public virtual async Task String_ends_with_on_argument_with_wildcard_constant(bo
{
await AssertQuery(
async,
- ss => ss.Set().Where(c => c.FirstName.EndsWith("%B")).Select(c => c.FirstName));
+ ss => ss.Set().Where(c => c.FirstName.EndsWith("%B")).Select(c => c.FirstName),
+ ss => ss.Set().Where(c => c.FirstName.MaybeScalar(x => x.EndsWith("%B")) == true).Select(c => c.FirstName));
await AssertQuery(
async,
- ss => ss.Set().Where(c => c.FirstName.EndsWith("a_")).Select(c => c.FirstName));
+ ss => ss.Set().Where(c => c.FirstName.EndsWith("a_")).Select(c => c.FirstName),
+ ss => ss.Set().Where(c => c.FirstName.MaybeScalar(x => x.EndsWith("a_")) == true).Select(c => c.FirstName));
await AssertQuery(
async,
@@ -323,27 +344,27 @@ await AssertQuery(
await AssertQuery(
async,
ss => ss.Set().Where(c => c.FirstName.EndsWith("")).Select(c => c.FirstName),
- ss => ss.Set().Select(c => c.FirstName));
+ ss => ss.Set().Where(c => c.FirstName != null).Select(c => c.FirstName));
await AssertQuery(
async,
- ss => ss.Set().Where(c => c.FirstName.EndsWith("_Ba_")).Select(c => c.FirstName));
+ ss => ss.Set().Where(c => c.FirstName.EndsWith("_Ba_")).Select(c => c.FirstName),
+ ss => ss.Set().Where(c => c.FirstName.MaybeScalar(x => x.EndsWith("_Ba_")) == true).Select(c => c.FirstName));
await AssertQuery(
async,
ss => ss.Set().Where(c => !c.FirstName.EndsWith("%B%a%r")).Select(c => c.FirstName),
- ss => ss.Set().Where(c => !c.FirstName.MaybeScalar(x => x.EndsWith("%B%a%r")) == true)
- .Select(c => c.FirstName));
+ ss => ss.Set().Where(c => c.FirstName.MaybeScalar(x => x.EndsWith("%B%a%r")) != true).Select(c => c.FirstName));
await AssertQuery(
async,
ss => ss.Set().Where(c => !c.FirstName.EndsWith("")).Select(c => c.FirstName),
- ss => ss.Set().Where(c => !c.FirstName.MaybeScalar(x => x.EndsWith("")) == true).Select(c => c.FirstName));
+ ss => ss.Set().Where(c => c.FirstName.MaybeScalar(x => x.EndsWith("")) != true).Select(c => c.FirstName));
await AssertQuery(
async,
ss => ss.Set().Where(c => !c.FirstName.EndsWith(null)).Select(c => c.FirstName),
- ss => ss.Set().Where(c => false).Select(c => c.FirstName));
+ ss => ss.Set().Where(c => true).Select(c => c.FirstName));
}
[ConditionalTheory]
@@ -353,12 +374,14 @@ public virtual async Task String_ends_with_on_argument_with_wildcard_parameter(b
var prm1 = "%B";
await AssertQuery(
async,
- ss => ss.Set().Where(c => c.FirstName.EndsWith(prm1)).Select(c => c.FirstName));
+ ss => ss.Set().Where(c => c.FirstName.EndsWith(prm1)).Select(c => c.FirstName),
+ ss => ss.Set().Where(c => c.FirstName.MaybeScalar(x => x.EndsWith(prm1)) == true).Select(c => c.FirstName));
var prm2 = "a_";
await AssertQuery(
async,
- ss => ss.Set().Where(c => c.FirstName.EndsWith(prm2)).Select(c => c.FirstName));
+ ss => ss.Set().Where(c => c.FirstName.EndsWith(prm2)).Select(c => c.FirstName),
+ ss => ss.Set().Where(c => c.FirstName.MaybeScalar(x => x.EndsWith(prm2)) == true).Select(c => c.FirstName));
var prm3 = (string)null;
await AssertQuery(
@@ -370,30 +393,31 @@ await AssertQuery(
await AssertQuery(
async,
ss => ss.Set().Where(c => c.FirstName.EndsWith(prm4)).Select(c => c.FirstName),
- ss => ss.Set().Select(c => c.FirstName));
+ ss => ss.Set().Where(c => c.FirstName.MaybeScalar(x => x.EndsWith(prm4)) == true).Select(c => c.FirstName));
var prm5 = "_Ba_";
await AssertQuery(
async,
- ss => ss.Set().Where(c => c.FirstName.EndsWith(prm5)).Select(c => c.FirstName));
+ ss => ss.Set().Where(c => c.FirstName.EndsWith(prm5)).Select(c => c.FirstName),
+ ss => ss.Set().Where(c => c.FirstName.MaybeScalar(x => x.EndsWith(prm5)) == true).Select(c => c.FirstName));
var prm6 = "%B%a%r";
await AssertQuery(
async,
ss => ss.Set().Where(c => !c.FirstName.EndsWith(prm6)).Select(c => c.FirstName),
- ss => ss.Set().Where(c => !c.FirstName.MaybeScalar(x => x.EndsWith(prm6)) == true).Select(c => c.FirstName));
+ ss => ss.Set().Where(c => c.FirstName.MaybeScalar(x => x.EndsWith(prm6)) != true).Select(c => c.FirstName));
var prm7 = "";
await AssertQuery(
async,
ss => ss.Set().Where(c => !c.FirstName.EndsWith(prm7)).Select(c => c.FirstName),
- ss => ss.Set().Where(c => false).Select(c => c.FirstName));
+ ss => ss.Set().Where(c => c.FirstName == null).Select(c => c.FirstName));
var prm8 = (string)null;
await AssertQuery(
async,
ss => ss.Set().Where(c => !c.FirstName.EndsWith(prm8)).Select(c => c.FirstName),
- ss => ss.Set().Where(c => false).Select(c => c.FirstName));
+ ss => ss.Set().Where(c => true).Select(c => c.FirstName));
}
[ConditionalTheory]
@@ -406,7 +430,7 @@ public virtual Task String_ends_with_on_argument_with_wildcard_column(bool async
.Where(r => r.fn.EndsWith(r.ln)),
ss => ss.Set().Select(c => c.FirstName)
.SelectMany(c => ss.Set().Select(c2 => c2.LastName), (fn, ln) => new { fn, ln })
- .Where(r => r.ln == "" || r.fn.EndsWith(r.ln)),
+ .Where(r => r.fn.MaybeScalar(x => r.ln.MaybeScalar(xx => x.EndsWith(xx))) == true),
elementSorter: e => (e.fn, e.ln),
elementAsserter: (e, a) =>
{
@@ -424,7 +448,7 @@ public virtual Task String_ends_with_on_argument_with_wildcard_column_negated(bo
.Where(r => !r.fn.EndsWith(r.ln)),
ss => ss.Set().Select(c => c.FirstName)
.SelectMany(c => ss.Set().Select(c2 => c2.LastName), (fn, ln) => new { fn, ln })
- .Where(r => r.ln != "" && !r.fn.MaybeScalar(x => r.ln.MaybeScalar(xx => x.EndsWith(xx))) == true));
+ .Where(r => !(r.fn.MaybeScalar(x => r.ln.MaybeScalar(xx => x.EndsWith(xx))) == true)));
[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
@@ -436,7 +460,7 @@ public virtual Task String_ends_with_inside_conditional(bool async)
.Where(r => r.fn.EndsWith(r.ln) ? true : false),
ss => ss.Set().Select(c => c.FirstName)
.SelectMany(c => ss.Set().Select(c2 => c2.LastName), (fn, ln) => new { fn, ln })
- .Where(r => r.ln == "" || r.fn.EndsWith(r.ln) ? true : false),
+ .Where(r => r.fn.MaybeScalar(x => r.ln.MaybeScalar(xx => x.EndsWith(xx))) == true),
elementSorter: e => (e.fn, e.ln),
elementAsserter: (e, a) =>
{
@@ -455,7 +479,7 @@ public virtual Task String_ends_with_inside_conditional_negated(bool async)
ss => ss.Set().Select(c => c.FirstName)
.SelectMany(c => ss.Set().Select(c2 => c2.LastName), (fn, ln) => new { fn, ln })
.Where(
- r => r.ln != "" && !r.fn.MaybeScalar(x => r.ln.MaybeScalar(xx => x.EndsWith(xx))) == true
+ r => !(r.fn.MaybeScalar(x => r.ln.MaybeScalar(xx => x.EndsWith(xx))) == true)
? true
: false));
@@ -466,6 +490,8 @@ public virtual Task String_ends_with_equals_nullable_column(bool async)
async,
ss => ss.Set().SelectMany(c => ss.Set(), (c1, c2) => new { c1, c2 })
.Where(r => r.c1.FirstName.EndsWith(r.c2.LastName) == r.c1.NullableBool.Value),
+ ss => ss.Set().SelectMany(c => ss.Set(), (c1, c2) => new { c1, c2 })
+ .Where(r => (r.c1.FirstName != null && r.c2.LastName != null && r.c1.FirstName.EndsWith(r.c2.LastName)) == r.c1.NullableBool),
elementSorter: e => (e.c1.Id, e.c2.Id),
elementAsserter: (e, a) =>
{
@@ -480,6 +506,8 @@ public virtual Task String_ends_with_not_equals_nullable_column(bool async)
async,
ss => ss.Set().SelectMany(c => ss.Set(), (c1, c2) => new { c1, c2 })
.Where(r => r.c1.FirstName.EndsWith(r.c2.LastName) != r.c1.NullableBool.Value),
+ ss => ss.Set().SelectMany(c => ss.Set(), (c1, c2) => new { c1, c2 })
+ .Where(r => (r.c1.FirstName != null && r.c2.LastName != null && r.c1.FirstName.EndsWith(r.c2.LastName)) != r.c1.NullableBool),
elementSorter: e => (e.c1.Id, e.c2.Id),
elementAsserter: (e, a) =>
{
diff --git a/test/EFCore.Specification.Tests/Query/NorthwindFunctionsQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/NorthwindFunctionsQueryTestBase.cs
index 4a514b97812..ad7957ec1fd 100644
--- a/test/EFCore.Specification.Tests/Query/NorthwindFunctionsQueryTestBase.cs
+++ b/test/EFCore.Specification.Tests/Query/NorthwindFunctionsQueryTestBase.cs
@@ -40,6 +40,18 @@ public virtual Task String_StartsWith_Literal(bool async)
ss => ss.Set().Where(c => c.ContactName.StartsWith("M")),
entryCount: 12);
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual Task String_StartsWith_Parameter(bool async)
+ {
+ var pattern = "M";
+
+ return AssertQuery(
+ async,
+ ss => ss.Set().Where(c => c.ContactName.StartsWith(pattern)),
+ entryCount: 12);
+ }
+
[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
public virtual Task String_StartsWith_Identity(bool async)
@@ -72,6 +84,18 @@ public virtual Task String_EndsWith_Literal(bool async)
ss => ss.Set().Where(c => c.ContactName.EndsWith("b")),
entryCount: 1);
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual Task String_EndsWith_Parameter(bool async)
+ {
+ var pattern = "b";
+
+ return AssertQuery(
+ async,
+ ss => ss.Set().Where(c => c.ContactName.EndsWith(pattern)),
+ entryCount: 1);
+ }
+
[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
public virtual Task String_EndsWith_Identity(bool async)
diff --git a/test/EFCore.Specification.Tests/TestUtilities/ExpectedQueryRewritingVisitor.cs b/test/EFCore.Specification.Tests/TestUtilities/ExpectedQueryRewritingVisitor.cs
index 29b9aa43db5..b091a5a1d63 100644
--- a/test/EFCore.Specification.Tests/TestUtilities/ExpectedQueryRewritingVisitor.cs
+++ b/test/EFCore.Specification.Tests/TestUtilities/ExpectedQueryRewritingVisitor.cs
@@ -11,15 +11,6 @@ private static readonly MethodInfo _maybeDefaultIfEmpty
private static readonly MethodInfo _maybeMethod
= typeof(TestExtensions).GetMethod(nameof(TestExtensions.Maybe));
- private static readonly MethodInfo _containsMethodInfo
- = typeof(string).GetRuntimeMethod(nameof(string.Contains), new[] { typeof(string) });
-
- private static readonly MethodInfo _startsWithMethodInfo
- = typeof(string).GetRuntimeMethod(nameof(string.StartsWith), new[] { typeof(string) });
-
- private static readonly MethodInfo _endsWithMethodInfo
- = typeof(string).GetRuntimeMethod(nameof(string.EndsWith), new[] { typeof(string) });
-
private static readonly MethodInfo _getShadowPropertyValueMethodInfo
= typeof(ExpectedQueryRewritingVisitor).GetMethod(nameof(GetShadowPropertyValue));
@@ -95,14 +86,6 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp
return Visit(rewritten);
}
- if (!_negated
- && (methodCallExpression.Method == _containsMethodInfo
- || methodCallExpression.Method == _startsWithMethodInfo
- || methodCallExpression.Method == _endsWithMethodInfo))
- {
- return RewriteStartsWithEndsWithContains(methodCallExpression);
- }
-
if (methodCallExpression.Method.IsGenericMethod
&& methodCallExpression.Method.GetGenericMethodDefinition() == EnumerableMethods.DefaultIfEmptyWithoutArgument)
{
@@ -187,41 +170,6 @@ private Expression RewriteJoinGroupJoin(MethodCallExpression methodCallExpressio
resultSelector);
}
- private Expression RewriteStartsWithEndsWithContains(MethodCallExpression methodCallExpression)
- {
- // c.FirstName.StartsWith(c.Nickname)
- // gets converted to:
- // c.Maybe(x => x.FirstName).MaybeScalar(x => c.Maybe(xx => xx.Nickname).MaybeScalar(xx => x.StartsWith(xx)))
- var caller = Visit(methodCallExpression.Object);
- var argument = Visit(methodCallExpression.Arguments[0]);
- var outerMaybeScalarMethod = _maybeScalarNullableMethod.MakeGenericMethod(typeof(string), typeof(bool));
- var innerMaybeScalarMethod = _maybeScalarNonNullableMethod.MakeGenericMethod(typeof(string), typeof(bool));
-
- var outerMaybeScalarLambdaParameter = Expression.Parameter(typeof(string), "x");
- var innerMaybeScalarLambdaParameter = Expression.Parameter(typeof(string), "xx");
- var innerMaybeScalarLambda = Expression.Lambda(
- methodCallExpression.Update(
- outerMaybeScalarLambdaParameter,
- new[] { innerMaybeScalarLambdaParameter }),
- innerMaybeScalarLambdaParameter);
-
- var innerMaybeScalar = Expression.Call(
- innerMaybeScalarMethod,
- argument,
- innerMaybeScalarLambda);
-
- var outerMaybeScalarLambda = Expression.Lambda(
- innerMaybeScalar,
- outerMaybeScalarLambdaParameter);
-
- var outerMaybeScalar = Expression.Call(
- outerMaybeScalarMethod,
- caller,
- outerMaybeScalarLambda);
-
- return Expression.Equal(outerMaybeScalar, Expression.Constant(true, typeof(bool?)));
- }
-
public static TResult GetShadowPropertyValue(TEntity entity, Func