From 3c45933404ede9e64513681dde194ef761b9b979 Mon Sep 17 00:00:00 2001 From: ugurozturk Date: Fri, 20 Feb 2026 13:56:23 +0300 Subject: [PATCH 1/2] Improve FindAsync method to handle entity attachment and state management --- .../EntityFrameworkCore/EfCoreRepository.cs | 29 +++++++++++++++---- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/Domain/Repositories/EntityFrameworkCore/EfCoreRepository.cs b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/Domain/Repositories/EntityFrameworkCore/EfCoreRepository.cs index ff4aeabc44..7479e4219a 100644 --- a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/Domain/Repositories/EntityFrameworkCore/EfCoreRepository.cs +++ b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/Domain/Repositories/EntityFrameworkCore/EfCoreRepository.cs @@ -476,11 +476,30 @@ public class EfCoreRepository : EfCoreRepository FindAsync(TKey id, bool includeDetails = true, CancellationToken cancellationToken = default) { - return includeDetails - ? await (await WithDetailsAsync()).OrderBy(e => e.Id).FirstOrDefaultAsync(e => e.Id!.Equals(id), GetCancellationToken(cancellationToken)) - : !ShouldTrackingEntityChange() - ? await (await GetQueryableAsync()).OrderBy(e => e.Id).FirstOrDefaultAsync(e => e.Id!.Equals(id), GetCancellationToken(cancellationToken)) - : await (await GetDbSetAsync()).FindAsync(new object[] { id! }, GetCancellationToken(cancellationToken)); + if (includeDetails) + { + return await (await WithDetailsAsync()).OrderBy(e => e.Id).FirstOrDefaultAsync(e => e.Id!.Equals(id), GetCancellationToken(cancellationToken)); + } + + if (!ShouldTrackingEntityChange()) + { + return await (await GetQueryableAsync()).OrderBy(e => e.Id).FirstOrDefaultAsync(e => e.Id!.Equals(id), GetCancellationToken(cancellationToken)); + } + + var dbSet = await GetDbSetAsync(); + + var entity = await dbSet.FindAsync(new object[] { id! }, GetCancellationToken(cancellationToken)); + if (entity == null) + { + return null; + } + + if (dbSet.Entry(entity).State == EntityState.Detached) + { + dbSet.Attach(entity); + } + + return entity; } public virtual async Task DeleteAsync(TKey id, bool autoSave = false, CancellationToken cancellationToken = default) From 4bac0f1d7685e42b5d81176557129b2594dc4725 Mon Sep 17 00:00:00 2001 From: maliming Date: Sun, 22 Feb 2026 18:48:19 +0800 Subject: [PATCH 2/2] Respect entity tracking by forcing AsTracking --- .../EfCoreRepositoryExtensions.cs | 2 +- .../EntityFrameworkCore/EfCoreRepository.cs | 27 +----- .../ChangeTrackingInterceptor_Tests.cs | 91 +++++++++++++++++++ 3 files changed, 95 insertions(+), 25 deletions(-) diff --git a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/Domain/Repositories/EfCoreRepositoryExtensions.cs b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/Domain/Repositories/EfCoreRepositoryExtensions.cs index 614ba135ff..d054733785 100644 --- a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/Domain/Repositories/EfCoreRepositoryExtensions.cs +++ b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/Domain/Repositories/EfCoreRepositoryExtensions.cs @@ -49,6 +49,6 @@ public static class EfCoreRepositoryExtensions public static IQueryable AsNoTrackingIf(this IQueryable queryable, bool condition) where TEntity : class, IEntity { - return condition ? queryable.AsNoTracking() : queryable; + return condition ? queryable.AsNoTracking() : queryable.AsTracking(); } } diff --git a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/Domain/Repositories/EntityFrameworkCore/EfCoreRepository.cs b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/Domain/Repositories/EntityFrameworkCore/EfCoreRepository.cs index 7479e4219a..cf8f994d42 100644 --- a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/Domain/Repositories/EntityFrameworkCore/EfCoreRepository.cs +++ b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/Domain/Repositories/EntityFrameworkCore/EfCoreRepository.cs @@ -476,30 +476,9 @@ public class EfCoreRepository : EfCoreRepository FindAsync(TKey id, bool includeDetails = true, CancellationToken cancellationToken = default) { - if (includeDetails) - { - return await (await WithDetailsAsync()).OrderBy(e => e.Id).FirstOrDefaultAsync(e => e.Id!.Equals(id), GetCancellationToken(cancellationToken)); - } - - if (!ShouldTrackingEntityChange()) - { - return await (await GetQueryableAsync()).OrderBy(e => e.Id).FirstOrDefaultAsync(e => e.Id!.Equals(id), GetCancellationToken(cancellationToken)); - } - - var dbSet = await GetDbSetAsync(); - - var entity = await dbSet.FindAsync(new object[] { id! }, GetCancellationToken(cancellationToken)); - if (entity == null) - { - return null; - } - - if (dbSet.Entry(entity).State == EntityState.Detached) - { - dbSet.Attach(entity); - } - - return entity; + return includeDetails + ? await (await WithDetailsAsync()).OrderBy(e => e.Id).FirstOrDefaultAsync(e => e.Id!.Equals(id), GetCancellationToken(cancellationToken)) + : await (await GetQueryableAsync()).OrderBy(e => e.Id).FirstOrDefaultAsync(e => e.Id!.Equals(id), GetCancellationToken(cancellationToken)); } public virtual async Task DeleteAsync(TKey id, bool autoSave = false, CancellationToken cancellationToken = default) diff --git a/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/ChangeTracking/ChangeTrackingInterceptor_Tests.cs b/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/ChangeTracking/ChangeTrackingInterceptor_Tests.cs index 87b487b68a..9581df3a19 100644 --- a/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/ChangeTracking/ChangeTrackingInterceptor_Tests.cs +++ b/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/ChangeTracking/ChangeTrackingInterceptor_Tests.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; using Shouldly; using Volo.Abp.DependencyInjection; using Volo.Abp.Domain.ChangeTracking; @@ -83,6 +84,96 @@ public class ChangeTrackingInterceptor_Tests : TestAppTestBase>(); + + Guid personId = default; + await WithUnitOfWorkAsync(async () => + { + var p = await repository.FindAsync(x => x.Name == "people1"); + p.ShouldNotBeNull(); + personId = p.Id; + }); + + // Simulate global NoTracking configured on DbContext (e.g. optionsBuilder.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking)) + await WithUnitOfWorkAsync(async () => + { + var db = await repository.GetDbContextAsync(); + db.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking; + db.ChangeTracker.Entries().Count().ShouldBe(0); + + // FindAsync(id): ShouldTrackingEntityChange()=true, GetQueryableAsync() uses AsTracking() to override global NoTracking + var person = await repository.FindAsync(personId, includeDetails: false); + person.ShouldNotBeNull(); + db.ChangeTracker.Entries().Count().ShouldBe(1); + db.Entry(person).State.ShouldBe(EntityState.Unchanged); + }); + + await WithUnitOfWorkAsync(async () => + { + var db = await repository.GetDbContextAsync(); + db.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking; + db.ChangeTracker.Entries().Count().ShouldBe(0); + + // FindAsync(predicate): same - AsTracking() overrides global NoTracking + var person = await repository.FindAsync(x => x.Name == "people1"); + person.ShouldNotBeNull(); + db.ChangeTracker.Entries().Count().ShouldBe(1); + db.Entry(person).State.ShouldBe(EntityState.Unchanged); + }); + + await WithUnitOfWorkAsync(async () => + { + var db = await repository.GetDbContextAsync(); + db.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking; + db.ChangeTracker.Entries().Count().ShouldBe(0); + + // GetListAsync: same - AsTracking() overrides global NoTracking + var list = await repository.GetListAsync(); + list.Count.ShouldBeGreaterThan(0); + db.ChangeTracker.Entries().Count().ShouldBe(list.Count); + }); + } + + [Fact] + public async Task Repository_Should_Respect_NoTracking_When_EntityChangeTracking_Is_Disabled_With_Global_NoTracking() + { + await AddSomePeopleAsync(); + + var repository = GetRequiredService>(); + + Guid personId = default; + await WithUnitOfWorkAsync(async () => + { + var p = await repository.FindAsync(x => x.Name == "people1"); + p.ShouldNotBeNull(); + personId = p.Id; + }); + + await WithUnitOfWorkAsync(async () => + { + var db = await repository.GetDbContextAsync(); + db.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking; + db.ChangeTracker.Entries().Count().ShouldBe(0); + + // When tracking is explicitly disabled, entity should NOT be tracked regardless of global setting + using (repository.DisableTracking()) + { + var person = await repository.FindAsync(personId, includeDetails: false); + person.ShouldNotBeNull(); + db.ChangeTracker.Entries().Count().ShouldBe(0); + + var list = await repository.GetListAsync(); + list.Count.ShouldBeGreaterThan(0); + db.ChangeTracker.Entries().Count().ShouldBe(0); + } + }); + } + private async Task AddSomePeopleAsync() { var repository = GetRequiredService>();