Skip to content

[release/10.0] Fix ArgumentOutOfRangeException when deleting from a complex collection with nested collections#37702

Merged
AndriySvyryd merged 1 commit intorelease/10.0from
copilot/fix-complex-collection-deletion
Feb 21, 2026
Merged

[release/10.0] Fix ArgumentOutOfRangeException when deleting from a complex collection with nested collections#37702
AndriySvyryd merged 1 commit intorelease/10.0from
copilot/fix-complex-collection-deletion

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Feb 13, 2026

Fixes #37585

Description
When deleting an item from a complex collection that contains nested complex collections, an ArgumentOutOfRangeException is thrown during SaveChanges. The database update succeeds, but the exception prevents normal completion of the operation.

Customer impact
An exception is thrown in the affected scenarios; there is no good workaround. Models with nested complex collections are relatively common for apps mapping to JSON columns.

How found
7 customer reports on 10.0.x

Regression
No, complex collections are a new feature in EF 10

Testing
Tests added

Risk
Low. The changes only affect nested complex collections. Quirk mode added.

Original prompt

This section details on the original issue you should resolve

<issue_title>Deleting an item from a ComplexCollection, that contains an array results in Error</issue_title>
<issue_description>### Bug description

This may be resolved with the same fix as #37377; however, posting to be sure.

When deleting an item from a ComplexCollection, that Contains an array, an error is thrown.

Using the code below, calling SaveChanges(), results in an exception. What is unusual, is the save still goes through and the database result is correct.

Your code

#:package Microsoft.EntityFrameworkCore@10.0.2
#:package Npgsql.EntityFrameworkCore.PostgreSQL@10.0.0
#:package Testcontainers@4.10.0

using DotNet.Testcontainers.Builders;
using DotNet.Testcontainers.Containers;
using EfCore10Regression.Tests;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Npgsql;
using static EfCore10Regression.Tests.Models;

var fixture = new PostgresContainerFixture();
await fixture.InitializeAsync();
Console.WriteLine($"Postgres started. ConnectionString={fixture.ConnectionString}");
Console.WriteLine($"Logging SQL to: {fixture.LogFilePath}");

await using (var ctx = fixture.CreateContext())
{
    var modelBs = new List<ModelB>
    {
        new() {
            Id = 1,
            ModelCs = []
        }
    };

    ctx.Models.Add(new ModelA
    {
        Id = 0,
        ModelBs = modelBs
    });

    await ctx.SaveChangesAsync();
}

await using var context = fixture.CreateContext();

var rawData = await context.Models.SingleAsync();
Console.WriteLine($"Nbr items before {rawData.ModelBs.Count}");

rawData.ModelBs = [.. rawData.ModelBs.Where(x => x.Id != 1)];
Console.WriteLine($"Nbr items after {rawData.ModelBs.Count}");

await context.SaveChangesAsync();
Console.WriteLine("SUCCESS!!");
Console.ReadLine();

await fixture.DisposeAsync();

