Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Query: Remove client implementation for EF.Functions.Like #20311

Merged
merged 2 commits into from
Mar 17, 2020
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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