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

Add EF.Functions and SQL Like #7611

Merged
merged 2 commits into from
Mar 27, 2017
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
@@ -0,0 +1,51 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore.Specification.Tests;
using Microsoft.EntityFrameworkCore.Specification.Tests.TestModels.Northwind;
using Microsoft.EntityFrameworkCore.Specification.Tests.TestUtilities.Xunit;
using Xunit;

namespace Microsoft.EntityFrameworkCore.Relational.Specification.Tests
{
public abstract class RelationalQueryTestBase<TFixture> : QueryTestBase<TFixture>
where TFixture : NorthwindQueryFixtureBase, new()
{
[ConditionalFact]
public virtual void String_Like_Literal()
{
using (var context = CreateContext())
{
var count = context.Customers.Count(c => EF.Functions.Like(c.ContactName, "%M%"));
Assert.Equal(19, count);
}
}

[ConditionalFact]
public virtual void String_Like_Identity()
{
using (var context = CreateContext())
{
var count = context.Customers.Count(c => EF.Functions.Like(c.ContactName, c.ContactName));
Assert.Equal(91, count);
}
}

[ConditionalFact]
public virtual void String_Like_Literal_With_Escape()
{
using (var context = CreateContext())
{
var count = context.Customers.Count(c => EF.Functions.Like(c.ContactName, "!%", '!'));
Assert.Equal(0, count);
}
}

protected RelationalQueryTestBase(TFixture fixture) : base(fixture) {}
}
}

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

3 changes: 3 additions & 0 deletions src/EFCore.Relational/Properties/RelationalStrings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -307,4 +307,7 @@
<data name="UnsupportedPropertyType" xml:space="preserve">
<value>No mapping to a relational type can be found for property '{entity}.{property}' with the CLR type '{clrType}'.</value>
</data>
<data name="DbFunctionsDirectCall" xml:space="preserve">
<value>This function can only be invoked from LINQ to Entities.</value>
</data>
</root>
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore.Query.Expressions;
using Microsoft.EntityFrameworkCore.Utilities;

