Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 17 additions & 5 deletions src/EFCore/Query/QueryContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -114,12 +114,24 @@ public virtual void InitializeStateManager(bool standAlone = false)
/// </summary>
[EntityFrameworkInternal]
public virtual InternalEntityEntry? TryGetEntry(
IKey key,
object[] keyValues,
bool throwOnNullKey,
out bool hasNullKey)
IKey key,
object[] keyValues,
bool throwOnNullKey,
out bool hasNullKey)
{
// InitializeStateManager will populate the field before calling here
=> _stateManager!.TryGetEntry(key, keyValues, throwOnNullKey, out hasNullKey);
var entry = _stateManager!.TryGetEntry(key, keyValues, throwOnNullKey, out hasNullKey);

// An entity returned by a tracking query provably exists in the store, so an entry that is being tracked as
// Added (e.g. a new entity whose key matches an existing row, possibly created during an Add graph traversal)
// must be corrected to Unchanged. Otherwise it would be re-inserted, causing a duplicate key. See #35762.
if (entry is { EntityState: EntityState.Added })
{
entry.SetEntityState(EntityState.Unchanged);
}

return entry;
}

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
Expand Down
11 changes: 11 additions & 0 deletions test/EFCore.Specification.Tests/LoadTestBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1726,6 +1726,12 @@ public virtual async Task Load_collection_untyped(EntityState state, bool async)
Assert.Equal(2, parent.Children.Count());
Assert.All(parent.Children.Select(e => e.Parent), c => Assert.Same(parent, c));

var expectedChildState = state == EntityState.Detached ? EntityState.Detached : EntityState.Unchanged;
foreach (var child in parent.Children)
{
Assert.Equal(expectedChildState, context.Entry(child).State);
}

Assert.Equal(state == EntityState.Detached ? 0 : 3, context.ChangeTracker.Entries().Count());
}

Expand Down Expand Up @@ -1868,9 +1874,14 @@ public virtual async Task Load_one_to_one_reference_to_dependent_untyped(EntityS

var single = context.ChangeTracker.Entries<Single>().Single().Entity;

Assert.Equal(EntityState.Unchanged, context.Entry(single).State);
Assert.Same(single, parent.Single);
Assert.Same(parent, single.Parent);
}
else
{
Assert.Equal(EntityState.Detached, navigationEntry.EntityEntry.State);
}
}

[Theory, InlineData(EntityState.Unchanged, true), InlineData(EntityState.Unchanged, false),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Microsoft.Data.Sqlite;

namespace Microsoft.EntityFrameworkCore;

public class LoadExistingEntityStateSqliteTest
{
[Fact] // Issue #35762
public void Load_collection_marks_tracked_Added_member_that_exists_in_store_as_Unchanged()
{
using var connection = new SqliteConnection("Data Source=:memory:");
connection.Open();

int municipalityId;
using (var context = new LoadContext(connection))
{
context.Database.EnsureCreated();
var municipality = new Municipality
{
Name = "M1",
Residences = [new ChildResidence { Id = 1 }, new ChildResidence { Id = 2 }]
};
context.Add(municipality);
context.SaveChanges();
municipalityId = municipality.Id;
}

using (var context = new LoadContext(connection))
{
var municipality = context.Set<Municipality>().Single();

// A residence that already exists in the store is incorrectly tracked as Added.
var existing = new ChildResidence { Id = 1, MunicipalityId = municipalityId };
context.Add(existing);

var collectionEntry = context.Entry(municipality).Collection(m => m.Residences);
Assert.False(collectionEntry.IsLoaded);
Assert.Equal(EntityState.Added, context.Entry(existing).State);

collectionEntry.Load();

// The load query returned the residence, proving it exists in the store, so it is now Unchanged.
Assert.Equal(EntityState.Unchanged, context.Entry(existing).State);
}
}

private class LoadContext(SqliteConnection connection) : DbContext
{
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
=> optionsBuilder.UseSqlite(connection);

public DbSet<Municipality> Municipalities
=> Set<Municipality>();

public DbSet<ChildResidence> ChildResidences
=> Set<ChildResidence>();
}

private class Municipality
{
public int Id { get; set; }
public string Name { get; set; } = null!;
public List<ChildResidence> Residences { get; set; } = null!;
}

private class ChildResidence
{
public int Id { get; set; }
public int MunicipalityId { get; set; }
public Municipality Municipality { get; set; } = null!;
}
}
Loading