BEST
DEALS
OF THE
YEAR!
SAVE UP TO $3,000
24 NOV
1 DEC
00 Days
00 Hrs
00 Min
00 Sec
Open Closed

LastModification fields not updated when changing one-to-many relationship #10057


User avatar
0
tim.delandtsheer created

Hi,

We're using ABP with EFCore to SQL Server (using an Angular frontend). All our entities are FullAuditedXXXWithUser<long, IdentityUser>. The LastModificationTime is not always updated, specially when updating a relationship.

For example,: we have a book with one author (one-to-many. Book has a field long? AuthorId {get;set;}). When updating some fields from book, the modification fields are updated. When only updating the author (choosing a new author from the dropdown), the modification fields are not updated. We expected those auditfields to be updated.

Is there some (undocumented) configuration we need to do, or a bug?

We've created a new simple ABP project to reproduce, and the same behaviour happens.

  • Template: app
  • Created ABP Studio Version: 1.1.2
  • Current ABP Studio Version: 1.4.1
  • Tiered: No
  • Multi-Tenancy: No
  • UI Framework: angular
  • Theme: leptonx
  • Theme Style: system
  • Theme Menu Placement: side
  • Run Install Libs: Yes
  • Progressive Web App: No
  • Run Progressive Web App Support: No
  • Database Provider: ef
  • Database Management System: sqlserver
  • Separate Tenant Schema: No
  • Create Initial Migration: Yes
  • Run Db Migrator: Yes
  • Mobile Framework: none
  • Public Website: No
  • Social Login: No
  • Include Tests: Yes
  • Kubernetes Configuration: No
  • Distributed Event Bus: none
  • Use Local References: No
  • Optional Modules:
    • GDPR
    • TextTemplateManagement
    • LanguageManagement
    • AuditLogging
    • OpenIddictAdmin
  • Selected Languages: Dutch, English, Français
  • Default Language: Dutch
  • Create Command: abp new AbpShowcase -t app --ui-framework angular --database-provider ef --database-management-system sqlserver --theme leptonx --without-cms-kit --sample-crud-page --dont-run-bundling --no-multi-tenancy --no-social-logins -no-saas -no-file-management
  • Exception message and full stack trace: NA
  • Steps to reproduce the issue:
    • In a new ABP Project (Angular, EF Core to SQL Server) and create two FullAuditedAggregates with a one-to-many relationship (eg. a book has one author)
      • The should be a field public long? AuthorId {get;set;}
    • On the detailscreen of book, choose another author. (update only this one-to-many property!)
    • ==> Auditfields LastModificationTime and LastModifier are not updated.