namespace Microsoft.EntityFrameworkCore.Query.ExpressionTranslators.Internal
{
/// <summary>
/// This API supports the Entity Framework Core infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public class LikeTranslator : IMethodCallTranslator
{
private static readonly MethodInfo _methodInfo
= typeof(RelationalDbFunctionsExtensions).GetRuntimeMethod(nameof(RelationalDbFunctionsExtensions.Like),
new[] { typeof(DbFunctions), typeof(string), typeof(string) });

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

/// <summary>
/// This API supports the Entity Framework Core infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public virtual Expression Translate(MethodCallExpression methodCallExpression)
{
Check.NotNull(methodCallExpression, nameof(methodCallExpression));

if (Equals(methodCallExpression.Method, _methodInfo))
{
return new LikeExpression(methodCallExpression.Arguments[1], methodCallExpression.Arguments[2]);
}

if (Equals(methodCallExpression.Method, _methodInfoWithEscape))
{
return new LikeExpression(methodCallExpression.Arguments[1], methodCallExpression.Arguments[2], methodCallExpression.Arguments[3]);
}

return null;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ protected RelationalCompositeMethodCallTranslator(
{
new EnumHasFlagTranslator(),
new EqualsTranslator(dependencies.Logger),
new IsNullOrEmptyTranslator()
new IsNullOrEmptyTranslator(),
new LikeTranslator()
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -597,7 +597,9 @@ var arguments
= methodCallExpression.Arguments
.Where(e => !(e is QuerySourceReferenceExpression)
&& !(e is SubQueryExpression))
.Select(e => (e as ConstantExpression)?.Value is Array ? e : Visit(e))
.Select(e => (e as ConstantExpression)?.Value is Array || e.Type == typeof(DbFunctions)
? e
: Visit(e))
.Where(e => e != null)
.ToArray();

Expand Down
31 changes: 29 additions & 2 deletions src/EFCore.Relational/Query/Expressions/LikeExpression.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,22 @@ public LikeExpression([NotNull] Expression match, [NotNull] Expression pattern)
Pattern = pattern;
}

/// <summary>
/// Creates a new instance of LikeExpression.
/// </summary>
/// <param name="match"> The expression to match. </param>
/// <param name="pattern"> The pattern to match. </param>
/// <param name="escapeChar"> The escape character to use in <paramref name="pattern"/>. </param>
public LikeExpression([NotNull] Expression match, [NotNull] Expression pattern, [CanBeNull] Expression escapeChar)
{
Check.NotNull(match, nameof(match));
Check.NotNull(pattern, nameof(pattern));

Match = match;
Pattern = pattern;
EscapeChar = escapeChar;
}

/// <summary>
/// Gets the match expression.
/// </summary>
Expand All @@ -44,6 +60,15 @@ public LikeExpression([NotNull] Expression match, [NotNull] Expression pattern)
/// </value>
public virtual Expression Pattern { get; }

/// <summary>
/// Gets the escape character to use in <see cref="Pattern"/>.
/// </summary>
/// <value>
/// The escape character to use. If null, no escape character is used.
/// </value>
[CanBeNull]
public virtual Expression EscapeChar { get; }

/// <summary>
/// Returns the node type of this <see cref="Expression" />. (Inherited from <see cref="Expression" />.)
/// </summary>
Expand Down Expand Up @@ -87,17 +112,19 @@ protected override Expression VisitChildren(ExpressionVisitor visitor)
{
var newMatchExpression = visitor.Visit(Match);
var newPatternExpression = visitor.Visit(Pattern);
var newEscapeCharExpression = EscapeChar == null ? null : visitor.Visit(EscapeChar);

return (newMatchExpression != Match)
|| (newPatternExpression != Pattern)
? new LikeExpression(newMatchExpression, newPatternExpression)
|| (newEscapeCharExpression != EscapeChar)
? new LikeExpression(newMatchExpression, newPatternExpression, newEscapeCharExpression)
: this;
}

/// <summary>
/// Creates a <see cref="string" /> representation of the Expression.
/// </summary>
/// <returns>A <see cref="string" /> representation of the Expression.</returns>
public override string ToString() => Match + " LIKE " + Pattern;
public override string ToString() => $"{Match} LIKE {Pattern}{(EscapeChar == null ? "" : $" ESCAPE {EscapeChar}")}";
}
}
6 changes: 6 additions & 0 deletions src/EFCore.Relational/Query/Sql/DefaultQuerySqlGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1243,6 +1243,12 @@ public virtual Expression VisitLike(LikeExpression likeExpression)

Visit(likeExpression.Pattern);

if (likeExpression.EscapeChar != null)
{
_relationalCommandBuilder.Append(" ESCAPE ");
Visit(likeExpression.EscapeChar);
}

_typeMapping = parentTypeMapping;

return likeExpression;
Expand Down
51 changes: 51 additions & 0 deletions src/EFCore.Relational/RelationalDbFunctionsExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore.Internal;
using JetBrains.Annotations;

namespace Microsoft.EntityFrameworkCore
{
/// <summary>
/// Provides CLR methods that get translated to database functions when used in LINQ to Entities queries.
/// The methods on this class are accessed via <see cref="EF.Functions"/>.
/// </summary>
// ReSharper disable once InconsistentNaming
public static class RelationalDbFunctionsExtensions
{
/// <summary>
/// Indicates whether the specified input string matches the given pattern by sending an SQL LIKE
/// expression to the database.
/// </summary>
/// <param name="functions">Should always be <see cref="EF.Functions"/>.</param>
/// <param name="input">The string to search for a match.</param>
/// <param name="pattern">The regular expression pattern to match.</param>
/// <returns><string>true</string> if the LIKE expression finds a match; otherwise, <string>false</string>.</returns>
public static bool Like(
[NotNull] this DbFunctions functions,
[NotNull] string input,
[NotNull] string pattern)
=> throw new NotSupportedException(RelationalStrings.DbFunctionsDirectCall);

/// <summary>
/// Indicates whether the specified input string matches the given pattern by sending an SQL LIKE
/// expression to the database.
/// </summary>
/// <param name="functions">Should always be <see cref="EF.Functions"/>.</param>
/// <param name="input">The string to search for a match.</param>
/// <param name="pattern">The regular expression pattern to match.</param>
/// <param name="escapeChar">Character to use as an escape character in <paramref name="pattern"/>.</param>
/// <returns><string>true</string> if the LIKE expression finds a match; otherwise, <string>false</string>.</returns>
public static bool Like(
[NotNull] this DbFunctions functions,
[NotNull] string input,
[NotNull] string pattern,
char escapeChar)
=> throw new NotSupportedException(RelationalStrings.DbFunctionsDirectCall);
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using JetBrains.Annotations;

namespace Microsoft.EntityFrameworkCore.Query.ExpressionTranslators.Internal
Expand Down
15 changes: 15 additions & 0 deletions src/EFCore/DbFunctions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

namespace Microsoft.EntityFrameworkCore
{
/// <summary>
/// Provides CLR methods that get translated to database functions when used in LINQ to Entities queries.
/// The methods on this class are accessed via <see cref="EF.Functions"/>.
/// </summary>
// ReSharper disable once InconsistentNaming
public class DbFunctions
{
internal DbFunctions() { }
}
}
6 changes: 6 additions & 0 deletions src/EFCore/EF.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,5 +41,11 @@ public static TProperty Property<TProperty>(
{
throw new InvalidOperationException(CoreStrings.PropertyMethodInvoked);
}

/// <summary>
/// Provides CLR methods that get translated to database functions when used in LINQ to Entities queries.
/// Calling these methods in other contexts (e.g. LINQ to Objects) will throw a <see cref="NotSupportedException"/>.
/// </summary>
public static DbFunctions Functions { get; } = new DbFunctions();
}
}
42 changes: 40 additions & 2 deletions test/EFCore.SqlServer.FunctionalTests/QuerySqlServerTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
using Microsoft.EntityFrameworkCore.Specification.Tests;
using Microsoft.EntityFrameworkCore.Specification.Tests.TestModels.Northwind;
using Microsoft.EntityFrameworkCore.Specification.Tests.TestUtilities.Xunit;
using Microsoft.EntityFrameworkCore.Relational.Specification.Tests;
using Microsoft.EntityFrameworkCore.SqlServer.FunctionalTests.Utilities;
using Xunit;
using Xunit.Abstractions;
Expand All @@ -22,7 +23,7 @@

namespace Microsoft.EntityFrameworkCore.SqlServer.FunctionalTests
{
public class QuerySqlServerTest : QueryTestBase<NorthwindQuerySqlServerFixture>
public class QuerySqlServerTest : RelationalQueryTestBase<NorthwindQuerySqlServerFixture>
{
public QuerySqlServerTest(NorthwindQuerySqlServerFixture fixture, ITestOutputHelper testOutputHelper)
: base(fixture)
Expand Down Expand Up @@ -4843,6 +4844,43 @@ FROM [Customers] AS [c]
Sql);
}

public override void String_Like_Literal()
{
using (var context = CreateContext())
{
var count = context.Customers.Count(c => EF.Functions.Like(c.ContactName, "%M%"));
Assert.Equal(34, count); // case-insensitive
}

Assert.Equal(
@"SELECT COUNT(*)
FROM [Customers] AS [c]
WHERE [c].[ContactName] LIKE N'%M%'",
Sql);
}

public override void String_Like_Identity()
{
base.String_Like_Identity();

Assert.Equal(
@"SELECT COUNT(*)
FROM [Customers] AS [c]
WHERE [c].[ContactName] LIKE [c].[ContactName]",
Sql);
}

public override void String_Like_Literal_With_Escape()
{
base.String_Like_Literal_With_Escape();

Assert.Equal(
@"SELECT COUNT(*)
FROM [Customers] AS [c]
WHERE [c].[ContactName] LIKE N'!%' ESCAPE '!'",
Sql);
}

public override void String_Compare_simple_zero()
{
base.String_Compare_simple_zero();
Expand Down Expand Up @@ -7398,4 +7436,4 @@ THEN CAST(1 AS BIT) ELSE CAST(0 AS BIT)

private static string Sql => TestSqlLoggerFactory.Sql.Replace(Environment.NewLine, FileLineEnding);
}
}
}
Loading