Skip to content

Commit

Permalink
Doc updates for breaking changes and switching to daily GA build (#3492)
Browse files Browse the repository at this point in the history
  • Loading branch information
ajcvickers authored Oct 19, 2021
1 parent 1c255b6 commit bdfafd0
Show file tree
Hide file tree
Showing 7 changed files with 483 additions and 169 deletions.
139 changes: 136 additions & 3 deletions entity-framework/core/what-is-new/ef-core-6.0/breaking-changes.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
title: Breaking changes in EF Core 6.0 - EF Core
description: Complete list of breaking changes introduced in Entity Framework Core 6.0
author: ajcvickers
ms.date: 10/8/2021
ms.date: 10/17/2021
uid: core/what-is-new/ef-core-6.0/breaking-changes
---

Expand All @@ -17,17 +17,22 @@ The following API and behavior changes have the potential to break existing appl
| [Changing the owner of an owned entity now throws an exception](#owned-reparenting) | Medium |
| [Cosmos: Related entity types are discovered as owned](#cosmos-owned) | Medium |
| [Cleaned up mapping between DeleteBehavior and ON DELETE values](#on-delete) | Low |
| [In-memory database validates required properties do not contain nulls](#in-memory-required) | Low |
| [Removed last ORDER BY when joining for collections](#last-order-by) | Low |
| [DbSet no longer implements IAsyncEnumerable](#dbset-iasyncenumerable) | Low |
| [TVF return entity type is also mapped to a table by default](#tvf-table) | Low |
| [Check constraint name uniqueness is now validated](#unique-check-constraints) | Low |
| [Added IReadOnly Metadata interfaces and removed extension methods](#ireadonly-metadata) | Low |
| [SQL Server: More errors are considered transient](#transient-errors) | Low |
| [Cosmos: More characters are escaped in 'id' values](#cosmos-id) | Low |
| [Some Singleton services are now Scoped](#query-services) | Low |
| [New caching API for extensions that add or replace services](#extensions-caching) | Low |
| [Some Singleton services are now Scoped](#query-services) | Low* |
| [New caching API for extensions that add or replace services](#extensions-caching) | Low* |
| [New snapshot model initialization procedure](#snapshot-initialization) | Low |
| [`OwnedNavigationBuilder.HasIndex` returns a different type now](#owned-index) | Low |
| [Pre-initialized navigations are overridden by values from database queries](#overwrite-navigations) | Low |
| [Unknown enum string values in the database are not converted to the enum default when queried](#unknown-emums) | Low |

\* These changes are of particular interest to authors of database providers and extensions.

## Medium-impact changes

Expand Down Expand Up @@ -125,6 +130,36 @@ You can choose to either apply these operations or manually remove them from the

SQL Server doesn't support RESTRICT, so these foreign keys were already created using NO ACTION. The migration operations will have no affect on SQL Server and are safe to remove.

<a name="in-memory-required"></a>

### In-memory database validates required properties do not contain nulls

[Tracking Issue #10613](https://github.com/dotnet/efcore/issues/10613)

#### Old behavior

The in-memory database allowed saving null values even when the property was configured as required.

#### New behavior

The in-memory database throws a `Microsoft.EntityFrameworkCore.DbUpdateException` when `SaveChanges` or `SaveChangesAsync` is called and a required property is set to null.

#### Why

The in-memory database behavior now matches the behavior of other databases.

#### Mitigations

The previous behavior (i.e. not checking null values) can be restored when configuring the in-memory provider. For example:

```csharp
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder
.UseInMemoryDatabase("MyDatabase", b => b.EnableNullChecks(false));
}
```

<a name="last-order-by"></a>

### Removed last ORDER BY when joining for collections
Expand Down Expand Up @@ -433,3 +468,101 @@ The returned builder object wasn't typed correctly.
#### Mitigations

Recompiling your assembly against the latest version of EF Core will be enough to fix any issues caused by this change.

<a name="overwrite-navigations"></a>

### Pre-initialized navigations are overridden by values from database queries

[Tracking Issue #23851](https://github.com/dotnet/efcore/issues/23851)

#### Old behavior

Navigation properties set to an empty object were left unchanged for tracking queries, but were overwritten for non-tracking queries. For example, consider the following entity types:

```csharp
public class Foo
{
public int Id { get; set; }

public Bar Bar { get; set; } = new(); // Don't do this.
}

public class Bar
{
public int Id { get; set; }
}
```

A no-tracking query for `Foo` including `Bar` set `Foo.Bar` to the entity queried from the database. For example, this code:

```csharp
var foo = context.Foos.AsNoTracking().Include(e => e.Bar).Single();
Console.WriteLine($"Foo.Bar.Id = {foo.Bar.Id}");
```

Printed `Foo.Bar.Id = 1`.

However, the same query run for tracking didn't overwrite `Foo.Bar` with the entity queried from the database. For example, this code:

```csharp
var foo = context.Foos.Include(e => e.Bar).Single();
Console.WriteLine($"Foo.Bar.Id = {foo.Bar.Id}");
```

Printed `Foo.Bar.Id = 0`.

#### New behavior

In EF Core 6.0, the behavior of tracking queries now matches that of no-tracking queries. This means that both this code:

```csharp
var foo = context.Foos.AsNoTracking().Include(e => e.Bar).Single();
Console.WriteLine($"Foo.Bar.Id = {foo.Bar.Id}");
```

And this code:

```csharp
var foo = context.Foos.Include(e => e.Bar).Single();
Console.WriteLine($"Foo.Bar.Id = {foo.Bar.Id}");
```

Print `Foo.Bar.Id = 1`.

#### Why

There are two reasons for making this change:

1. To ensure that tracking and no-tracking queries have consistent behavior.
2. When a database is queried it is reasonable to assume that the application code wants to get back the values that are stored in the database.

#### Mitigations

There are two mitigations:

1. Do not query for objects from the database that should not be included in the results. For example, in the code snippets above, do not `Include` `Foo.Bar` if the `Bar` instance should not be returned from the database and included in the results.
2. Set the value of the navigation after querying from the database. For example, in the code snippets above, call `foo.Bar = new()` after running the query.

Also, consider not initializing related entity instances to default objects. This implies that the related instance is a new entity, not saved to the database, with no key value set. If instead the related entity does exist in the database, then the data in code is fundamentally at odds with the data stored in the database.

<a name="unknown-emums"></a>

### Unknown enum string values in the database are not converted to the enum default when queried

[Tracking Issue #24084](https://github.com/dotnet/efcore/issues/24084)

#### Old behavior

Enum properties can be mapped to string columns in the database using `HasConversion<string>()` or `EnumToStringConverter`. This results in EF Core converting string values in the column to matching members of the .NET enum type. However, if the string value did not match and enum member, then the property was set to the default value for the enum.

#### New behavior

EF Core 6.0 now throws an `InvalidOperationException` with the message "Cannot convert string value '`{value}`' from the database to any value in the mapped '`{enumType}`' enum."

#### Why

Converting to the default value can result in database corruption if the entity is later saved back to the database.

#### Mitigations

Ideally, ensure that the database column only contains valid values. Alternately, implement a `ValueConverter` with the old behavior.
177 changes: 167 additions & 10 deletions entity-framework/core/what-is-new/ef-core-6.0/whatsnew.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
title: What's New in EF Core 6.0
description: Overview of new features in EF Core 6.0
author: ajcvickers
ms.date: 09/28/2021
ms.date: 10/17/2021
uid: core/what-is-new/ef-core-6.0/whatsnew
---

Expand Down Expand Up @@ -1743,6 +1743,149 @@ LEFT JOIN [Feet] AS [f] ON [p].[Id] = [f].[Id]
GROUP BY [p].[FirstName]
```

**Example 10:**

<!--
var results = from Person person1
in from Person person2
in context.People
select person2
join Shoes shoes
in context.Shoes
on person1.Age equals shoes.Age
group shoes by
new
{
person1.Id,
shoes.Style,
shoes.Age
}
into temp
select
new
{
temp.Key.Id,
temp.Key.Age,
temp.Key.Style,
Values = from t
in temp
select
new
{
t.Id,
t.Style,
t.Age
}
};
-->
[!code-csharp[GroupBy10](../../../../samples/core/Miscellaneous/NewInEFCore6/GroupBySample.cs?name=GroupBy10)]

```sql
SELECT [t].[Id], [t].[Age], [t].[Style], [t0].[Id], [t0].[Style], [t0].[Age], [t0].[Id0]
FROM (
SELECT [p].[Id], [s].[Age], [s].[Style]
FROM [People] AS [p]
INNER JOIN [Shoes] AS [s] ON [p].[Age] = [s].[Age]
GROUP BY [p].[Id], [s].[Style], [s].[Age]
) AS [t]
LEFT JOIN (
SELECT [s0].[Id], [s0].[Style], [s0].[Age], [p0].[Id] AS [Id0]
FROM [People] AS [p0]
INNER JOIN [Shoes] AS [s0] ON [p0].[Age] = [s0].[Age]
) AS [t0] ON (([t].[Id] = [t0].[Id0]) AND (([t].[Style] = [t0].[Style]) OR ([t].[Style] IS NULL AND [t0].[Style] IS NULL))) AND ([t].[Age] = [t0].[Age])
ORDER BY [t].[Id], [t].[Style], [t].[Age], [t0].[Id0]
```

**Example 11:**

<!--
var grouping = context.People
.GroupBy(i => i.LastName)
.Select(g => new { LastName = g.Key, Count = g.Count() , First = g.FirstOrDefault(), Take = g.Take(2)})
.OrderByDescending(e => e.LastName)
.ToList();
-->
[!code-csharp[GroupBy11](../../../../samples/core/Miscellaneous/NewInEFCore6/GroupBySample.cs?name=GroupBy11)]

```sql
SELECT [t].[LastName], [t].[c], [t0].[Id], [t2].[Id], [t2].[Age], [t2].[FirstName], [t2].[LastName], [t2].[MiddleInitial], [t0].[Age], [t0].[FirstName], [t0].[LastName], [t0].[MiddleInitial]
FROM (
SELECT [p].[LastName], COUNT(*) AS [c]
FROM [People] AS [p]
GROUP BY [p].[LastName]
) AS [t]
LEFT JOIN (
SELECT [t1].[Id], [t1].[Age], [t1].[FirstName], [t1].[LastName], [t1].[MiddleInitial]
FROM (
SELECT [p0].[Id], [p0].[Age], [p0].[FirstName], [p0].[LastName], [p0].[MiddleInitial], ROW_NUMBER() OVER(PARTITION BY [p0].[LastName] ORDER BY [p0].[Id]) AS [row]
FROM [People] AS [p0]
) AS [t1]
WHERE [t1].[row] <= 1
) AS [t0] ON [t].[LastName] = [t0].[LastName]
LEFT JOIN (
SELECT [t3].[Id], [t3].[Age], [t3].[FirstName], [t3].[LastName], [t3].[MiddleInitial]
FROM (
SELECT [p1].[Id], [p1].[Age], [p1].[FirstName], [p1].[LastName], [p1].[MiddleInitial], ROW_NUMBER() OVER(PARTITION BY [p1].[LastName] ORDER BY [p1].[Id]) AS [row]
FROM [People] AS [p1]
) AS [t3]
WHERE [t3].[row] <= 2
) AS [t2] ON [t].[LastName] = [t2].[LastName]
ORDER BY [t].[LastName] DESC, [t0].[Id], [t2].[LastName], [t2].[Id]
```

**Example 12:**

<!--
var grouping = context.People
.Include(e => e.Shoes)
.OrderBy(e => e.FirstName)
.ThenBy(e => e.LastName)
.GroupBy(e => e.FirstName)
.Select(g => new { Name = g.Key, People = g.ToList()})
.ToList();
-->
[!code-csharp[GroupBy12](../../../../samples/core/Miscellaneous/NewInEFCore6/GroupBySample.cs?name=GroupBy12)]

```sql
SELECT [t].[FirstName], [t0].[Id], [t0].[Age], [t0].[FirstName], [t0].[LastName], [t0].[MiddleInitial], [t0].[Id0], [t0].[Age0], [t0].[PersonId], [t0].[Style]
FROM (
SELECT [p].[FirstName]
FROM [People] AS [p]
GROUP BY [p].[FirstName]
) AS [t]
LEFT JOIN (
SELECT [p0].[Id], [p0].[Age], [p0].[FirstName], [p0].[LastName], [p0].[MiddleInitial], [s].[Id] AS [Id0], [s].[Age] AS [Age0], [s].[PersonId], [s].[Style]
FROM [People] AS [p0]
LEFT JOIN [Shoes] AS [s] ON [p0].[Id] = [s].[PersonId]
) AS [t0] ON [t].[FirstName] = [t0].[FirstName]
ORDER BY [t].[FirstName], [t0].[Id]
```

**Example 13:**

<!--
var grouping = context.People
.GroupBy(m => new {m.FirstName, m.MiddleInitial })
.Select(am => new
{
Key = am.Key,
Items = am.ToList()
})
.ToList();
-->
[!code-csharp[GroupBy13](../../../../samples/core/Miscellaneous/NewInEFCore6/GroupBySample.cs?name=GroupBy13)]

```sql
SELECT [t].[FirstName], [t].[MiddleInitial], [p0].[Id], [p0].[Age], [p0].[FirstName], [p0].[LastName], [p0].[MiddleInitial]
FROM (
SELECT [p].[FirstName], [p].[MiddleInitial]
FROM [People] AS [p]
GROUP BY [p].[FirstName], [p].[MiddleInitial]
) AS [t]
LEFT JOIN [People] AS [p0] ON (([t].[FirstName] = [p0].[FirstName]) OR ([t].[FirstName] IS NULL AND [p0].[FirstName] IS NULL)) AND (([t].[MiddleInitial] = [p0].[MiddleInitial]) OR ([t].[MiddleInitial] IS NULL AND [p0].[MiddleInitial] IS NULL))
ORDER BY [t].[FirstName], [t].[MiddleInitial]
```

**Model**

The entity types used for these examples are:
Expand Down Expand Up @@ -2742,9 +2885,21 @@ modelBuilder.Entity<Cat>()

GitHub Issue: [#13850](https://github.com/dotnet/efcore/issues/13850).

> [!IMPORTANT]
> Due to the problems outlined below, the constructors for `ValueConverter` that allow conversion of nulls have been marked with `[EntityFrameworkInternal]` for the EF Core 6.0 release. Using these constructors will now generate a build warning.
Value converters do not generally allow the conversion of null to some other value. This is because the same value converter can be used for both nullable and non-nullable types, which is very useful for PK/FK combinations where the FK is often nullable and the PK is not.

Starting with EF Core 6.0, a value converter can be created that does convert nulls. One example of where this can be useful is when the database contains nulls, but the entity type wants to use some other default value for the property. For example, consider an enum where its default value is "Unknown":
Starting with EF Core 6.0, a value converter can be created that does convert nulls. However, validation of this feature has revealed proved it to be very problematic in practice with many pitfalls. For example:

* [Value conversion to null in the store generates bad queries](https://github.com/dotnet/efcore/issues/26209)
* [Value conversion from null in the store generates bad queries](https://github.com/dotnet/efcore/issues/26210)
* [Value converters do not handle cases where the database column has multiple different values that convert to the same value](https://github.com/dotnet/efcore/issues/13850#issuecomment-894166107)
* [Allow value converters to change nullability of columns](https://github.com/dotnet/efcore/issues/24685)

These are not trivial issues and for the query issues they are not easy to detect. Therefore, we have marked this feature as internal for EF Core 6.0. You can still use it, but you will get a compiler warning. The warning can be disabled using `#pragma warning disable EF1001`.

One example of where converting nulls can be useful is when the database contains nulls, but the entity type wants to use some other default value for the property. For example, consider an enum where its default value is "Unknown":

<!--
public enum Breed
Expand All @@ -2759,16 +2914,18 @@ public enum Breed
However, the database may have null values when the breed is unknown. In EF Core 6.0, a value converter can be used to account for this:

<!--
public class BreedConverter : ValueConverter<Breed, string>
{
public BreedConverter()
: base(
v => v == Breed.Unknown ? null : v.ToString(),
v => v == null ? Breed.Unknown : Enum.Parse<Breed>(v),
convertsNulls: true)
public class BreedConverter : ValueConverter<Breed, string>
{
#pragma warning disable EF1001
public BreedConverter()
: base(
v => v == Breed.Unknown ? null : v.ToString(),
v => v == null ? Breed.Unknown : Enum.Parse<Breed>(v),
convertsNulls: true)
{
}
#pragma warning restore EF1001
}
}
-->
[!code-csharp[BreedConverter](../../../../samples/core/Miscellaneous/NewInEFCore6/ConvertNullsSample.cs?name=BreedConverter)]

Expand Down
1 change: 1 addition & 0 deletions samples/NuGet.Config
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
<configuration>
<packageSources>
<clear />
<add key="dotnet6" value="https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet6/nuget/v3/index.json" />
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
</packageSources>
<disabledPackageSources>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Cosmos" Version="6.0.0-rc.1.21452.10" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Cosmos" Version="6.0.0-rtm.21515.19" />
</ItemGroup>

</Project>
Loading

0 comments on commit bdfafd0

Please sign in to comment.