12 Answer(s)
  • User Avatar
    0
    AI-Bot created
    Support Team ABP Agent
    1. Solution
    • Root cause

      • In ABP, modification audit properties (LastModificationTime/LastModifierId) are set by ApplyAbpConcepts during SaveChanges only when EF Core reports the entity as Modified. Historically, changing only a navigation/foreign key could be missed by EF change detection, so audit fields were not updated.
      • From ABP 8.3 onward, the framework explicitly updates ModificationAuditProperties when a navigation property changes. On earlier versions (like 9.2.3 is newer than 8.3, so it already includes this behavior), you should get updates if the FK or navigation change is tracked as a modification within the same DbContext/Unit of Work.
    • What to verify in your app

      1. Ensure updates happen inside a Unit of Work and the same DbContext instance tracks the entity and the FK change before SaveChanges.
        • Use repository.GetAsync(id), mutate tracked entity’s AuthorId (or navigation), then await repository.UpdateAsync(entity, autoSave: true) or rely on UoW completion.
      2. Change the FK scalar property, not only the navigation proxy:
    var book = await _bookRepository.GetAsync(id);
         book.AuthorId = input.AuthorId; // set the FK scalar
         // or set book.Author = await _authorRepository.GetAsync(input.AuthorId);
         await _bookRepository.UpdateAsync(book, autoSave: true);
    
    1. Do not use ExecuteUpdate/ExecuteDelete bulk operations for this scenario; they bypass EF tracking, so ABP auditing (including modification audit fields) will not run.
    2. Make sure CurrentUser is available if you expect LastModifierId. If the operation is AllowAnonymous or executed by background processes, LastModifierId may be null unless you set a principal:
    var claims = new List<Claim> { new Claim(AbpClaimTypes.UserId, userId.ToString()) };
         using (_currentPrincipalAccessor.Change(claims))
         {
             await _bookRepository.UpdateAsync(book, autoSave: true);
         }
    
    1. If you’re attaching a detached entity (e.g., mapping DTO to new entity instance and calling UpdateAsync), ensure EF knows which properties changed. Prefer fetch-and-mutate; otherwise you may need to mark the FK property as modified on the DbContext entry.
    • ABP 8.3+ behavior (applies to 9.2.3)

      • ABP updates ModificationAuditProperties when navigation properties change. If you still see no update:
        • Confirm that AuthorId change results in EF change tracker marking the entity as Modified.
        • Confirm the change is not done via raw SQL or bulk update APIs.
        • Confirm the update path is not suppressing ApplyAbpConcepts (e.g., custom DbContext SaveChanges override that skips base behavior).
    • Minimal working example

    public class BookAppService : ApplicationService
      {
          private readonly IRepository<Book, long> _bookRepository;
    
          public BookAppService(IRepository<Book, long> bookRepository)
          {
              _bookRepository = bookRepository;
          }
    
          public async Task UpdateAuthorAsync(long id, long? newAuthorId)
          {
              var book = await _bookRepository.GetAsync(id); // tracked
              book.AuthorId = newAuthorId;
              await _bookRepository.UpdateAsync(book, autoSave: true); // triggers ApplyAbpConcepts
              // After save, LastModificationTime and LastModifierId should be updated (if CurrentUser.Id exists)
          }
      }
    
    • If you must use detached updates
    var book = await _bookRepository.GetAsync(id);
      _ = ObjectMapper.Map(input, book); // ensure AuthorId ends up different
      await _bookRepository.UpdateAsync(book, autoSave: true);
    
    • Summary
      • With ABP 9.2.3, changing a FK/navigation should update LastModificationTime/LastModifierId provided:
        • The change is tracked by EF in a normal UoW (not bulk ops/raw SQL).
        • You change the entity instance fetched from repository (tracked).
        • CurrentUser is set if you expect LastModifierId.
      • If your code path still does not update the audit fields, refactor to fetch-and-mutate within the repository/UoW as shown above.
    1. Sources
    • https://abp.io/docs/latest/release-info/migration-guides/abp-8-3
    • https://abp.io/support/questions/6298
    • https://abp.io/community/articles/zvr01mtn

    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.

  • User Avatar
    0
    tim.delandtsheer created

    Hi,

    We already tried suggestions of the AI-bot .

    • We are updating by ID. (no navigation property). In EF Core, the Id is configured as a foreign key by default.
    • We are using the default generated code from ABP-Suite. So tracked entity, explicit update. Not directly on the database.
  • User Avatar
    0
    maliming created
    Support Team Fullstack Developer

    hi

    We've created a new simple ABP project to reproduce, and the same behaviour happens.

    Can you share the test project to liming.ma@volosoft.com?

    Thanks.

  • User Avatar
    0
    maliming created
    Support Team Fullstack Developer

    hi

    I have downloaded the AbpShowcase project. Can you share the steps?

    Thanks.

  • User Avatar
    0
    tim.delandtsheer created

    Hi, some steps for the test-project to reproduce

    Getting project ready (its a simple ABP Starter)

    1. Instal libs (nuget, yarn)
    2. Run SQL-migrations (DbMigrator)
    3. Run Application (Host)

    Reproduce issue

    1. Navigate to "Genres" and create 2 to 3 genres. They are required to select in the next steps.
    2. Navigate to "Books" and create a new book. Fill in a single genre in the dropdown and some other fields. In the database, creation-audit fields are filled in. (Expected)
    3. Open the edit modal of the book, edit some fields and save. In the database, modification-audit fields are filled in. (Expected)
    4. Open the edit modal again and ONLY update the genre dropdown: pick another value and save. In the database, modification-audit fields are not changed. We expect them to be updated, because the field GenreId is updated.
  • User Avatar
    0
    maliming created
    Support Team Fullstack Developer

    hi

    The relationship of Genres and Movies is Many-to-Many instead of One-To-Many

  • User Avatar
    0
    tim.delandtsheer created

    Hi

    We have both a many-to-many and a one-to-many on our test-entity, as shown on the screenshot below. The bug is about the one-to-many, when changing the dropdown on the "main"-tab.

  • User Avatar
    0
    maliming created
    Support Team Fullstack Developer

    hi

    I will check it again.

    Thanks.

  • User Avatar
    0
    maliming created
    Support Team Fullstack Developer

    hi

    Please try to override the PublishEventsForChangedEntityOnSaveChangeAsync and PublishEventsForTrackedEntity methods and test again.

    Thanks.

    [ReplaceDbContext(typeof(IIdentityProDbContext))]
    [ConnectionStringName("Default")]
    public class AbpShowcaseDbContext :
        AbpDbContext<AbpShowcaseDbContext>,
        IIdentityProDbContext
    {
    
        public AbpShowcaseDbContext(DbContextOptions<AbpShowcaseDbContext> options)
            : base(options)
        {
    
        }
    
        protected override Task PublishEventsForChangedEntityOnSaveChangeAsync()
        {
            foreach (var entityEntry in AbpEfCoreNavigationHelper.GetChangedEntityEntries())
            {
                if (EntityChangeOptions.Value.PublishEntityUpdatedEventWhenNavigationChanges)
                {
                    var ignoredEntity = EntityChangeOptions.Value.IgnoredNavigationEntitySelectors.Any(selector => selector.Predicate(entityEntry.Entity.GetType()));
                    var onlyForeignKeyModifiedEntity = entityEntry.State == EntityState.Modified && entityEntry.Properties.Where(x => x.IsModified).All(x => x.Metadata.IsForeignKey());
    
                    if ((entityEntry.State == EntityState.Unchanged && ignoredEntity) || ignoredEntity)
                    {
                        continue;
                    }
    
                    if (EntityChangeOptions.Value.UpdateAggregateRootWhenNavigationChanges &&
                        EntityChangeOptions.Value.IgnoredUpdateAggregateRootSelectors.All(selector => !selector.Predicate(entityEntry.Entity.GetType())) &&
                        entityEntry.State == EntityState.Unchanged)
                    {
                        ApplyAbpConceptsForModifiedEntity(entityEntry, true);
                    }
    
                    if (entityEntry.Entity is ISoftDelete && entityEntry.Entity.As<ISoftDelete>().IsDeleted)
                    {
                        EntityChangeEventHelper.PublishEntityDeletedEvent(entityEntry.Entity);
                    }
                    else
                    {
                        EntityChangeEventHelper.PublishEntityUpdatedEvent(entityEntry.Entity);
                    }
                }
                else if (entityEntry.Properties.Any(x => x.IsModified && (x.Metadata.ValueGenerated == ValueGenerated.Never || x.Metadata.ValueGenerated == ValueGenerated.OnAdd)))
                {
                    if (entityEntry.Properties.Where(x => x.IsModified).All(x => x.Metadata.IsForeignKey()))
                    {
                        // Skip `PublishEntityDeletedEvent/PublishEntityUpdatedEvent` if only foreign keys have changed.
                        break;
                    }
    
                    if (entityEntry.Entity is ISoftDelete && entityEntry.Entity.As<ISoftDelete>().IsDeleted)
                    {
                        EntityChangeEventHelper.PublishEntityDeletedEvent(entityEntry.Entity);
                    }
                    else
                    {
                        EntityChangeEventHelper.PublishEntityUpdatedEvent(entityEntry.Entity);
                    }
                }
            }
    
            return Task.CompletedTask;
        }
    
        protected override void PublishEventsForTrackedEntity(EntityEntry entry)
        {
            switch (entry.State)
            {
                case EntityState.Added:
                    ApplyAbpConceptsForAddedEntity(entry);
                    EntityChangeEventHelper.PublishEntityCreatedEvent(entry.Entity);
                    break;
    
                case EntityState.Modified:
                    if (entry.Properties.Any(x => x.IsModified && (x.Metadata.ValueGenerated == ValueGenerated.Never || x.Metadata.ValueGenerated == ValueGenerated.OnAdd)))
                    {
                        // if (entry.Properties.Where(x => x.IsModified).All(x => x.Metadata.IsForeignKey()))
                        // {
                        //     // Skip `PublishEntityDeletedEvent/PublishEntityUpdatedEvent` if only foreign keys have changed.
                        //     break;
                        // }
    
                        ApplyAbpConceptsForModifiedEntity(entry);
                        if (entry.Entity is ISoftDelete && entry.Entity.As<ISoftDelete>().IsDeleted)
                        {
                            EntityChangeEventHelper.PublishEntityDeletedEvent(entry.Entity);
                        }
                        else
                        {
                            EntityChangeEventHelper.PublishEntityUpdatedEvent(entry.Entity);
                        }
                    }
                    else if (EntityChangeOptions.Value.PublishEntityUpdatedEventWhenNavigationChanges &&
                             EntityChangeOptions.Value.IgnoredNavigationEntitySelectors.All(selector => !selector.Predicate(entry.Entity.GetType())) &&
                             AbpEfCoreNavigationHelper.IsNavigationEntryModified(entry))
                    {
                        if (EntityChangeOptions.Value.UpdateAggregateRootWhenNavigationChanges &&
                            EntityChangeOptions.Value.IgnoredUpdateAggregateRootSelectors.All(selector => !selector.Predicate(entry.Entity.GetType())))
                        {
                            ApplyAbpConceptsForModifiedEntity(entry, true);
                        }
    
                        if (entry.Entity is ISoftDelete && entry.Entity.As<ISoftDelete>().IsDeleted)
                        {
                            EntityChangeEventHelper.PublishEntityDeletedEvent(entry.Entity);
                        }
                        else
                        {
                            EntityChangeEventHelper.PublishEntityUpdatedEvent(entry.Entity);
                        }
                    }
                    break;
    
                case EntityState.Deleted:
                    ApplyAbpConceptsForDeletedEntity(entry);
                    EntityChangeEventHelper.PublishEntityDeletedEvent(entry.Entity);
                    break;
            }
        }
    
    
  • User Avatar
    0
    tim.delandtsheer created

    Hi,

    It seems to work with the given overrides. However, it's sad that by implementing these overrides we have the risks of possible missing feature updates and even future breaking changes.

    Is this a temporary fix until a stable solution in the packages of ABP? Or will it be configurable in a next version?

  • User Avatar
    0
    maliming created
    Support Team Fullstack Developer

    hi

    We will improve it in the next 9.3.x patch version.

    Thanks.

  • User Avatar
    0
    tim.delandtsheer created

    Okay thanks!

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.1.0-preview. Updated on November 20, 2025, 09:12
1
ABP Assistant
🔐 You need to be logged in to use the chatbot. Please log in first.