Open Closed

Audit property setter stamps `LastModificationTime`/`LastModifierId` on unrelated tracked entities that have no real (non-audit) property changes after migrating from abp 8.2.3 to abo 10.5.0 #10874


User avatar
0
priyankasynapxe created

Migrated ABP Framework- 8.2.3 t0 ABP Framework- 10.5.0 and .NET 8 to NET Framework to .NET 10.

After upgrading our project's .NET version and ABP packages (and separately migrating AutoMapper → Mapperly, which we've ruled out as the cause), we observed that entities which are not explicitly updated in the current unit of work are still getting their audit columns (LastModificationTime, LastModifierId) stamped and persisted on SaveChanges().

Repro scenario

  1. In a single unit of work, we call _financialCounselingRepository.InsertAsync(financialCounseling, autoSave: true) — inserting a new FinancialCounseling (a FullAuditedEntity<long>).
  2. Earlier in the same unit of work, we load an unrelated/loosely-related Patient entity (also FullAuditedEntity<long>) via a repository read (tried both tracked and AsNoTracking() — issue persists once the entity re-enters the change tracker through navigation fixup).
  3. We do not call UpdateAsync on Patient, nor modify any of its business properties.
  4. On SaveChanges(), we instrumented ChangeTracker.Entries() right before EF Core's save call and observed:
    • Patient → EntityState.Modified, with only LastModificationTime reported as an IsModified property.
    • FinancialCounseling → EntityState.Modified, with only LastModificationTime,LastModifierId reported as IsModified.
    • Other entities in the same save batch (e.g. Encounter, TransactionLocking_TR) correctly show a full, expected set of business properties as modified — confirming those are legitimate changes.
  5. Result: Patient and FinancialCounseling rows get a LastModificationTime/LastModifierId update in the database even though no business column changed and no explicit update call was made against them.

What we expect

Entities that are EntityState.Modified purely because EF Core's DetectChanges()/navigation fixup flagged them (with zero non-audit, non-concurrency-stamp properties actually dirty) should not be audit-stamped by ABP's IAuditPropertySetter. Today, AuditPropertySetter (called from AbpDbContext's SaveChanges pipeline / HandlePropertiesBeforeSave) appears to stamp any entry with EntityState.Modified implementing IHasModificationTime, without checking whether any actual business property is dirty.

Questions for the ABP team

  1. Is this expected/by-design behavior — i.e., does AuditPropertySetter.SetModificationAuditProperties intentionally stamp any Modified entry regardless of which properties changed?
  2. Is there a supported/documented way to make ABP skip audit stamping when an entity's only dirty properties are audit/concurrency-stamp columns (LastModificationTime, LastModifierId, ConcurrencyStamp)?
  3. Could a recent EF Core or ABP version change have made DetectChanges()/relationship-fixup more aggressive about marking loaded-but-unmodified entities as Modified (e.g. due to shadow FK re-evaluation, concurrency token comparison, or navigation collection fixup)? We did not see this behavior prior to the .NET/ABP upgrade with the same code paths.
  4. Is IAuditPropertySetter the correct/supported extension point to override this (e.g., replacing the DI registration with a custom implementation that checks for non-audit dirty properties before stamping), or is there a more idiomatic ABP configuration option (e.g. under AbpAuditingOptions) we're missing?

What we've tried

  • Verified our Mapperly mapping profiles already Ignore() all audit-related members (Id, CreationTime, CreatorId, LastModificationTime, LastModifierId) on create/update DTO→entity mappings — ruled out as cause.
  • Applied AsNoTracking() to the relevant repository read methods — did not resolve the issue (entity still ends up Modified in the tracker by the time SaveChanges runs, likely via navigation fixup from a related tracked aggregate).
  • As a workaround, we added a guard in our custom AppDbContext.HandlePropertiesBeforeSave() override that detects entries where all IsModified properties are in {"LastModificationTime", "LastModifierId", "ConcurrencyStamp"} and resets entry.State = EntityState.Unchanged before calling UpdateConcurrencyStamp(entry). This resolves the symptom but feels like it's masking a framework-level behavior we don't fully understand.

Additional context

  • Entities involved: Patient : FullAuditedEntity<long>, FinancialCounseling : FullAuditedEntity<long> (has ICollection<FCEncounterRelation> FCEncounters navigation).
  • Child entity FCEncounterRelation (also FullAuditedEntity<long>) does not exhibit this false-positive — its modification-time column stays untouched in the same save batch, which is somewhat inconsistent with the parent aggregates being falsely flagged.
  • We'd appreciate confirmation on whether ABP intentionally propagates audit stamping to any Modified-state entity in the same SaveChanges batch regardless of relation to the actually-changed root entity, and whether there's a supported switch to opt out per-property rather than per-entity.
Markdown supported.
Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post)

