Skip to content

Comments

Fix IIdentityResolutionInterceptor not called when entity returned from multiple queries#37786

Draft
Copilot wants to merge 4 commits intomainfrom
copilot/fix-identity-resolution-interceptor
Draft

Fix IIdentityResolutionInterceptor not called when entity returned from multiple queries#37786
Copilot wants to merge 4 commits intomainfrom
copilot/fix-identity-resolution-interceptor

Conversation

Copy link
Contributor

Copilot AI commented Feb 24, 2026

IIdentityResolutionInterceptor.UpdateTrackedInstance was never invoked when a tracked entity was returned by a subsequent query. The generated shaper code short-circuited via TryGetEntry(key, keyValues) and returned the existing tracked entity directly, bypassing StartTrackingFromQuery entirely.

Changes

  • QueryContext.TryGetEntry: Modified to return null when HasResolutionInterceptor is true and the existing entry is Unchanged. This forces the shaper's materialize path, which calls StartTrackingFromQuery, which in turn invokes the interceptor. No public property is added to QueryContext.

  • StateManager.StartTrackingFromQuery: Added key-conflict detection: when a newly materialized entity shares a key with an already-tracked entry, calls the interceptor if the existing entry is Unchanged. Skips the interceptor for Modified/Deleted/Added entries to preserve in-memory user modifications (e.g., during lazy loads).

  • ShapedQueryCompilingExpressionVisitor: Added reset of instanceVariable = entry.Entity after StartTracking in MaterializeEntity so the correct (existing) entity is returned to the caller when the interceptor resolves to an already-tracked instance.

  • IStateManager / StateManager: Added HasResolutionInterceptor property used internally by QueryContext.TryGetEntry to detect whether an interceptor is registered.

Example

// Previously, UpdateTrackedInstance was never called here:
var blog = context.Blogs.First();
context.Database.ExecuteSqlRaw("UPDATE Blogs SET Title = 'Changed' WHERE Id = 1");
var sameBlog = context.Blogs.First(); // interceptor now called with existing entry + new entity
Original prompt

This section details on the original issue you should resolve

<issue_title>IIdentityResolutionInterceptor does not intercept the resolution of</issue_title>
<issue_description>### Bug description

I think there is an issue with IIdentityResolutionInterceptor, where it will not intercept the identity resolution between entities that are returned multiple times from different queries.

The basic pattern is:

var blog = context.Blogs.First();
context.Database.ExecuteSqlRaw("UPDATE Blogs SET Title = 'Changed' WHERE Id = 1");
var sameBlog = context.Blogs.First();

which will not be intercepted. Instead the old version of the entity will just "win".

I created a minimal demo showing how it will intercept the resolution in entities that are manually attached, but not ones that are returned from queries: https://github.com/jeremysalwen/IIdentityResolutionInterceptorDemonstration

Your code

https://github.com/jeremysalwen/IIdentityResolutionInterceptorDemonstration


using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.ChangeTracking;
using Microsoft.EntityFrameworkCore.Diagnostics;

using var context = new BlogContext();
context.Database.EnsureCreated();
context.Blogs.Add(new Blog { Title = "Original" });
context.SaveChanges();

Console.WriteLine("=== Scenario A: Re-query ===");
var blog = context.Blogs.First();
context.Database.ExecuteSqlRaw("UPDATE Blogs SET Title = 'Changed' WHERE Id = 1");
var sameBlog = context.Blogs.First();
Console.WriteLine($"Title: {sameBlog.Title}");

Console.WriteLine();
Console.WriteLine("=== Scenario B: Attach ===");
context.Attach(new Blog { Id = 1, Title = "Attached" });

public class Blog
{
    public int Id { get; set; }
    public string Title { get; set; } = "";
    public override string ToString() => $"Blog {{ Id = {Id}, Title = {Title} }}";
}

public class BlogContext : DbContext
{
    public DbSet<Blog> Blogs => Set<Blog>();

    protected override void OnConfiguring(DbContextOptionsBuilder options) => options
        .UseSqlite("Data Source=demo.db")
        .AddInterceptors(new LoggingInterceptor());
}

public class LoggingInterceptor : IIdentityResolutionInterceptor
{
    public void UpdateTrackedInstance(IdentityResolutionInterceptionData data, EntityEntry existing, object newInstance)
        => Console.WriteLine($"Interceptor called: existing={existing.Entity}, new={newInstance}");
}

Stack traces


Verbose output


EF Core version

9.0.205

Database provider

SQLite

Target framework

.NET 9.0

Operating system

Windows 11

IDE

No response</issue_description>

Comments on the Issue (you are @copilot in this section)


🔒 GitHub Advanced Security automatically protects Copilot coding agent pull requests. You can protect all pull requests by enabling Advanced Security for your repositories. Learn more about Advanced Security.

Copilot AI and others added 2 commits February 24, 2026 06:41
Co-authored-by: AndriySvyryd <6539701+AndriySvyryd@users.noreply.github.com>
… detection

Co-authored-by: AndriySvyryd <6539701+AndriySvyryd@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix IIdentityResolutionInterceptor not intercepting resolutions Fix IIdentityResolutionInterceptor not called when entity returned from multiple queries Feb 24, 2026
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
[EntityFrameworkInternal]
public virtual bool HasResolutionInterceptor
Copy link
Member

Choose a reason for hiding this comment

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

@copilot Find another way to fix this without adding a property to QueryContext. Be creative and explore several posibilities

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Addressed in e81c8cb. Instead of adding a property to QueryContext, I modified TryGetEntry to return null when _stateManager.HasResolutionInterceptor is true and the existing entry is Unchanged. This causes the shaper to take the materialize path, which calls StartTrackingFromQuery, which invokes the interceptor. No public property is added to QueryContext, and ShapedQueryCompilingExpressionVisitor no longer needs any new members.

…try when interceptor present

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.

IIdentityResolutionInterceptor does not intercept the resolution of

2 participants