Skip to content

Commit

Permalink
Query: Remove client implementation for EF.Functions.Like
Browse files Browse the repository at this point in the history
InMemory translates it using an provider specific method now

Resolves #20293
  • Loading branch information
smitpatel committed Mar 17, 2020
1 parent bca464e commit 47d1ebe
Show file tree
Hide file tree
Showing 11 changed files with 223 additions and 298 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Text;
using System.Text.RegularExpressions;
using JetBrains.Annotations;
using Microsoft.EntityFrameworkCore.Diagnostics;
using Microsoft.EntityFrameworkCore.Infrastructure;
Expand All @@ -20,7 +22,33 @@ namespace Microsoft.EntityFrameworkCore.InMemory.Query.Internal
{
public class InMemoryExpressionTranslatingExpressionVisitor : ExpressionVisitor
{
private const string CompiledQueryParameterPrefix = "__";
private const string _compiledQueryParameterPrefix = "__";

private static readonly MethodInfo _likeMethodInfo
= typeof(DbFunctionsExtensions).GetRuntimeMethod(
nameof(DbFunctionsExtensions.Like),
new[] { typeof(DbFunctions), typeof(string), typeof(string) });

private static readonly MethodInfo _likeMethodInfoWithEscape
= typeof(DbFunctionsExtensions).GetRuntimeMethod(
nameof(DbFunctionsExtensions.Like),
new[] { typeof(DbFunctions), typeof(string), typeof(string), typeof(string) });

private static readonly MethodInfo _inMemoryLikeMethodInfo
= typeof(InMemoryExpressionTranslatingExpressionVisitor)
.GetTypeInfo().GetDeclaredMethod(nameof(InMemoryLike));

// Regex special chars defined here:
// https://msdn.microsoft.com/en-us/library/4edbef7e(v=vs.110).aspx
private static readonly char[] _regexSpecialChars
= { '.', '$', '^', '{', '[', '(', '|', ')', '*', '+', '?', '\\' };

private static readonly string _defaultEscapeRegexCharsPattern
= BuildEscapeRegexCharsPattern(_regexSpecialChars);

private static readonly TimeSpan _regexTimeout = TimeSpan.FromMilliseconds(value: 1000.0);
private static string BuildEscapeRegexCharsPattern(IEnumerable<char> regexSpecialChars)
=> string.Join("|", regexSpecialChars.Select(c => @"\" + c));

private readonly QueryableMethodTranslatingExpressionVisitor _queryableMethodTranslatingExpressionVisitor;
private readonly EntityProjectionFindingExpressionVisitor _entityProjectionFindingExpressionVisitor;
Expand Down Expand Up @@ -536,6 +564,27 @@ MethodInfo GetMethod()
replacedReadExpression));
}

if (methodCallExpression.Method == _likeMethodInfo
|| methodCallExpression.Method == _likeMethodInfoWithEscape)
{
// EF.Functions.Like
var visitedArguments = new Expression[3];
visitedArguments[2] = Expression.Constant(null, typeof(string));
// Skip first DbFunctions argument
for (var i = 1; i < methodCallExpression.Arguments.Count; i++)
{
var argument = Visit(methodCallExpression.Arguments[i]);
if (TranslationFailed(methodCallExpression.Arguments[i], argument))
{
return null;
}

visitedArguments[i - 1] = argument;
}

return Expression.Call(_inMemoryLikeMethodInfo, visitedArguments);
}

// MethodCall translators
var @object = Visit(methodCallExpression.Object);
if (TranslationFailed(methodCallExpression.Object, @object))
Expand Down Expand Up @@ -740,7 +789,7 @@ protected override Expression VisitParameter(ParameterExpression parameterExpres
{
Check.NotNull(parameterExpression, nameof(parameterExpression));

if (parameterExpression.Name.StartsWith(CompiledQueryParameterPrefix, StringComparison.Ordinal))
if (parameterExpression.Name.StartsWith(_compiledQueryParameterPrefix, StringComparison.Ordinal))
{
return Expression.Call(
_getParameterValueMethodInfo.MakeGenericMethod(parameterExpression.Type),
Expand Down Expand Up @@ -809,5 +858,86 @@ protected override Expression VisitUnary(UnaryExpression unaryExpression)
[DebuggerStepThrough]
private bool TranslationFailed(Expression original, Expression translation)
=> original != null && (translation == null || translation is EntityProjectionExpression);

private static bool InMemoryLike(string matchExpression, string pattern, string escapeCharacter)
{
//TODO: this fixes https://github.com/aspnet/EntityFramework/issues/8656 by insisting that
// the "escape character" is a string but just using the first character of that string,
// but we may later want to allow the complete string as the "escape character"
// in which case we need to change the way we construct the regex below.
var singleEscapeCharacter =
(escapeCharacter == null || escapeCharacter.Length == 0)
? (char?)null
: escapeCharacter.First();

if (matchExpression == null
|| pattern == null)
{
return false;
}

if (matchExpression.Equals(pattern, StringComparison.OrdinalIgnoreCase))
{
return true;
}

if (matchExpression.Length == 0
|| pattern.Length == 0)
{
return false;
}

var escapeRegexCharsPattern
= singleEscapeCharacter == null
? _defaultEscapeRegexCharsPattern
: BuildEscapeRegexCharsPattern(_regexSpecialChars.Where(c => c != singleEscapeCharacter));

var regexPattern
= Regex.Replace(
pattern,
escapeRegexCharsPattern,
c => @"\" + c,
default,
_regexTimeout);

var stringBuilder = new StringBuilder();

for (var i = 0; i < regexPattern.Length; i++)
{
var c = regexPattern[i];
var escaped = i > 0 && regexPattern[i - 1] == singleEscapeCharacter;

switch (c)
{
case '_':
{
stringBuilder.Append(escaped ? '_' : '.');
break;
}
case '%':
{
stringBuilder.Append(escaped ? "%" : ".*");
break;
}
default:
{
if (c != singleEscapeCharacter)
{
stringBuilder.Append(c);
}

break;
}
}
}

regexPattern = stringBuilder.ToString();

return Regex.IsMatch(
matchExpression,
@"\A" + regexPattern + @"\s*\z",
RegexOptions.IgnoreCase | RegexOptions.Singleline,
_regexTimeout);
}
}
}
23 changes: 10 additions & 13 deletions src/EFCore.SqlServer/Extensions/SqlServerDbFunctionsExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System;
using System.Globalization;
using JetBrains.Annotations;
using Microsoft.EntityFrameworkCore.Diagnostics;
using Microsoft.EntityFrameworkCore.SqlServer.Internal;

// ReSharper disable once CheckNamespace
Expand Down Expand Up @@ -54,9 +55,7 @@ public static bool FreeText(
=> FreeTextCore(propertyReference, freeText, null);

private static bool FreeTextCore(string propertyName, string freeText, int? languageTerm)
{
throw new InvalidOperationException(SqlServerStrings.FunctionOnClient(nameof(FreeText)));
}
=> throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(FreeText)));

/// <summary>
/// <para>
Expand Down Expand Up @@ -97,9 +96,7 @@ public static bool Contains(
=> ContainsCore(propertyReference, searchCondition, null);

private static bool ContainsCore(string propertyName, string searchCondition, int? languageTerm)
{
throw new InvalidOperationException(SqlServerStrings.FunctionOnClient(nameof(Contains)));
}
=> throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Contains)));

