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 all commits
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);
}
}
}
Loading