Skip to content

Commit 9395fff

Browse files
authored
- Whats new in EF9 docs (#4823)
- Breaking change note for dotnet/efcore#33942 - Update function mappings Fixes #4765 Fixes #4805
1 parent 0214bec commit 9395fff

File tree

5 files changed

+343
-8
lines changed

5 files changed

+343
-8
lines changed

entity-framework/core/providers/sql-server/functions.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,8 @@ Math.Floor(d) | FLOOR(@d)
172172
Math.Log(d) | LOG(@d)
173173
Math.Log(a, newBase) | LOG(@a, @newBase)
174174
Math.Log10(d) | LOG10(@d)
175+
Math.Max(x, y) | GREATEST(@x, @y) | EF Core 9.0
176+
Math.Min(x, y) | LEAST(@x, @y) | EF Core 9.0
175177
Math.Pow(x, y) | POWER(@x, @y)
176178
Math.Round(d) | ROUND(@d, 0)
177179
Math.Round(d, decimals) | ROUND(@d, @decimals)
@@ -202,6 +204,7 @@ string.Compare(strA, strB) | CASE W
202204
string.Concat(str0, str1) | @str0 + @str1
203205
string.IsNullOrEmpty(value) | @value IS NULL OR @value LIKE N''
204206
string.IsNullOrWhiteSpace(value) | @value IS NULL OR @value = N''
207+
string.Join(", ", new [] { x, y, z}) | CONCAT_WS(N', ', @x, @y, @z) | EF Core 9.0
205208
stringValue.CompareTo(strB) | CASE WHEN @stringValue = @strB THEN 0 ... END
206209
stringValue.Contains(value) | @stringValue LIKE N'%' + @value + N'%'
207210
stringValue.EndsWith(value) | @stringValue LIKE N'%' + @value

entity-framework/core/providers/sqlite/functions.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,13 @@ This page shows which .NET members are translated into which SQL functions when
1414
.NET | SQL | Added in
1515
----------------------------------------------------- | ---------------------------------- | --------
1616
group.Average(x => x.Property) | AVG(Property)
17+
group.Average(x => x.DecimalProperty) | ef_avg(DecimalProperty) | EF Core 9.0
1718
group.Count() | COUNT(*)
1819
group.LongCount() | COUNT(*)
1920
group.Max(x => x.Property) | MAX(Property)
2021
group.Min(x => x.Property) | MIN(Property)
2122
group.Sum(x => x.Property) | SUM(Property)
23+
group.Sum(x => x.DecimalProperty) | ef_sum(DecimalProperty) | EF Core 9.0
2224
string.Concat(group.Select(x => x.Property)) | group_concat(Property, '') | EF Core 7.0
2325
string.Join(separator, group.Select(x => x.Property)) | group_concat(Property, @separator) | EF Core 7.0
2426

entity-framework/core/what-is-new/ef-core-9.0/breaking-changes.md

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,9 @@ EF Core 9 targets .NET 8. This means that existing applications that target .NET
2525
2626
| **Breaking change** | **Impact** |
2727
|:-----------------------------------------------------------------------------------------------------|------------|
28-
| [`EF.Functions.Unhex()` now returns `byte[]?`](#unhex) | Low |
28+
| [`EF.Functions.Unhex()` now returns `byte[]?`](#unhex) | Low |
2929
| [SqlFunctionExpression's nullability arguments' arity validated](#sqlfunctionexpression-nullability) | Low |
30+
| [`ToString()` method now returns empty string for `null` instances](#nullable-tostring) | Low |
3031

3132
## Low-impact changes
3233

@@ -80,6 +81,33 @@ Not having matching number of arguments and nullability propagation arguments ca
8081

8182
Make sure the `argumentsPropagateNullability` has same number of elements as the `arguments`. When in doubt use `false` for nullability argument.
8283

84+
<a name="nullable-tostring"></a>
85+
86+
### `ToString()` method now returns empty string for `null` instances
87+
88+
[Tracking Issue #33941](https://github.com/dotnet/efcore/issues/33941)
89+
90+
#### Old behavior
91+
92+
Previously EF returned inconsistent results for the `ToString()` method when the argument value was `null`. E.g. `ToString()` on `bool?` property with `null` value returned `null`, but for non-property `bool?` expressions whose value was `null` it returned `True`. The behavior was also incosistent for other data types, e.g. `ToString()` on `null` value enum returned empty string.
93+
94+
#### New behavior
95+
96+
Starting with EF Core 9.0, the `ToString()` method now consistently returns empty string in all cases when the argument value is `null`.
97+
98+
#### Why
99+
100+
The old behavior was inconsistent across different data types and situations, as well as not aligned with the [C# behavior](/dotnet/api/system.nullable-1.tostring#returns).
101+
102+
#### Mitigations
103+
104+
To revert to the old behavior, rewrite the query accordingly:
105+
106+
```csharp
107+
var newBehavior = context.Entity.Select(x => x.NullableBool.ToString());
108+
var oldBehavior = context.Entity.Select(x => x.NullableBool == null ? null : x.NullableBool.ToString());
109+
```
110+
83111
## Azure Cosmos DB breaking changes
84112

85113
Extensive work has gone into making the Azure Cosmos DB provider better in 9.0. The changes include a number of high-impact breaking changes; if you are upgrading an existing application, please read the following carefully.

entity-framework/core/what-is-new/ef-core-9.0/whatsnew.md

Lines changed: 188 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -615,6 +615,8 @@ FROM [Posts] AS [p]
615615
WHERE [p].[Title] = N'.NET Blog' AND [p].[Id] = 1
616616
```
617617

618+
#### The `EF.Parameter` method
619+
618620
EF9 introduces the `EF.Parameter` method to do the opposite. That is, force EF to use a parameter even if the value is a constant in code. For example:
619621

620622
<!--
@@ -635,6 +637,59 @@ FROM [Posts] AS [p]
635637
WHERE [p].[Title] = @__p_0 AND [p].[Id] = @__id_1
636638
```
637639

640+
<a name="parameterized-collections"></a>
641+
642+
#### Parameterized primitive collections
643+
644+
EF8 changed the way [some queries that use primitive collections are translated](xref:core/what-is-new/ef-core-8.0/whatsnew#queries-with-primitive-collections). When a LINQ query contains a parameterized primitive collection, EF converts its contents to JSON and pass it as a single parameter value the query:
645+
646+
<!--
647+
#region DefaultParameterizationPrimitiveCollection
648+
async Task<List<Post>> GetPostsPrimitiveCollection(int[] ids)
649+
=> await context.Posts
650+
.Where(e => e.Title == ".NET Blog" && ids.Contains(e.Id))
651+
.ToListAsync();
652+
-->
653+
[!code-csharp[ForceParameter](../../../../samples/core/Miscellaneous/NewInEFCore9/QuerySample.cs?name=DefaultParameterizationPrimitiveCollection)]
654+
655+
This will result in the following translation on SQL Server:
656+
657+
```output
658+
Executed DbCommand (5ms) [Parameters=[@__ids_0='[1,2,3]' (Size = 4000)], CommandType='Text', CommandTimeout='30']
659+
SELECT [p].[Id], [p].[Archived], [p].[AuthorId], [p].[BlogId], [p].[Content], [p].[Discriminator], [p].[PublishedOn], [p].[Rating], [p].[Title], [p].[PromoText], [p].[Metadata]
660+
FROM [Posts] AS [p]
661+
WHERE [p].[Title] = N'.NET Blog' AND [p].[Id] IN (
662+
SELECT [i].[value]
663+
FROM OPENJSON(@__ids_0) WITH ([value] int '$') AS [i]
664+
)
665+
```
666+
667+
This allows having the same SQL query for different parameterized collections (only the parameter value changes), but in some situations it can lead to performance issues as the database isn't able to optimally plan for the query. The `EF.Constant` method can be used to revert to the previous translation.
668+
669+
The following query uses `EF.Constant` to that effect:
670+
671+
<!--
672+
#region ForceConstantPrimitiveCollection
673+
async Task<List<Post>> GetPostsForceConstantCollection(int[] ids)
674+
=> await context.Posts
675+
.Where(e => e.Title == ".NET Blog" && EF.Constant(ids).Contains(e.Id))
676+
.ToListAsync();
677+
-->
678+
[!code-csharp[ForceParameter](../../../../samples/core/Miscellaneous/NewInEFCore9/QuerySample.cs?name=ForceConstantPrimitiveCollection)]
679+
680+
The resulting SQL is as follows:
681+
682+
```sql
683+
SELECT [p].[Id], [p].[Archived], [p].[AuthorId], [p].[BlogId], [p].[Content], [p].[Discriminator], [p].[PublishedOn], [p].[Rating], [p].[Title], [p].[PromoText], [p].[Metadata]
684+
FROM [Posts] AS [p]
685+
WHERE [p].[Title] = N'.NET Blog' AND [p].[Id] IN (1, 2, 3)
686+
```
687+
688+
Moreover, EF9 introduces `TranslateParameterizedCollectionsToConstants` [context option](/ef/core/dbcontext-configuration/#dbcontextoptions) that can be used to prevent primitive collection parameterization for all queries. We also added a complementing `TranslateParameterizedCollectionsToParameters` which forces parameterization of primitive collections explicitly (this is the default behavior).
689+
690+
> [!TIP]
691+
> The `EF.Parameter` method overrides the context option. If you want to prevent parameterization of primitive collections for most of your queries (but not all), you can set the context option `TranslateParameterizedCollectionsToConstants` and use `EF.Parameter` for the queries or individual variables that you want to parameterize.
692+
638693
<a name="inlinedsubs"></a>
639694

640695
### Inlined uncorrelated subqueries
@@ -685,7 +740,71 @@ ORDER BY (SELECT 1)
685740
OFFSET @__p_0 ROWS FETCH NEXT @__p_1 ROWS ONLY
686741
```
687742

688-
<a name="hashsetasync"></a>
743+
<a name="aggregate-over-subquery"></a>
744+
745+
### Aggregate functions over subqueries and aggregates on SQL Server
746+
747+
EF9 improves the translation of some complex queries using aggregate functions composed over subqueries or other aggregate functions.
748+
Below is an example of such query:
749+
750+
<!--
751+
var latestPostsAverageRatingByLanguage = await context.Blogs.
752+
Select(x => new
753+
{
754+
x.Language,
755+
LatestPostRating = x.Posts.OrderByDescending(xx => xx.PublishedOn).FirstOrDefault().Rating
756+
})
757+
.GroupBy(x => x.Language)
758+
.Select(x => x.Average(xx => xx.LatestPostRating))
759+
.ToListAsync();
760+
-->
761+
[!code-csharp[AggregateOverSubquery](../../../../samples/core/Miscellaneous/NewInEFCore9/QuerySample.cs?name=AggregateOverSubquery)]
762+
763+
First, `Select` computes `LatestPostRating` for each `Post` which requires a subquery when translating to SQL. Later in the query these results are aggregated using `Average` operation. The resulting SQL looks as follows when run on SQL Server:
764+
765+
```sql
766+
SELECT AVG([s].[Rating])
767+
FROM [Blogs] AS [b]
768+
OUTER APPLY (
769+
SELECT TOP(1) [p].[Rating]
770+
FROM [Posts] AS [p]
771+
WHERE [b].[Id] = [p].[BlogId]
772+
ORDER BY [p].[PublishedOn] DESC
773+
) AS [s]
774+
GROUP BY [b].[Language]
775+
```
776+
777+
In previous versions EF Core would generate invalid SQL for similar queries, trying to apply the aggregate operation directly over the subquery. This is not allowed on SQL Server and results in an exception.
778+
Same principle applies to queries using aggregate over another aggregate:
779+
780+
<!--
781+
var topRatedPostsAverageRatingByLanguage = await context.Blogs.
782+
Select(x => new
783+
{
784+
x.Language,
785+
TopRating = x.Posts.Max(x => x.Rating)
786+
})
787+
.GroupBy(x => x.Language)
788+
.Select(x => x.Average(xx => xx.TopRating))
789+
.ToListAsync();
790+
-->
791+
[!code-csharp[AggregateOverAggregate](../../../../samples/core/Miscellaneous/NewInEFCore9/QuerySample.cs?name=AggregateOverAggregate)]
792+
793+
> [!NOTE]
794+
> This change doesn't affect Sqlite, which supports aggregates over subqueries (or other aggregates) and it does not support `LATERAL JOIN` (`APPLY`). Below is the SQL for the first query running on Sqlite:
795+
>
796+
> ```sql
797+
> SELECT ef_avg((
798+
> SELECT "p"."Rating"
799+
> FROM "Posts" AS "p"
800+
> WHERE "b"."Id" = "p"."BlogId"
801+
> ORDER BY "p"."PublishedOn" DESC
802+
> LIMIT 1))
803+
> FROM "Blogs" AS "b"
804+
> GROUP BY "b"."Language"
805+
> ```
806+
807+
<a name="count-not-zero"></a>
689808
690809
### Queries using Count != 0 are optimized
691810
@@ -712,6 +831,8 @@ WHERE EXISTS (
712831
WHERE "b"."Id" = "p"."BlogId")
713832
```
714833
834+
<a name="comparison-null-semantics"></a>
835+
715836
### C# semantics for comparison operations on nullable values
716837

717838
In EF8 comparisons between nullable elements were not performed correctly for some scenarios. In C#, if one or both operands are null, the result of a comparison operation is false; otherwise, the contained values of operands are compared. In EF8 we used to translate comparisons using database null semantics. This would produce results different than similar query using LINQ to Objects.
@@ -782,6 +903,59 @@ EF9 now properly handles these scenarios, producing results consistent with LINQ
782903

783904
This enhancement was contributed by [@ranma42](https://github.com/ranma42). Many thanks!
784905

906+
<a name="order-operator"></a>
907+
908+
### Translation of `Order` and `OrderDescending` LINQ operators
909+
910+
EF9 enables the translation of LINQ simplified ordering operations (`Order` and `OrderDescending`). These work similar to `OrderBy`/`OrderByDescending` but don't require an argument. Instead, they apply default ordering - for entities this means ordering based on primary key values and for other types, ordering based on the values themselves.
911+
912+
Below is an example query which takes advantage of the simplified ordering operators:
913+
914+
<!--
915+
var orderOperation = await context.Blogs
916+
.Order()
917+
.Select(x => new
918+
{
919+
x.Name,
920+
OrderedPosts = x.Posts.OrderDescending().ToList(),
921+
OrderedTitles = x.Posts.Select(xx => xx.Title).Order().ToList()
922+
})
923+
.ToListAsync();
924+
-->
925+
[!code-csharp[OrderOrderDescending](../../../../samples/core/Miscellaneous/NewInEFCore9/QuerySample.cs?name=OrderOrderDescending)]
926+
927+
This query is equivalent to the following:
928+
929+
<!--
930+
var orderByEquivalent = await context.Blogs
931+
.OrderBy(x => x.Id)
932+
.Select(x => new
933+
{
934+
x.Name,
935+
OrderedPosts = x.Posts.OrderByDescending(xx => xx.Id).ToList(),
936+
OrderedTitles = x.Posts.Select(xx => xx.Title).OrderBy(xx => xx).ToList()
937+
})
938+
.ToListAsync();
939+
-->
940+
[!code-csharp[OrderByEquivalent](../../../../samples/core/Miscellaneous/NewInEFCore9/QuerySample.cs?name=OrderByEquivalent)]
941+
942+
and produces the following SQL:
943+
944+
```sql
945+
SELECT [b].[Name], [b].[Id], [p].[Id], [p].[Archived], [p].[AuthorId], [p].[BlogId], [p].[Content], [p].[Discriminator], [p].[PublishedOn], [p].[Rating], [p].[Title], [p].[PromoText], [p].[Metadata], [p0].[Title], [p0].[Id]
946+
FROM [Blogs] AS [b]
947+
LEFT JOIN [Posts] AS [p] ON [b].[Id] = [p].[BlogId]
948+
LEFT JOIN [Posts] AS [p0] ON [b].[Id] = [p0].[BlogId]
949+
ORDER BY [b].[Id], [p].[Id] DESC, [p0].[Title]
950+
```
951+
952+
> [!NOTE]
953+
> `Order` and `OrderDescending` methods are only supported for collections of entities, complex types or scalars - they will not work on more complex projections, e.g. collections of anonymous types containing multiple properties.
954+
955+
This enhancement was contributed by the EF Team alumnus [@bricelam](https://github.com/bricelam). Many thanks!
956+
957+
<a name="improved-negation"></a>
958+
785959
### Improved translation of logical negation operator (!)
786960

787961
EF9 brings many optimizimations around SQL `CASE/WHEN`, `COALESCE`, negation, and various other constructs; most of these were contributed by Andrea Canciani ([@ranma42](https://github.com/ranma42)) - many thanks for all of these! Below, we'll detail just a few of these optimizations around logical negation.
@@ -848,7 +1022,7 @@ On SQL Server, when projecting a negated bool property:
8481022
<!--
8491023
var negatedBoolProjection = await context.Posts.Select(x => new { x.Title, Active = !x.Archived }).ToListAsync();
8501024
-->
851-
[!code-csharp[XorBoolProjection](../../../../samples/core/Miscellaneous/NewInEFCore9/QuerySample.cs?name=XorBoolProjection)]
1025+
[!code-csharp[NegatedBoolProjection](../../../../samples/core/Miscellaneous/NewInEFCore9/QuerySample.cs?name=NegatedBoolProjection)]
8521026

8531027
EF8 would generate a `CASE` block because comparisons can't appear in the projection directly in SQL Server queries:
8541028

@@ -860,13 +1034,20 @@ END AS [Active]
8601034
FROM [Posts] AS [p]
8611035
```
8621036

863-
In EF9 this translation has been simplified and now uses exclusive or (`^`):
1037+
In EF9, this translation has been simplified and now uses bitwise NOT (`~`):
8641038

8651039
```sql
866-
SELECT [p].[Title], [p].[Archived] ^ CAST(1 AS bit) AS [Active]
1040+
SELECT [p].[Title], ~[p].[Archived] AS [Active]
8671041
FROM [Posts] AS [p]
8681042
```
8691043

1044+
<a name="azuresql-azuresynapse"></a>
1045+
1046+
### Better support for Azure SQL and Azure Synapse
1047+
1048+
EF9 allows for more flexibility when specifying the type of SQL Server which is being targeted. Instead of configuring EF with `UseSqlServer`, you can now specify `UseAzureSql` or `UseAzureSynapse`.
1049+
This allows EF to produce better SQL when using Azure SQL or Azure Synapse. EF can take advantage of the database specific features (e.g. [dedicated type for JSON on Azure SQL](/sql/t-sql/data-types/json-data-type)), or work around its limitations (e.g. [`ESCAPE` clause is not available when using `LIKE` on Azure Synapse](/sql/t-sql/language-elements/like-transact-sql#syntax)).
1050+
8701051
### Other query improvements
8711052

8721053
* The primitive collections querying support [introduced in EF8](xref:core/what-is-new/ef-core-8.0/whatsnew#queries-with-primitive-collections) has been extended to support all `ICollection<T>` types. Note that this applies only to parameter and inline collections - primitive collections that are part of entities are still limited to arrays, lists and [in EF9 also read-only arrays/lists](#read-only-primitive-collections).
@@ -878,6 +1059,9 @@ FROM [Posts] AS [p]
8781059
* `Sum` and `Average` now work for decimals on SQLite ([#33721](https://github.com/dotnet/efcore/pull/33721), contributed by [@ranma42](https://github.com/ranma42)).
8791060
* Fixes and optimizations to `string.StartsWith` and `EndsWith` ([#31482](https://github.com/dotnet/efcore/pull/31482)).
8801061
* `Convert.To*` methods can now accept argument of type `object` ([#33891](https://github.com/dotnet/efcore/pull/33891), contributed by [@imangd](https://github.com/imangd)).
1062+
* Exclusive-Or (XOR) operation is now translated on SQL Server ([#34071](https://github.com/dotnet/efcore/pull/34071), contributed by [@ranma42](https://github.com/ranma42)).
1063+
* Optimizations around nullability for `COLLATE` and `AT TIME ZONE` operations ([#34263](https://github.com/dotnet/efcore/pull/34263), contributed by [@ranma42](https://github.com/ranma42)).
1064+
* Optimizations for `DISTINCT` over `IN`, `EXISTS` and set operations ([#34381](https://github.com/dotnet/efcore/pull/34381), contributed by [@ranma42](https://github.com/ranma42)).
8811065

8821066
The above were only some of the more important query improvements in EF9; see [this issue](https://github.com/dotnet/efcore/issues/34151) for a more complete listing.
8831067

0 commit comments

Comments
 (0)