diff --git a/src/EFCore.Relational/Query/RelationalEvaluatableExpressionFilter.cs b/src/EFCore.Relational/Query/RelationalEvaluatableExpressionFilter.cs index 9f7397a7b88..c2027496d3b 100644 --- a/src/EFCore.Relational/Query/RelationalEvaluatableExpressionFilter.cs +++ b/src/EFCore.Relational/Query/RelationalEvaluatableExpressionFilter.cs @@ -58,13 +58,22 @@ public override bool IsEvaluatableExpression(Expression expression, IModel model Check.NotNull(expression, nameof(expression)); Check.NotNull(model, nameof(model)); - if (expression is MethodCallExpression methodCallExpression - && model.FindDbFunction(methodCallExpression.Method) != null) + if (expression is MethodCallExpression methodCallExpression) { - // Never evaluate DbFunction - // If it is inside lambda then we will have whole method call - // If it is outside of lambda then it will be evaluated for table valued function already. - return false; + var method = methodCallExpression.Method; + + if (model.FindDbFunction(method) != null) + { + // Never evaluate DbFunction + // If it is inside lambda then we will have whole method call + // If it is outside of lambda then it will be evaluated for table valued function already. + return false; + } + + if (method.DeclaringType == typeof(RelationalDbFunctionsExtensions)) + { + return false; + } } return base.IsEvaluatableExpression(expression, model); diff --git a/src/EFCore.SqlServer/Extensions/SqlServerServiceCollectionExtensions.cs b/src/EFCore.SqlServer/Extensions/SqlServerServiceCollectionExtensions.cs index 60741eece1b..b23face2ef7 100644 --- a/src/EFCore.SqlServer/Extensions/SqlServerServiceCollectionExtensions.cs +++ b/src/EFCore.SqlServer/Extensions/SqlServerServiceCollectionExtensions.cs @@ -64,6 +64,7 @@ public static IServiceCollection AddEntityFrameworkSqlServer([NotNull] this ISer .TryAdd() .TryAdd() .TryAdd(p => p.GetService()) + .TryAdd() .TryAdd() .TryAdd() .TryAdd() diff --git a/src/EFCore.SqlServer/Query/Internal/SqlServerEvaluatableExpressionFilter.cs b/src/EFCore.SqlServer/Query/Internal/SqlServerEvaluatableExpressionFilter.cs new file mode 100644 index 00000000000..ac99c8d4970 --- /dev/null +++ b/src/EFCore.SqlServer/Query/Internal/SqlServerEvaluatableExpressionFilter.cs @@ -0,0 +1,47 @@ +// 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.Linq.Expressions; +using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Query; + +namespace Microsoft.EntityFrameworkCore.SqlServer.Query.Internal +{ + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public class SqlServerEvaluatableExpressionFilter : RelationalEvaluatableExpressionFilter + { + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public SqlServerEvaluatableExpressionFilter([NotNull] EvaluatableExpressionFilterDependencies dependencies, [NotNull] RelationalEvaluatableExpressionFilterDependencies relationalDependencies) + : base(dependencies, relationalDependencies) + { + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public override bool IsEvaluatableExpression(Expression expression, IModel model) + { + if (expression is MethodCallExpression methodCallExpression + && methodCallExpression.Method.DeclaringType == typeof(SqlServerDbFunctionsExtensions)) + { + return false; + } + + return base.IsEvaluatableExpression(expression, model); + } + } +} diff --git a/src/EFCore/Query/EvaluatableExpressionFilter.cs b/src/EFCore/Query/EvaluatableExpressionFilter.cs index 848a4e4bb84..34eaffa5b7d 100644 --- a/src/EFCore/Query/EvaluatableExpressionFilter.cs +++ b/src/EFCore/Query/EvaluatableExpressionFilter.cs @@ -2,6 +2,7 @@ // 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.Expressions; using System.Reflection; using JetBrains.Annotations; @@ -108,7 +109,8 @@ public virtual bool IsEvaluatableExpression(Expression expression, IModel model) if (Equals(method, _guidNewGuid) || Equals(method, _randomNextNoArgs) || Equals(method, _randomNextOneArg) - || Equals(method, _randomNextTwoArgs)) + || Equals(method, _randomNextTwoArgs) + || method.DeclaringType == typeof(DbFunctionsExtensions)) { return false; } diff --git a/test/EFCore.Relational.Specification.Tests/Query/RelationalNorthwindDbFunctionsQueryTestBase.cs b/test/EFCore.Relational.Specification.Tests/Query/RelationalNorthwindDbFunctionsQueryTestBase.cs index c28bc347e03..39954b9cfd0 100644 --- a/test/EFCore.Relational.Specification.Tests/Query/RelationalNorthwindDbFunctionsQueryTestBase.cs +++ b/test/EFCore.Relational.Specification.Tests/Query/RelationalNorthwindDbFunctionsQueryTestBase.cs @@ -44,6 +44,16 @@ public virtual Task Collate_case_sensitive(bool async) c => EF.Functions.Collate(c.ContactName, CaseSensitiveCollation) == "maria anders", c => c.ContactName.Equals("maria anders", StringComparison.Ordinal)); + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Collate_case_sensitive_constant(bool async) + => AssertCount( + async, + ss => ss.Set(), + ss => ss.Set(), + c => c.ContactName == EF.Functions.Collate("maria anders", CaseSensitiveCollation), + c => c.ContactName.Equals("maria anders", StringComparison.Ordinal)); + protected abstract string CaseInsensitiveCollation { get; } protected abstract string CaseSensitiveCollation { get; } } diff --git a/test/EFCore.Specification.Tests/Query/NorthwindDbFunctionsQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/NorthwindDbFunctionsQueryTestBase.cs index ad8d8afbcc3..59a08a8ad25 100644 --- a/test/EFCore.Specification.Tests/Query/NorthwindDbFunctionsQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/NorthwindDbFunctionsQueryTestBase.cs @@ -47,6 +47,26 @@ public virtual Task Like_literal_with_escape(bool async) c => EF.Functions.Like(c.ContactName, "!%", "!"), c => c.ContactName.Contains("%")); + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Like_all_literals(bool async) + => AssertCount( + async, + ss => ss.Set(), + ss => ss.Set(), + c => EF.Functions.Like("FOO", "%O%"), + c => "FOO".Contains("O") || "FOO".Contains("m")); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Like_all_literals_with_escape(bool async) + => AssertCount( + async, + ss => ss.Set(), + ss => ss.Set(), + c => EF.Functions.Like("%", "!%", "!"), + c => "%".Contains("%")); + protected NorthwindContext CreateContext() => Fixture.CreateContext(); } diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindDbFunctionsQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindDbFunctionsQuerySqlServerTest.cs index 45b802ba9a2..3564af61f71 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindDbFunctionsQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindDbFunctionsQuerySqlServerTest.cs @@ -56,6 +56,26 @@ FROM [Customers] AS [c] WHERE [c].[ContactName] LIKE N'!%' ESCAPE N'!'"); } + public override async Task Like_all_literals(bool async) + { + await base.Like_all_literals(async); + + AssertSql( + @"SELECT COUNT(*) +FROM [Customers] AS [c] +WHERE N'FOO' LIKE N'%O%'"); + } + + public override async Task Like_all_literals_with_escape(bool async) + { + await base.Like_all_literals_with_escape(async); + + AssertSql( + @"SELECT COUNT(*) +FROM [Customers] AS [c] +WHERE N'%' LIKE N'!%' ESCAPE N'!'"); + } + public override async Task Collate_case_insensitive(bool async) { await base.Collate_case_insensitive(async); @@ -76,6 +96,16 @@ FROM [Customers] AS [c] WHERE [c].[ContactName] COLLATE Latin1_General_CS_AS = N'maria anders'"); } + public override async Task Collate_case_sensitive_constant(bool async) + { + await base.Collate_case_sensitive_constant(async); + + AssertSql( + @"SELECT COUNT(*) +FROM [Customers] AS [c] +WHERE [c].[ContactName] = N'maria anders' COLLATE Latin1_General_CS_AS"); + } + protected override string CaseInsensitiveCollation => "Latin1_General_CI_AI"; @@ -1102,6 +1132,23 @@ FROM [Orders] AS [o] } } + [ConditionalFact] + public virtual void DataLength_all_constants() + { + using (var context = CreateContext()) + { + var count = context.Orders + .Count(c => EF.Functions.DataLength("foo") == 3); + + Assert.Equal(0, count); + + AssertSql( + @"SELECT COUNT(*) +FROM [Orders] AS [o] +WHERE CAST(DATALENGTH(N'foo') AS int) = 3"); + } + } + private void AssertSql(params string[] expected) => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); }