2 Answer(s)
  • User Avatar
    0
    AI-Bot created
    Support Team ABP Agent
    • (1) Is this expected/by-design?
      In ABP’s EF Core save pipeline, modification audit properties are applied based on EntityEntry.State == Modified, not based on “did any non-audit business property change”. So if EF Core marks an entity as Modified (even due to fixup/DetectChanges side effects), ABP will set LastModificationTime / LastModifierId for that entry. There is no documented built-in rule that says “skip stamping if only audit/concurrency properties are dirty”.

    • (2) Supported/documented way to skip audit stamping when only audit/concurrency props are dirty?
      ABP provides a property-level mechanism, but it is for a different scenario: you can mark a specific property with [DisableAuditing(UpdateModificationProps = false)] so changing that property won’t update modification audit props. This does not directly solve your case (where the entity is already Modified for reasons you don’t want).
      Example from docs:

    public class MyUser : Entity<Guid>
      {
          public string Name { get; set; }
    
          [DisableAuditing(UpdateModificationProps = false, PublishEntityEvent = false)]
          public string ReadCount { get; set; }
      }
    

    In your scenario (only audit props appear modified), there is no documented ABP option that automatically ignores/rolls back such “audit-only modifications” at the entity level.

    • (3) Could EF Core / ABP changes be making this more visible?
      Yes, this symptom is consistent with EF Core deciding an entity is Modified due to relationship fixup / FK synchronization / DetectChanges behavior changes across versions. Once EF Core marks Patient as Modified, ABP will treat it as a real update and stamp audit props. ABP itself also has options around “navigation changes” for entity change events, which indicates the framework has had to account for EF Core navigation/FK change detection differences:

      • PublishEntityUpdatedEventWhenNavigationChanges (default true) and related behavior are documented under entity change events.
    • (4) Correct extension point?
      Given there’s no documented switch for “audit-only modified entries”, the most deterministic approach is exactly what you already implemented: intercept in your DbContext save pipeline (e.g., HandlePropertiesBeforeSave) and reset the entry to Unchanged when the only modified properties are audit/concurrency-stamp ones.
      Overriding/replacing IAuditPropertySetter could work, but ABP doesn’t document it as a supported “filter-out audit-only modifications” feature, and without ABP source verification available here, I can’t provide a safe/accurate replacement registration snippet.

    Validated references (closest official knobs relevant to this behavior):

    • ABP Audit Logging docs (property-level DisableAuditing(UpdateModificationProps = false)): https://abp.io/docs/10.5/framework/infrastructure/audit-logging
    • ABP entity base classes/audit properties overview: https://abp.io/docs/10.5/framework/architecture/domain-driven-design/entities
    • ABP entity change event option re navigation changes (PublishEntityUpdatedEventWhenNavigationChanges): https://abp.io/docs/10.5/framework/infrastructure/event-bus/local/
    • Related support thread showing ABP/EFC navigation-change handling touchpoints: https://abp.io/support/questions/10057

    This response was generated by AI. If it didn’t solve your problem, feel free to post a new answer — our support team will jump in to help you soon.

    Markdown supported.
    Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post)
  • User Avatar
    0
    maliming created
    Support Team Fullstack Developer

    Hi,

    ABP updates the principal entity when one of its related entities is added, updated or deleted: it stamps the modification audit properties on it even when none of its own properties changed. That's why the entry ends up Modified with only the audit columns dirty. It started in 8.3.0, so you didn't hit it on 8.2.3. It's controlled by the UpdateAggregateRootWhenNavigationChanges option, which is true by default.

    Keep the option on and list the entities you want to skip:

    Configure<AbpEntityChangeOptions>(options =>
    {
        options.IgnoredUpdateAggregateRootSelectors.Add("Patient", type => type == typeof(Patient));
        options.IgnoredUpdateAggregateRootSelectors.Add("FinancialCounseling", type => type == typeof(FinancialCounseling));
    });
    

    AbpEntityChangeOptions is in Volo.Abp.Domain.Entities.Events. The entity updated event is still published for these entities, and the extra ConcurrencyStamp update is skipped as well, so you can drop the guard from your HandlePropertiesBeforeSave override.

    There is also a global UpdateAggregateRootWhenNavigationChanges = false, but I wouldn't turn that one off. It applies to every aggregate root in the application, including the ones coming from the ABP modules, so none of them gets its audit properties and concurrency stamp updated anymore when only its child collections change.

    There is no reason to replace IAuditPropertySetter here, it doesn't get the EF entry so it can't tell which properties are dirty.

    Two related changes in 10.7: unitOfWork.DisableUpdateAggregateRootWhenNavigationChanges() disables it for a single unit of work, and the principal is now updated only when it really has a navigation property for the changed relation. So if nothing but the foreign key ties FinancialCounseling to Patient, 10.7 stops stamping Patient without any configuration.

    Thanks

    Markdown supported.
    Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post)
Boost Your Development
ABP Live Training
Packages
See Trainings
Mastering ABP Framework Book
The Official Guide
Mastering
ABP Framework
Learn More
Mastering ABP Framework Book
Made with ❤️ on ABP v10.8.0-preview. Updated on September 28, 2026, 11:44
1
ABP Assistant
🔐 You need to be logged in to use the chatbot. Please log in first.