/// <summary>
/// Counts the number of year boundaries crossed between the startDate and endDate.
Expand Down Expand Up @@ -982,7 +979,7 @@ public static int DateDiffWeek(
public static bool IsDate(
[CanBeNull] this DbFunctions _,
[NotNull] string expression)
=> throw new InvalidOperationException(SqlServerStrings.FunctionOnClient(nameof(IsDate)));
=> throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(IsDate)));

/// <summary>
/// Initializes a new instance of the <see cref="DateTime" /> structure to the specified year, month, day, hour, minute, second, and millisecond.
Expand All @@ -1006,7 +1003,7 @@ public static DateTime DateTimeFromParts(
int minute,
int second,
int millisecond)
=> throw new InvalidOperationException(SqlServerStrings.FunctionOnClient(nameof(DateTimeFromParts)));
=> throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(DateTimeFromParts)));

/// <summary>
/// Initializes a new instance of the <see cref="DateTime" /> structure to the specified year, month, day.
Expand All @@ -1022,7 +1019,7 @@ public static DateTime DateFromParts(
int year,
int month,
int day)
=> throw new InvalidOperationException(SqlServerStrings.FunctionOnClient(nameof(DateFromParts)));
=> throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(DateFromParts)));

/// <summary>
/// Initializes a new instance of the <see cref="DateTime" /> structure to the specified year, month, day, hour, minute, second, fractions, and precision.
Expand All @@ -1048,7 +1045,7 @@ public static DateTime DateTime2FromParts(
int second,
int fractions,
int precision)
=> throw new InvalidOperationException(SqlServerStrings.FunctionOnClient(nameof(DateTime2FromParts)));
=> throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(DateTime2FromParts)));

