Skip to content

Validate entity model and support optional one-to-one relationships on SQL Server - #2090

Merged
bkoelman merged 1 commit into
masterfrom
fix-one-to-one-mssql
Oct 6, 2026
Merged

bkoelman merged 1 commit into
masterfrom
fix-one-to-one-mssql

Conversation

@bkoelman

@bkoelman bkoelman commented Sep 27, 2026 •

Copy link
Copy Markdown
Member

Due to limitations in SQL Server, the default delete behavior for nullable foreign keys in Entity Framework Core is ClientSetNull instead of SetNull. This means that EF Core sends UPDATE statements to set all related foreign keys to null before sending the DELETE statement. But that only works when all related entities are loaded in the change tracker upfront.

For example, consider the following model:

[Resource]
public class Order : Identifiable<long>
{
}

[Resource]
public class Customer : Identifiable<long>
{
    [ForeignKey("FirstOrderId")]
    public Order? FirstOrder { get; set; }

    [HasOne]
    [ForeignKey("LastOrderId")]
    public Order? LastOrder { get; set; }
}

When sending DELETE /orders/1, this PR ensures that all customers with FirstOrderId or LastOrderId = 1 get loaded in the change tracker before deleting the order. The missing [HasOne] on FirstOrder is intentional: this also works for relationships that are not publicly exposed.

All that side-loading, which only happens when ClientSetNull is used, is of course highly inefficient. Other databases, such as PostgreSQL and MySQL, don't need this workaround. We've documented this for a long time.

This PR adds validation of the Entity Framework Core mappings. It fails when ClientSetNull is used (explicitly or implicitly) with databases other than SQL Server. It also validates that no Identifying Foreign Keys are used, which break JSON:API semantics. By default, an exception is thrown at startup, which can be downgraded to logged warnings by setting options.ThrowForProblematicEntityMappings = false.

DapperExample was similarly updated to fully support nullable foreign keys when deleting a resource.

Note

The ClientCascase delete mode (also only needed for SQL Server) remains broken. Supporting that means we need to load all related entities recursively in the change tracker. It is never used by default in Entity Framework Core. For databases other than SQL Server, use Cascade instead. When using SQL Server, send JSON:API requests to delete related resources first to avoid a foreign key constraint error.

Fixes #1118.

QUALITY CHECKLIST

@bkoelman
bkoelman force-pushed the fix-one-to-one-mssql branch 5 times, most recently from a8a1ac2 to 793e2ea Compare September 27, 2026 20:53
@codecov

codecov Bot commented Sep 27, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 95.13889% with 14 lines in your changes missing coverage. Please review.
✅ Project coverage is 92.14%. Comparing base (ebdd586) to head (ea6a6be).

Files with missing lines Patch % Lines
...xamples/DapperExample/Repositories/DapperFacade.cs 58.33% 4 Missing and 1 partial ⚠️
...Configuration/EntityFrameworkCoreModelValidator.cs 95.12% 2 Missing and 2 partials ⚠️
...les/DapperExample/Repositories/DapperRepository.cs 57.14% 2 Missing and 1 partial ⚠️
...TranslationToSql/DataModel/BaseDataModelService.cs 93.75% 0 Missing and 1 partial ⚠️
...Core/Repositories/EntityFrameworkCoreRepository.cs 98.11% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #2090      +/-   ##
==========================================
+ Coverage   92.03%   92.14%   +0.11%     
==========================================
  Files         364      366       +2     
  Lines       11898    12079     +181     
  Branches     1995     2032      +37     
==========================================
+ Hits        10950    11130     +180     
+ Misses        586      585       -1     
- Partials      362      364       +2     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@bkoelman
bkoelman force-pushed the fix-one-to-one-mssql branch 2 times, most recently from e03d36b to 001d6b2 Compare October 5, 2026 00:05
@bkoelman bkoelman changed the title Fix deleting one-to-one relationships on SQL Server Validate entity model and support optional one-to-one relationships on SQL Server Oct 6, 2026
@bkoelman
bkoelman force-pushed the fix-one-to-one-mssql branch from 1853d74 to ea6a6be Compare October 6, 2026 04:39
@bkoelman
bkoelman marked this pull request as ready for review October 6, 2026 04:55
@bkoelman
bkoelman merged commit 335628d into master Oct 6, 2026
15 checks passed
@bkoelman
bkoelman deleted the fix-one-to-one-mssql branch October 6, 2026 04:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

Enhance support for deleting resource with incoming optional 1-to-1 relationship when using DeleteBehavior.ClientSetNull

1 participant