Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Breaking change for nested optional dependents sharing table with no required properties #3496

Merged
merged 1 commit into from
Oct 19, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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/17/2021
ms.date: 10/19/2021
uid: core/what-is-new/ef-core-6.0/breaking-changes
---

Expand All @@ -14,6 +14,7 @@ The following API and behavior changes have the potential to break existing appl

| **Breaking change** | **Impact** |
|:--------------------------------------------------------------------------------------------------------------------------------------|------------|
| [Nested optional dependents sharing a table and with no required properties cannot be saved](#nested-optionals) | High |
| [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 |
Expand All @@ -34,6 +35,68 @@ The following API and behavior changes have the potential to break existing appl

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

## High-impact changes

<a name="nested-optionals"></a>

### Nested optional dependents sharing a table and with no required properties are disallowed

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

#### Old behavior

Models with nested optional dependents sharing a table and with no required properties were allowed, but could result in data loss when querying for the data and then saving again. For example, consider the following model:

```csharp
public class Customer
{
public int Id { get; set; }
public string Name { get; set; }
public ContactInfo ContactInfo { get; set; }
}

[Owned]
public class ContactInfo
{
public string Phone { get; set; }
public Address Address { get; set; }
}

[Owned]
public class Address
{
public string House { get; set; }
public string Street { get; set; }
public string City { get; set; }
public string Postcode { get; set; }
}
```

None of the properties in `ContactInfo` or `Address` are required, and all these entity types are mapped to the same table. The rules for optional dependents (as opposed to required dependents) say that if all of the columns for `ContactInfo` are null, then no instance of `ContactInfo` will be created when querying for the owner `Customer`. However, this also means that no instance of `Address` will be created, even if the `Address` columns are non-null.

#### New behavior

Attempting to use this model will now throw the following exception:

> System.InvalidOperationException:
> Entity type 'ContactInfo' is an optional dependent using table sharing and containing other dependents without any required non shared property to identify whether the entity exists. If all nullable properties contain a null value in database then an object instance won't be created in the query causing nested dependent's values to be lost. Add a required property to create instances with null values for other properties or mark the incoming navigation as required to always create an instance.

This prevents data loss when querying and saving data.

#### Why

Using models with nested optional dependents sharing a table and with no required properties often resulted in silent data loss.

#### Mitigations

Avoid using optional dependents sharing a table and with no required properties. There are three easy ways to do this:

1. Make the dependents required. This means that the dependent entity will always have a value after it is queried, even if all its properties are null.
2. Make sure that the dependent contains at least one required property.
3. Map optional dependents to their own table, instead of sharing a table with the principal.

The problems with optional dependents and examples of these mitigations are included in the documentation for [What's new in EF Core 6.0](xref:core/what-is-new/ef-core-6.0/whatsnew#changes-to-owned-optional-dependent-handling).

## Medium-impact changes

<a name="owned-reparenting"></a>
Expand Down
33 changes: 31 additions & 2 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: 10/17/2021
ms.date: 10/19/2021
uid: core/what-is-new/ef-core-6.0/whatsnew
---

Expand Down Expand Up @@ -2539,7 +2539,36 @@ For this reason, EF Core 6.0 will now warn you when saving an optional dependent
> warn: 9/27/2021 09:25:01.338 RelationalEventId.OptionalDependentWithAllNullPropertiesWarning[20704] (Microsoft.EntityFrameworkCore.Update)
> The entity of type 'Address' with primary key values {CustomerId: -2147482646} is an optional dependent using table sharing. The entity does not have any property with a non-default value to identify whether the entity exists. This means that when it is queried no object instance will be created instead of an instance with all properties set to default values. Any nested dependents will also be lost. Either don't save any instance with only default values or mark the incoming navigation as required in the model.

This becomes even more tricky where the optional dependent itself acts a a principal for a further optional dependent, also mapped to the same table.
This becomes even more tricky where the optional dependent itself acts a a principal for a further optional dependent, also mapped to the same table. Rather than just warning, EF Core 6.0 disallows just cases of nested optional dependents. For example, consider the following model, where `ContactInfo` is owned by `Customer` and `Address` is in turned owned by `ContactInfo`:

<!--
public class Customer
{
public int Id { get; set; }
public string Name { get; set; }
public ContactInfo ContactInfo { get; set; }
}

public class ContactInfo
{
public string Phone { get; set; }
public Address Address { get; set; }
}

public class Address
{
public string House { get; set; }
public string Street { get; set; }
public string City { get; set; }
public string Postcode { get; set; }
}
-->
[!code-csharp[NestedWithoutRequiredProperty](../../../../samples/core/Miscellaneous/NewInEFCore6/OptionalDependentsSample.cs?name=NestedWithoutRequiredProperty)]

Now if `ContactInfo.Phone` is null, then EF Core will not create an instance of `Address` if the relationship is optional, even though the address itself may have data. For this kind of model, EF Core 6.0 will throw the following exception:

> System.InvalidOperationException:
> Entity type 'ContactInfo' is an optional dependent using table sharing and containing other dependents without any required non shared property to identify whether the entity exists. If all nullable properties contain a null value in database then an object instance won't be created in the query causing nested dependent's values to be lost. Add a required property to create instances with null values for other properties or mark the incoming navigation as required to always create an instance.

The bottom line here is to avoid the case where an optional dependent can contain all nullable property values and shares a table with its principal. There are three easy ways to avoid this:

Expand Down
2 changes: 2 additions & 0 deletions entity-framework/toc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
href: core/index.md
- name: "What's new in EF Core 6.0"
href: /ef/core/what-is-new/ef-core-6.0/whatsnew
- name: "Breaking changes in EF Core 6.0"
href: /ef/core/what-is-new/ef-core-6.0/breaking-changes
- name: Getting started
items:
- name: EF Core Overview
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ public static void Optional_dependents_without_a_required_property()
{
var connection = context.Database.GetDbConnection();
connection.Open();

using var command = connection.CreateCommand();
command.CommandText = "SELECT Id, Name, Address_House, Address_Street, Address_City, Address_Postcode FROM Customers2";

Expand Down Expand Up @@ -427,7 +427,7 @@ public class Customer
{
public int Id { get; set; }
public string Name { get; set; }

[Required]
public Address Address { get; set; }
}
Expand Down Expand Up @@ -462,6 +462,32 @@ public class Address
#endregion
}

public class NestedWithoutRequiredProperty
{
#region NestedWithoutRequiredProperty
public class Customer
{
public int Id { get; set; }
public string Name { get; set; }
public ContactInfo ContactInfo { get; set; }
}

public class ContactInfo
{
public string Phone { get; set; }
public Address Address { get; set; }
}

public class Address
{
public string House { get; set; }
public string Street { get; set; }
public string City { get; set; }
public string Postcode { get; set; }
}
#endregion
}

public class SomeDbContext : DbContext
{
public DbSet<PrincipalWithOptionalDependents> PrincipalsWithOptionalDependents { get; set; }
Expand All @@ -484,7 +510,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)
modelBuilder
.Entity<WithRequiredProperty.Customer>()
.OwnsOne(e => e.Address);

modelBuilder
.Entity<WithoutRequiredProperty.Customer>()
.OwnsOne(e => e.Address);
Expand Down