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

DbSet<T>.Local.Count not updated when calling ChangeTracker.Clear() #27750

Closed
jvandertil opened this issue Apr 4, 2022 · 1 comment · Fixed by #28960
Closed

DbSet<T>.Local.Count not updated when calling ChangeTracker.Clear() #27750

jvandertil opened this issue Apr 4, 2022 · 1 comment · Fixed by #28960
Labels
area-change-tracking closed-fixed The issue has been fixed and is/will be included in the release indicated by the issue milestone. customer-reported type-bug
Milestone

Comments

@jvandertil
Copy link

While working with an application that uses the ChangeTracker to clear the unit of work I ran into strange behavior with DbSet.Local.
After calling context.ChangeTracker.Clear() null items remains in DbSet.Local.

This caught me off guard because the nullability annotations state that there can be no null items.
The minimal repro case uses a single item that is added, but if you add more entries and then clear the change tracker each entry will result in a null item.

I observed this initially with the SqlServer provider, but the repro also works with the InMemory provider.
I assume that this problem is part of the core package.

Please see the attached working repro case.

Minimal working repro

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net6.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.EntityFrameworkCore" Version="6.0.3" />
    <PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="6.0.3" />
  </ItemGroup>

</Project>
using Microsoft.EntityFrameworkCore;
using System.ComponentModel.DataAnnotations;

var context = new TestContext();

context.Entities.Add(new TestEntity());

Console.WriteLine("Count local: {0}", context.Entities.Local.Count()); // 1
Console.WriteLine("Count local null: {0}", context.Entities.Local.Count(x => x is null)); // 0

context.ChangeTracker.Clear();

// If the Local collection is viewed in the debugger the Results view shows a 'null' item.

// I expected this to be 0, but there is still something in it.
Console.WriteLine("Count local: {0}", context.Entities.Local.Count()); // 1

// This is strange, because even enumerating it using foreach will skip these items
Console.WriteLine("Count local null: {0}", context.Entities.Local.Count(x => x is null)); // 0 

// Note the call to ToList(), now we have a List with null entries in it.
Console.WriteLine("Count local null to list: {0}", context.Entities.Local.ToList().Count(x => x is null)); // 1 

class TestContext : DbContext
{
    public DbSet<TestEntity> Entities => Set<TestEntity>();

    public TestContext()
        : base(new DbContextOptionsBuilder<TestContext>().UseInMemoryDatabase("test").Options)
    {
    }
}

class TestEntity
{
    [Key]
    public int Id { get; set; }
}

Include provider and version information

EF Core version:
Database provider: Microsoft.EntityFrameworkCore.SqlServer and Microsoft.EntityFrameworkCore.InMemory atleast
Target framework: .NET 6.0
Operating system: Windows 11
IDE: Visual Studio 2022 (17.1.3)

@jvandertil
Copy link
Author

jvandertil commented Apr 4, 2022

After some research it looks like the root cause of the behavior I am seeing is that the _count field (and ICollection<T>.Count property) on the LocalView<T> is not updated when ChangeTracker.Clear() is called.

This in turn causes code that has fast paths for ICollection<T> to behave incorrectly.

For example my ToList() call will result in a call to new List<T>() which has a fast path for ICollection<T> taking the Count from the passed in collection to pre-size the array and then calling CopyTo() to fill the array. Since the StateManager.GetNonDeletedEntities<TEntity>() call returns an empty enumerable (as expected), the CopyTo method doesn't actually copy anything in, but the Count of List<T> is > 0, thus allowing null entries to appear.

update:
Failing test, I added this in ChangeTrackerTest.cs

    [ConditionalFact]
    public void Clearing_change_tracker_resets_local_view_count()
    {
        using var context = new LikeAZooContext();

        int originalCount = context.Cats.Local.Count;
        context.Cats.Add(new Cat(3));

        context.ChangeTracker.Clear();

        Assert.Equal(originalCount, context.Cats.Local.Count);
    }

@jvandertil jvandertil changed the title DbSet.Local contains null item(s) after calling ChangeTracker.Clear() DbSet<T>.Local.Count not updated when calling ChangeTracker.Clear() Apr 4, 2022
@ajcvickers ajcvickers self-assigned this Apr 5, 2022
@ajcvickers ajcvickers added this to the 7.0.0 milestone Apr 5, 2022
@ajcvickers ajcvickers added the closed-fixed The issue has been fixed and is/will be included in the release indicated by the issue milestone. label Sep 2, 2022
ajcvickers added a commit that referenced this issue Sep 2, 2022
Fixes #27056 (`LocalView<TEntity>.Contains` returns true for detached entity)
Fixes #27750 (`DbSet<T>.Local.Count` not updated when calling `ChangeTracker.Clear()`)
@ajcvickers ajcvickers modified the milestones: 7.0.0, 7.0.0-rc2 Sep 9, 2022
@ajcvickers ajcvickers modified the milestones: 7.0.0-rc2, 7.0.0 Nov 5, 2022
@ajcvickers ajcvickers removed their assignment Aug 31, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-change-tracking closed-fixed The issue has been fixed and is/will be included in the release indicated by the issue milestone. customer-reported type-bug
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants