Open Closed

Deleting a entity in a collection from an aggregate root trigger EntityUpdatedEventData on that entity #7029


User avatar
0
JohnJonsson created
  • ABP Framework version: v8.1.0
  • UI Type: Angular
  • Database System: EF Core (SQL Server)
  • Tiered (for MVC) or Auth Server Separated (for Angular): no
  • Exception message and full stack trace:
  • Steps to reproduce the issue:

Create a Aggregate root which have a collection of subentities. Create a method in the aggregate root that remove a subentity from the collection. Then Listen to the built-in events updated and deleted for the subentity.

When the RemoveSubEntity is called both EntityUpdatedEventData and EntityDeletedEventData is triggered

    public class ParentEntity : AggregateRoot<int>
    {
        public virtual List<SubEntity> SubEntities { get; set; }

        public void RemoveSubEntity(int id)
        {
            var subEntity = SubEntities.FirstOrDefault(x => x.Id == id);
            if(subEntity != null) 
                SubEntities.Remove(subEntity);
        }
    }
    ...
    public class SubEntity : Entity<int>
    {
        public int ParentEntityId { get; set; }
    }
    ...
    public class ExamplesEventHandler : ITransientDependency, ILocalEventHandler<EntityUpdatedEventData<SubEntity>>, ILocalEventHandler<EntityDeletedEventData<SubEntity>>
    {
        public async Task HandleEventAsync(EntityUpdatedEventData<SubEntity> eventData)
        {
        }

        public async Task HandleEventAsync(EntityDeletedEventData<SubEntity> eventData)
        {
        }
    }

This behaviour have changed from abp version 4.4.2. In that version only the EntityDeletedEventData is triggered. Which in turn breaks the code our existing functionality when we uppgrade to (8.1.0 or 7.3.3 which also has the same problem).

Why is the EntityUpdatedEventData triggered when removing an entity in a collection from a aggregate root? The Documentation states this

  • EntityUpdatedEventData<T> is published just after an entity was successfully updated.
  • EntityDeletedEventData<T> is published just after an entity was successfully deleted.
        public void RemoveSubEntity(int id)
        {
            var subEntity = SubEntities.FirstOrDefault(x => x.Id == id);
            if(subEntity != null) 
                SubEntities.Remove(subEntity);
        }

I cant se why the subEntity int the code above would be updated

Best regards John


