Skip to content

Fix ArgumentOutOfRangeException when deleting from ComplexCollection with nested collections#37702

Draft
Copilot wants to merge 8 commits intorelease/10.0from
copilot/fix-complex-collection-deletion
Draft

Fix ArgumentOutOfRangeException when deleting from ComplexCollection with nested collections#37702
Copilot wants to merge 8 commits intorelease/10.0from
copilot/fix-complex-collection-deletion

Conversation

Copy link
Contributor

Copilot AI commented Feb 13, 2026

Description
When deleting an item from a ComplexCollection that contains nested arrays or lists, an ArgumentOutOfRangeException is thrown during SaveChanges in the AcceptChanges phase. The database update succeeds, but the exception prevents normal completion of the operation.

Customer impact
Customers cannot delete items from complex collections that contain nested collections/arrays using patterns like entity.Collection = [.. entity.Collection.Where(predicate)] without encountering an exception. While the database is updated correctly, the exception disrupts the application flow and prevents proper error handling.

Workaround: Use RemoveAt() or Clear() methods instead of reassigning the collection with filtered results, or enable the old behavior with AppContext.SetSwitch("Microsoft.EntityFrameworkCore.Issue37585", true).

How found
Customer reported (issue #37585). At least one customer is affected based on the report and reactions (7 👍, 1 👀).

Regression
This appears to be a regression in EF Core 10.0.2.

Testing

  • Added Remove_element_from_complex_collection_using_Where_with_nested_array test that reproduces the exact scenario
  • All 20 ComplexCollectionJsonUpdate tests pass
  • All 23 InternalComplexEntry tests pass
  • Verified the fix handles the edge case of invalid ordinals during state transitions
  • Added quirk mode switch to allow reverting to old behavior if needed

Risk
Low. The changes are surgical and only affect reading collection counts when the containing entry has invalid ordinals:

  • Checks if containing entry is a complex entry with ordinal < 0 before reading from CLR
  • Falls back to entries list count (source of truth) when ordinal is invalid
  • Normal behavior preserved - CLR collection is read when ordinals are valid
  • Quirk mode switch (Microsoft.EntityFrameworkCore.Issue37585) allows customers to use old behavior during migration
  • All existing tests pass, confirming no regression in normal scenarios
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 and others added 4 commits February 13, 2026 21:11
…with nested arrays

Handle ArgumentOutOfRangeException when reading collection counts during state transitions in SetState.
This fixes an issue where deleting an item from a ComplexCollection that contains arrays/lists would
cause an exception during AcceptChanges when trying to read property values using out-of-range ordinals.

Co-authored-by: AndriySvyryd <6539701+AndriySvyryd@users.noreply.github.com>
… counts

Co-authored-by: AndriySvyryd <6539701+AndriySvyryd@users.noreply.github.com>
Move ArgumentOutOfRangeException handling from SetState to GetOrCreateEntries to handle all cases
where collection reading might fail due to out-of-range ordinals.

Co-authored-by: AndriySvyryd <6539701+AndriySvyryd@users.noreply.github.com>
- Extract TryGetCollectionCount helper method to reduce code duplication
- Add detailed documentation explaining when and why ArgumentOutOfRangeException occurs
- Add initial state verification to test to ensure proper deletion behavior

Co-authored-by: AndriySvyryd <6539701+AndriySvyryd@users.noreply.github.com>
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
…ing state transitions

Instead of catching exceptions defensively, fix the root cause by not reading from the CLR collection
when it would require using out-of-bounds ordinals. During SetState after AcceptChanges, use the
already-populated entries lists as the source of truth. In GetOrCreateEntries, catch the exception
when reading from CLR fails and fall back to using the existing entries list.

This addresses the feedback to fix the code that calculates indices rather than just catching exceptions.

Co-authored-by: AndriySvyryd <6539701+AndriySvyryd@users.noreply.github.com>
@AndriySvyryd
Copy link
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)

…nting CLR reads

The previous approach of using _entries?.Count in SetState was incorrect because SetState is called
during change detection (not just after AcceptChanges). During change detection, we need to read from
the CLR collection to detect the new size. The fix now:
1. Always tries to read from CLR collection first
2. Falls back to existing entries list count only when ArgumentOutOfRangeException occurs
3. Applies this pattern in both SetState and GetOrCreateEntries

Co-authored-by: AndriySvyryd <6539701+AndriySvyryd@users.noreply.github.com>
Copy link
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
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
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.

Instead of catching ArgumentOutOfRangeException, the fix now checks if the containing entry is a
complex entry with an invalid ordinal (< 0) before attempting to read from the CLR collection.
When the ordinal is invalid, it safely uses the existing entries list count as a fallback.

Added AppContext switch "Microsoft.EntityFrameworkCore.Issue37585" to enable the old buggy behavior
for customers who need it during migration.

This avoids exceptions while properly handling the edge case of out-of-bounds ordinals during
state transitions when deleting items from complex collections with nested collections.

Co-authored-by: AndriySvyryd <6539701+AndriySvyryd@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants