diff --git a/entity-framework/core/what-is-new/ef-core-8.0/breaking-changes.md b/entity-framework/core/what-is-new/ef-core-8.0/breaking-changes.md index f5fac2ae13..844ebf1b2e 100644 --- a/entity-framework/core/what-is-new/ef-core-8.0/breaking-changes.md +++ b/entity-framework/core/what-is-new/ef-core-8.0/breaking-changes.md @@ -22,6 +22,7 @@ EF Core 8 targets .NET 8. Applications targeting older .NET, .NET Core, and .NET | **Breaking change** | **Impact** | |:--------------------------------------------------------------------------------------------------------------|------------| | [`Contains` in LINQ queries may stop working on older SQL Server versions](#sqlserver-contains-compatibility) | High | +| [Possible query performance regressions around `Contains` in LINQ queries](#contains-perf-regression) | High | | [Enums in JSON are stored as ints instead of strings by default](#enums-as-ints) | High | | [SQL Server `date` and `time` now scaffold to .NET `DateOnly` and `TimeOnly`](#sqlserver-date-time-only) | Medium | | [Boolean columns with a database generated value are no longer scaffolded as nullable](#scaffold-bools) | Medium | @@ -47,17 +48,79 @@ EF Core 8 targets .NET 8. Applications targeting older .NET, .NET Core, and .NET #### Old behavior -Previously, when the `Contains` operator was used in LINQ queries with a parameterized value list, EF generated SQL that was inefficient but worked on all SQL Server versions. +EF had specialized support for LINQ queries using `Contains` operator over a parameterized value list: + +```c# +var names = new[] { "Blog1", "Blog2" }; + +var blogs = await context.Blogs + .Where(b => names.Contains(b.Name)) + .ToArrayAsync(); +``` + +Before EF Core 8.0, EF inserted the parameterized values as constants into the SQL: + +```sql +SELECT [b].[Id], [b].[Name] +FROM [Blogs] AS [b] +WHERE [b].[Name] IN (N'Blog1', N'Blog2') +``` #### New behavior -Starting with EF Core 8.0, EF now generates SQL that is more efficient, but is unsupported on SQL Server 2014 and below. +Starting with EF Core 8.0, EF now generates SQL that is more efficient in many cases, but is unsupported on SQL Server 2014 and below: + +```sql +SELECT [b].[Id], [b].[Name] +FROM [Blogs] AS [b] +WHERE [b].[Name] IN ( + SELECT [n].[value] + FROM OPENJSON(@__names_0) WITH ([value] nvarchar(max) '$') AS [n] +) +``` Note that newer SQL Server versions may be configured with an older [compatibility level](/sql/t-sql/statements/alter-database-transact-sql-compatibility-level), also making them incompatible with the new SQL. This can also occur with an Azure SQL database which was migrated from a previous on-premises SQL Server instance, carrying over the old compatibility level. #### Why -The previous SQL generated by EF Core for `Contains` inserted the parameterized values as constants in the SQL. For example, the following LINQ query: +The insertion of constant values into the SQL creates many performance problems, defeating query plan caching and causing unneeded evictions of other queries. The new EF Core 8.0 translation uses the SQL Server [`OPENJSON`](/sql/t-sql/functions/openjson-transact-sql) function to instead transfer the values as a JSON array. This solves the performance issues inherent in the previous technique; however, the `OPENJSON` function is unavailable in SQL Server 2014 and below. + +For more information about this change, [see this blog post](https://devblogs.microsoft.com/dotnet/announcing-ef8-preview-4/). + +#### Mitigations + +If your database is SQL Server 2016 (13.x) or newer, or if you're using Azure SQL, check the configured compatibility level of your database via the following command: + +```sql +SELECT name, compatibility_level FROM sys.databases; +``` + +If the compatibility level is below 130 (SQL Server 2016), consider modifying it to a newer value ([documentation](/sql/t-sql/statements/alter-database-transact-sql-compatibility-level#best-practices-for-upgrading-database-compatibility-leve)). + +Otherwise, if your database version really is older than SQL Server 2016, or is set to an old compatibility level which you cannot change for some reason, you can configure EF to revert to the older, pre-8.0 SQL. If you're using EF 9, you can use the newly-introduced : + +```c# +protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + => optionsBuilder.UseSqlServer("", o => o.TranslateParameterizedCollectionsToConstants()) +``` + +If you're using EF 8, you can achieve the same effect when using SQL Server by configuring EF's SQL compatibility level: + +```c# +protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + => optionsBuilder + .UseSqlServer(@"", o => o.UseCompatibilityLevel(120)); +``` + + + +### Possible query performance regressions around `Contains` in LINQ queries + +[Tracking Issue #32394](https://github.com/dotnet/efcore/issues/32394) + +#### Old behavior + +EF had specialized support for LINQ queries using `Contains` operator over a parameterized value list: ```c# var names = new[] { "Blog1", "Blog2" }; @@ -67,7 +130,7 @@ var blogs = await context.Blogs .ToArrayAsync(); ``` -... would be translated to the following SQL: +Before EF Core 8.0, EF inserted the parameterized values as constants into the SQL: ```sql SELECT [b].[Id], [b].[Name] @@ -75,21 +138,31 @@ FROM [Blogs] AS [b] WHERE [b].[Name] IN (N'Blog1', N'Blog2') ``` -Such insertion of constant values into the SQL creates many performance problems, defeating query plan caching and causing unneeded evictions of other queries. The new EF Core 8.0 translation uses the SQL Server [`OPENJSON`](/sql/t-sql/functions/openjson-transact-sql) function to instead transfer the values as a JSON array. This solves the performance issues inherent in the previous technique; however, the `OPENJSON` function is unavailable in SQL Server 2014 and below. +#### New behavior -For more information about this change, [see this blog post](https://devblogs.microsoft.com/dotnet/announcing-ef8-preview-4/). +Starting with EF Core 8.0, EF now generates the following: + +```sql +SELECT [b].[Id], [b].[Name] +FROM [Blogs] AS [b] +WHERE [b].[Name] IN ( + SELECT [n].[value] + FROM OPENJSON(@__names_0) WITH ([value] nvarchar(max) '$') AS [n] +) +``` + +However, after the release of EF 8 it turned out that while the new SQL is more efficient for most cases, it can be dramatically less efficient in a minority of cases, even causing query timeouts in some cases #### Mitigations -If your database is SQL Server 2016 (13.x) or newer, or if you're using Azure SQL, check the configured compatibility level of your database via the following command: +If you're using EF 9, you can use the newly-introduced to revert the `Contains` translation for all queries back to the pre-8.0 behavior: -```sql -SELECT name, compatibility_level FROM sys.databases; +```c# +protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + => optionsBuilder.UseSqlServer("", o => o.TranslateParameterizedCollectionsToConstants()) ``` -If the compatibility level is below 130 (SQL Server 2016), consider modifying it to a newer value ([documentation](/sql/t-sql/statements/alter-database-transact-sql-compatibility-level#best-practices-for-upgrading-database-compatibility-leve)). - -Otherwise, if your database version really is older than SQL Server 2016, or is set to an old compatibility level which you cannot change for some reason, configure EF Core to revert to the older, less efficient SQL as follows: +If you're using EF 8, you can achieve the same effect when using SQL Server by configuring EF's SQL compatibility level: ```c# protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) @@ -97,6 +170,14 @@ protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) .UseSqlServer(@"", o => o.UseCompatibilityLevel(120)); ``` +Finally, you can control the translation on a query-by-query basis using as follows: + +```c# +var blogs = await context.Blogs + .Where(b => EF.Constant(names).Contains(b.Name)) + .ToArrayAsync(); +``` + ### Enums in JSON are stored as ints instead of strings by default