10 Answer(s)
  • User Avatar
    0
    maliming created
    Support Team Fullstack Developer

    hi

    When the RemoveSubEntity is called both EntityUpdatedEventData and EntityDeletedEventData is triggered

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

    Thanks

  • User Avatar
    0
    JohnJonsson created

    hi

    When the RemoveSubEntity is called both EntityUpdatedEventData and EntityDeletedEventData is triggered

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

    Thanks

    I have sent a link to a github repository to your mail

  • User Avatar
    0
    maliming created
    Support Team Fullstack Developer

    hi

    Please make this repo private I have downloaded the sourecode.

    Thanks.

  • User Avatar
    0
    maliming created
    Support Team Fullstack Developer

    hi

    Try to override the protected override void PublishEventsForTrackedEntity(EntityEntry entry) method.

    The key changes are:

    if (entry.Properties.Where(x => x.IsModified).All(x => x.Metadata.IsForeignKey()))
    {
        // Skip if only foreign keys are changed.
        break;
    }
    
    using System;
    using System.Linq;
    using Microsoft.EntityFrameworkCore;
    using Volo.Abp.AuditLogging.EntityFrameworkCore;
    using Volo.Abp.BackgroundJobs.EntityFrameworkCore;
    using Volo.Abp.BlobStoring.Database.EntityFrameworkCore;
    using Volo.Abp.Data;
    using Volo.Abp.DependencyInjection;
    using Volo.Abp.EntityFrameworkCore;
    using Volo.Abp.FeatureManagement.EntityFrameworkCore;
    using Volo.Abp.Identity;
    using Volo.Abp.Identity.EntityFrameworkCore;
    using Volo.Abp.LanguageManagement.EntityFrameworkCore;
    using Volo.Abp.PermissionManagement.EntityFrameworkCore;
    using Volo.Abp.SettingManagement.EntityFrameworkCore;
    using Volo.Abp.TextTemplateManagement.EntityFrameworkCore;
    using Volo.Saas.EntityFrameworkCore;
    using Volo.Saas.Editions;
    using Volo.Saas.Tenants;
    using Volo.Abp.Gdpr;
    using Volo.Abp.OpenIddict.EntityFrameworkCore;
    using BugTracker.Examples;
    using Microsoft.EntityFrameworkCore.ChangeTracking;
    using Microsoft.EntityFrameworkCore.Metadata;
    using Volo.Abp;
    using Volo.Abp.EntityFrameworkCore.Modeling;
    
    namespace BugTracker.EntityFrameworkCore;
    
    [ReplaceDbContext(typeof(IIdentityProDbContext))]
    [ReplaceDbContext(typeof(ISaasDbContext))]
    [ConnectionStringName("Default")]
    public class BugTrackerDbContext :
        AbpDbContext<BugTrackerDbContext>,
        IIdentityProDbContext,
        ISaasDbContext
    {
        /* Add DbSet properties for your Aggregate Roots / Entities here. */
    
        public DbSet<ParentEntity> ParentEntities { get; set; }
    
        #region Entities from the modules
    
        /* Notice: We only implemented IIdentityProDbContext and ISaasDbContext
         * and replaced them for this DbContext. This allows you to perform JOIN
         * queries for the entities of these modules over the repositories easily. You
         * typically don't need that for other modules. But, if you need, you can
         * implement the DbContext interface of the needed module and use ReplaceDbContext
         * attribute just like IIdentityProDbContext and ISaasDbContext.
         *
         * More info: Replacing a DbContext of a module ensures that the related module
         * uses this DbContext on runtime. Otherwise, it will use its own DbContext class.
         */
    
        // Identity
        public DbSet<IdentityUser> Users { get; set; }
        public DbSet<IdentityRole> Roles { get; set; }
        public DbSet<IdentityClaimType> ClaimTypes { get; set; }
        public DbSet<OrganizationUnit> OrganizationUnits { get; set; }
        public DbSet<IdentitySecurityLog> SecurityLogs { get; set; }
        public DbSet<IdentityLinkUser> LinkUsers { get; set; }
        public DbSet<IdentityUserDelegation> UserDelegations { get; set; }
        public DbSet<IdentitySession> Sessions { get; set; }
    
        // SaaS
        public DbSet<Tenant> Tenants { get; set; }
        public DbSet<Edition> Editions { get; set; }
        public DbSet<TenantConnectionString> TenantConnectionStrings { get; set; }
    
        #endregion
    
        public BugTrackerDbContext(DbContextOptions<BugTrackerDbContext> options)
            : base(options)
        {
    
        }
    
        protected override void OnModelCreating(ModelBuilder builder)
        {
            base.OnModelCreating(builder);
    
            /* Include modules to your migration db context */
    
            builder.ConfigurePermissionManagement();
            builder.ConfigureSettingManagement();
            builder.ConfigureBackgroundJobs();
            builder.ConfigureAuditLogging();
            builder.ConfigureIdentityPro();
            builder.ConfigureOpenIddictPro();
            builder.ConfigureFeatureManagement();
            builder.ConfigureLanguageManagement();
            builder.ConfigureSaas();
            builder.ConfigureTextTemplateManagement();
            builder.ConfigureBlobStoring();
            builder.ConfigureGdpr();
    
            /* Configure your own tables/entities inside here */
    
            //builder.Entity<YourEntity>(b =>
            //{
            //    b.ToTable(BugTrackerConsts.DbTablePrefix + "YourEntities", BugTrackerConsts.DbSchema);
            //    b.ConfigureByConvention(); //auto configure for the base class props
            //    //...
            //});
    
            builder.Entity<ParentEntity>(b =>
            {
                b.ToTable(BugTrackerConsts.DbTablePrefix + "ParentEntities",
                    BugTrackerConsts.DbSchema);
                b.ConfigureByConvention(); //auto configure for the base class props
    
                b.HasMany(x => x.SubEntities).WithOne();
            });
    
            builder.Entity<SubEntity>(b =>
            {
                b.ToTable(BugTrackerConsts.DbTablePrefix + "SubEntities",
                    BugTrackerConsts.DbSchema);
                b.ConfigureByConvention(); //auto configure for the base class props
            });
        }
        
        protected override void PublishEventsForTrackedEntity(EntityEntry entry)
        {
            switch (entry.State)
            {
                case EntityState.Added:
                    ApplyAbpConceptsForAddedEntity(entry);
                    EntityChangeEventHelper.PublishEntityCreatedEvent(entry.Entity);
                    break;
    
                case EntityState.Modified:
                    ApplyAbpConceptsForModifiedEntity(entry);
                    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 if only foreign keys are changed.
                            break;
                        }
                        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;
            }
    
            if (EntityChangeOptions.Value.PublishEntityUpdatedEventWhenNavigationChanges)
            {
                foreach (var entityEntry in ChangeTracker.Entries().Where(x => x.State == EntityState.Unchanged && AbpEfCoreNavigationHelper.IsEntityEntryNavigationChanged(x)))
                {
                    if (entityEntry.Entity is ISoftDelete && entityEntry.Entity.As<ISoftDelete>().IsDeleted)
                    {
                        EntityChangeEventHelper.PublishEntityDeletedEvent(entityEntry.Entity);
                    }
                    else
                    {
                        EntityChangeEventHelper.PublishEntityUpdatedEvent(entityEntry.Entity);
                    }
                }
            }
        }
    }
    
    
  • User Avatar
    0
    maliming created
    Support Team Fullstack Developer

    I will try to find a better way.

  • User Avatar
    0
    JohnJonsson created

    Hi

    I have tested to override PublishEventsForTrackedEntity with the code you supplied but it didn´t work unfortunately

    Br John

  • User Avatar
    0
    maliming created
    Support Team Fullstack Developer

    hi

    I updated the code. Can you test the latest one?

  • User Avatar
    0
    maliming created
    Support Team Fullstack Developer

    The PR of ABP framework.

    https://github.com/abpframework/abp/pull/19523

  • User Avatar
    0
    JohnJonsson created

    hi

    I updated the code. Can you test the latest one?

    The latest code worked for me :)

    The PR of ABP framework.

    https://github.com/abpframework/abp/pull/19523

    Do you have a date or when you think at earliest when this PR will be in an official release?

    Br John

  • User Avatar
    0
    maliming created
    Support Team Fullstack Developer

    hi

    I think it will be released within 2 weeks.

    You can override it now.

    Thanks

Made with ❤️ on ABP v8.2.0-preview Updated on March 25, 2024, 15:11