namespace EfCore10Regression.Tests
{
    public class RegressionDbContext(DbContextOptions<RegressionDbContext> options) : DbContext(options)
    {
        public DbSet<Models.ModelA> Models => Set<Models.ModelA>();

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<Models.ModelA>(b => b.ComplexCollection(c => c.ModelBs, c =>
            {
                c.ToJson();
            }));
        }
    }

    public static class Models
    {
        public class ModelA
        {
            public int Id { get; set; }
            public List<ModelB> ModelBs { get; set; } = [];
        }

        public class ModelB
        {
            public int Id { get; set; }
            public List<ModelC> ModelCs { get; set; } = [];
        }

        public record ModelC
        {
            public Guid Id { get; set; }
        }
    }

    public class PostgresContainerFixture : IAsyncLifetime
    {
        private readonly IContainer _pgContainer;
        private StreamWriter? _logWriter;
        public string ConnectionString { get; private set; } = string.Empty;
        public string? LogFilePath { get; private set; }

        public PostgresContainerFixture()
        {
            _pgContainer = new ContainerBuilder("postgis/postgis:latest")
                .WithPortBinding(5432, assignRandomHostPort: true)
                .WithEnvironment("POSTGRES_PASSWORD", "postgres")
                .WithEnvironment("POSTGRES_USER", "postgres")
                .WithEnvironment("POSTGRES_DB", "testdb")
                .WithWaitStrategy(Wait.ForUnixContainer().UntilInternalTcpPortIsAvailable(5432))
                .Build();
        }

        public async Task InitializeAsync()
        {
            await _pgContainer.StartAsync();
            var host = _pgContainer.Hostname;
            var port = _pgContainer.GetMappedPublicPort(5432);
            ConnectionString = new NpgsqlConnectionStringBuilder
            {
                Host = host,
                Port = port,
                Username = "postgres",
                Password = "postgres",
                Database = "testdb"
            }.ToString();

            LogFilePath = Path.Combine(Path.GetTempPath(), $"efcore-sql-{Guid.NewGuid():N}.log");
            _logWriter = new StreamWriter(File.Open(LogFilePath, FileMode.Create, FileAccess.Write, FileShare.Read))
            {
                AutoFlush = true
            };
        }

        public async Task DisposeAsync()
        {
            await _pgContainer.StopAsync();
            await _pgContainer.DisposeAsync();
            _logWriter?.Dispose();
        }

        public RegressionDbContext CreateContext()
        {
            var optionsBuilder = new DbContextOptionsBuilder<RegressionDbContext>()
                .UseNpgsql(ConnectionString, a => a.UseNetTopologySuite())
                .EnableSensitiveDataLogging()
                .EnableDetailedErrors();

            if (_logWriter is not null)
            {
                optionsBuilder.LogTo(_logW...

</details>



<!-- START COPILOT CODING AGENT SUFFIX -->

- Fixes dotnet/efcore#37585

<!-- START COPILOT CODING AGENT TIPS -->
---Let Copilot coding agent [set things up for you](https://github.com/dotnet/efcore/issues/new?title=✨+Set+up+Copilot+instructions&body=Configure%20instructions%20for%20this%20repository%20as%20documented%20in%20%5BBest%20practices%20for%20Copilot%20coding%20agent%20in%20your%20repository%5D%28https://gh.io/copilot-coding-agent-tips%29%2E%0A%0A%3COnboard%20this%20repo%3E&assignees=copilot) — coding agent works faster and does higher quality work when set up for your repo.

Copilot AI changed the title [WIP] Fix deletion issue in ComplexCollection containing array Fix ArgumentOutOfRangeException when deleting from ComplexCollection with nested collections Feb 13, 2026
Copilot AI requested a review from AndriySvyryd February 13, 2026 21:27
@AndriySvyryd
Copy link
Copy Markdown
Member

@copilot Several tests are failing. Make sure to run all tests after fixing these:
Complex_collection_detects_moved_null_elements_and_replaced_instances_as_unchanged

Assert.Equal() Failure: Collections differ
                                                                           ↓ (pos 1)
Expected: <generated>                                           [Modified, Unchanged, Added]
Actual:   ListSelectIterator<InternalComplexEntry, EntityState> [Modified, Modified, Added]
                                                                           ↑ (pos 1)

Copy link
Copy Markdown
Contributor Author

Copilot AI commented Feb 13, 2026

@copilot Several tests are failing. Make sure to run all tests after fixing these:
Complex_collection_detects_moved_null_elements_and_replaced_instances_as_unchanged

...

Fixed in 704c6a1. The issue was that SetState is called during change detection (not just after AcceptChanges), so we need to read from the CLR collection to detect size changes. The fix now uses exception handling as a fallback only when ordinals are out of bounds, preserving normal change detection behavior.

@AndriySvyryd
Copy link
Copy Markdown
Member

@copilot Update progress by rewriting the PR summary using this template:

Fixes #{number}

**Description**
{Issue description}

**Customer impact**
{How does the reported issue affects customer's app? Are there workarounds?}

**How found**
{Was it customer reported or found during verification? How many customers are affected?}

**Regression**
{Is it a regression from a released version? Which one?}

**Testing**
{How the changes were tested}

**Risk**
{Low/Medium/High, argument why}

Copy link
Copy Markdown
Contributor Author

Copilot AI commented Feb 13, 2026

@copilot Update progress by rewriting the PR summary using this template:

...

Updated the PR description with the requested template format.

Copilot AI requested a review from AndriySvyryd February 18, 2026 18:09
@AndriySvyryd AndriySvyryd changed the title Fix ArgumentOutOfRangeException when deleting from ComplexCollection with nested collections [release/10.0] Fix ArgumentOutOfRangeException when deleting from a complex collection with nested collections Feb 18, 2026
@AndriySvyryd AndriySvyryd marked this pull request as ready for review February 18, 2026 20:47
@AndriySvyryd AndriySvyryd requested a review from a team as a code owner February 18, 2026 20:47
@AndriySvyryd AndriySvyryd assigned roji and unassigned AndriySvyryd Feb 18, 2026
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This pull request fixes an ArgumentOutOfRangeException that occurs when deleting an item from a complex collection that contains nested complex collections. The issue (#37585) affects EF Core 10.0.x where complex collections are a new feature. While the database update succeeds, the exception prevents normal completion of SaveChanges().

Changes:

  • Added a GetCollection method that safely handles accessing nested collections when the parent complex entry is in a deleted or added state (ordinal = -1)
  • Introduced a quirk mode switch Microsoft.EntityFrameworkCore.Issue37585 to allow reverting to the old behavior if needed
  • Added comprehensive test coverage for removal from complex collections with nested collections across multiple entity types (classes, structs, records, fields)

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated no comments.

File Description
src/EFCore/ChangeTracking/Internal/InternalEntryBase.InternalComplexCollectionEntry.cs Core fix: Added GetCollection method that returns null when parent complex entry has invalid ordinal (-1), preventing ArgumentOutOfRangeException when accessing nested collections. Added quirk mode switch.
test/EFCore.Specification.Tests/ComplexTypesTrackingTestBase.cs Added test methods for removing items from complex collections with nested collections, covering 8 different entity type variations (class, struct, readonly struct, record, field-based variants)
test/EFCore.SqlServer.FunctionalTests/ComplexTypesTrackingSqlServerTest.cs Overrode new test methods with empty bodies due to Issue #36175 (notification change tracking not supported) and field proxying limitations
test/EFCore.Relational.Specification.Tests/Update/ComplexCollectionJsonUpdateTestBase.cs Minor formatting change: converted expression-bodied method to block body for consistency

Copy link
Copy Markdown
Member

@artl93 artl93 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New feature, significant customer feedback. Approved.

@roji roji removed their assignment Feb 19, 2026
…with nested arrays

Fixes #37585

Co-authored-by: AndriySvyryd <6539701+AndriySvyryd@users.noreply.github.com>
@AndriySvyryd AndriySvyryd force-pushed the copilot/fix-complex-collection-deletion branch from 3a5f39a to 1ad6f04 Compare February 20, 2026 20:21
@AndriySvyryd AndriySvyryd merged commit 5f9c2c3 into release/10.0 Feb 21, 2026
7 checks passed
@AndriySvyryd AndriySvyryd deleted the copilot/fix-complex-collection-deletion branch February 21, 2026 01:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants