Skip to content

Commit

Permalink
Merge branch 'ef-functions' of https://github.com/roji/EntityFramework
Browse files Browse the repository at this point in the history
…into roji-ef-functions
  • Loading branch information
anpete committed Mar 27, 2017
2 parents 24d6eb8 + 834641e commit 936014b
Show file tree
Hide file tree
Showing 14 changed files with 279 additions and 7 deletions.
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

0 comments on commit 936014b

Please sign in to comment.