/// <summary>
/// Initializes a new instance of the <see cref="DateTimeOffset" /> structure to the specified year, month, day, hour, minute, second, fractions, hourOffset, minuteOffset and precision.
Expand Down Expand Up @@ -1078,7 +1075,7 @@ public static DateTimeOffset DateTimeOffsetFromParts(
int hourOffset,
int minuteOffset,
int precision)
=> throw new InvalidOperationException(SqlServerStrings.FunctionOnClient(nameof(DateTimeOffsetFromParts)));
=> throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(DateTimeOffsetFromParts)));

/// <summary>
/// Initializes a new instance of the <see cref="DateTime" /> structure to the specified year, month, day, hour and minute.
Expand All @@ -1098,7 +1095,7 @@ public static DateTime SmallDateTimeFromParts(
int day,
int hour,
int minute)
=> throw new InvalidOperationException(SqlServerStrings.FunctionOnClient(nameof(SmallDateTimeFromParts)));
=> throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(SmallDateTimeFromParts)));

/// <summary>
/// Initializes a new instance of the <see cref="TimeSpan" /> structure to the specified hour, minute, second, fractions, and precision.
Expand All @@ -1118,6 +1115,6 @@ public static TimeSpan TimeFromParts(
int second,
int fractions,
int precision)
=> throw new InvalidOperationException(SqlServerStrings.FunctionOnClient(nameof(TimeFromParts)));
=> throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(TimeFromParts)));
}
}
8 changes: 0 additions & 8 deletions src/EFCore.SqlServer/Properties/SqlServerStrings.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 0 additions & 3 deletions src/EFCore.SqlServer/Properties/SqlServerStrings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -238,9 +238,6 @@
<data name="DuplicateKeyMismatchedClustering" xml:space="preserve">
<value>The keys {key1} on '{entityType1}' and {key2} on '{entityType2}' are both mapped to '{table}.{keyName}' but with different clustering.</value>
</data>
<data name="FunctionOnClient" xml:space="preserve">
<value>The '{methodName}' method is not supported because the query has switched to client-evaluation. Inspect the log to determine which query expressions are triggering client-evaluation.</value>
</data>
<data name="LogConflictingValueGenerationStrategies" xml:space="preserve">
<value>Both the SqlServerValueGenerationStrategy {generationStrategy} and {otherGenerationStrategy} have been set on property '{propertyName}' on entity type '{entityName}'. Usually this is a mistake. Only use these at the same time if you are sure you understand the consequences.</value>
<comment>Warning SqlServerEventId.ConflictingValueGenerationStrategiesWarning string string string string</comment>
Expand Down
Loading

0 comments on commit 47d1ebe

Please sign in to comment.