diff --git a/entity-framework/core/what-is-new/ef-core-11.0/breaking-changes.md b/entity-framework/core/what-is-new/ef-core-11.0/breaking-changes.md index f4e3c56230..94472b4b37 100644 --- a/entity-framework/core/what-is-new/ef-core-11.0/breaking-changes.md +++ b/entity-framework/core/what-is-new/ef-core-11.0/breaking-changes.md @@ -33,6 +33,7 @@ This page documents API and behavior changes that have the potential to break ex | [Cosmos: the default discriminator property is now named `Discriminator` in the model](#cosmos-discriminator-property-name) | Low | | [Owned JSON collections without an explicit key are obsolete](#owned-json-collections-obsolete) | Low | | [`Property` no longer configures primitive collections](#property-not-primitive-collection) | Low | +| [Split queries now throw when concurrent modifications are detected](#split-query-concurrency-exception) | Low | ## Medium-impact changes @@ -479,6 +480,74 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) In most cases no change is required, since primitive collections are discovered by convention. + + +### Split queries now throw when concurrent modifications are detected + +[Tracking Issue #33826](https://github.com/dotnet/efcore/issues/33826) + +#### Old behavior + +Previously, when a split query (using `AsSplitQuery()`) encountered out-of-order or orphaned child rows caused by concurrent data modifications between the split query's SQL statements, EF Core silently discarded the affected child collections. The result was an entity with an empty collection even though the related rows still existed—no exception was thrown and no warning was logged. + +#### New behavior + +Starting with EF Core 11.0, EF Core throws a `DbQueryConcurrencyException` when split query results cannot be correlated because of concurrent data modifications. The exception message describes the situation and suggests remediation: + +> The results of a split query could not be correlated because the data was modified concurrently while the query was executing. Re-execute the query, or execute it within a serializable or snapshot transaction to prevent concurrent modifications. + +#### Why + +Silently returning incorrect data (empty collections for entities that have related rows) is far worse than surfacing an error. This scenario is intrinsically caused by the lack of data-consistency guarantees in split queries when the database is modified between statements. Throwing a retriable exception makes the problem visible and gives callers a clear path to recovery. + +#### Mitigations + +The simplest mitigation is to re-execute the query; the concurrent modification is transient and the retry will typically succeed: + +```csharp +const int maxRetries = 3; + +List blogs; +for (var attempt = 0; attempt < maxRetries; attempt++) +{ + try + { + blogs = await context.Blogs + .Include(b => b.Posts) + .AsSplitQuery() + .ToListAsync(); + break; + } + catch (DbQueryConcurrencyException) when (attempt < maxRetries - 1) + { + // Retry on concurrent modification + } +} +``` + +Alternatively, wrap the split query in a serializable or snapshot transaction to prevent concurrent modifications from affecting the results: + +```csharp +await using var transaction = + await context.Database.BeginTransactionAsync(IsolationLevel.Serializable); + +var blogs = await context.Blogs + .Include(b => b.Posts) + .AsSplitQuery() + .ToListAsync(); + +await transaction.CommitAsync(); +``` + +If neither retry nor a transaction is acceptable, switch to a single query (`AsSingleQuery()`) which is always consistent: + +```csharp +var blogs = await context.Blogs + .Include(b => b.Posts) + .AsSingleQuery() + .ToListAsync(); +``` + ## Microsoft.Data.Sqlite breaking changes