Skip to content

Commit 88bfa81

Browse files
committed
Improve foreign key change detection in domain events
https://abp.io/support/questions/10057
1 parent f58dffa commit 88bfa81

File tree

8 files changed

+127
-8
lines changed

8 files changed

+127
-8
lines changed

framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/AbpDbContext.cs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -268,7 +268,7 @@ protected virtual Task PublishEventsForChangedEntityOnSaveChangeAsync()
268268
if (EntityChangeOptions.Value.PublishEntityUpdatedEventWhenNavigationChanges)
269269
{
270270
var ignoredEntity = EntityChangeOptions.Value.IgnoredNavigationEntitySelectors.Any(selector => selector.Predicate(entityEntry.Entity.GetType()));
271-
var onlyForeignKeyModifiedEntity = entityEntry.State == EntityState.Modified && entityEntry.Properties.Where(x => x.IsModified).All(x => x.Metadata.IsForeignKey());
271+
var onlyForeignKeyModifiedEntity = entityEntry.State == EntityState.Modified && entityEntry.Properties.Where(x => x.IsModified).All(x => x.Metadata.IsForeignKey() && (x.CurrentValue == null || x.OriginalValue?.ToString() == x.CurrentValue?.ToString()));
272272
if ((entityEntry.State == EntityState.Unchanged && ignoredEntity) || onlyForeignKeyModifiedEntity && ignoredEntity)
273273
{
274274
continue;
@@ -292,7 +292,7 @@ protected virtual Task PublishEventsForChangedEntityOnSaveChangeAsync()
292292
}
293293
else if (entityEntry.Properties.Any(x => x.IsModified && (x.Metadata.ValueGenerated == ValueGenerated.Never || x.Metadata.ValueGenerated == ValueGenerated.OnAdd)))
294294
{
295-
if (entityEntry.Properties.Where(x => x.IsModified).All(x => x.Metadata.IsForeignKey()))
295+
if (entityEntry.Properties.Where(x => x.IsModified).All(x => x.Metadata.IsForeignKey() && (x.CurrentValue == null || x.OriginalValue?.ToString() == x.CurrentValue?.ToString())))
296296
{
297297
// Skip `PublishEntityDeletedEvent/PublishEntityUpdatedEvent` if only foreign keys have changed.
298298
break;
@@ -428,7 +428,8 @@ protected virtual void PublishEventsForTrackedEntity(EntityEntry entry)
428428
case EntityState.Modified:
429429
if (entry.Properties.Any(x => x.IsModified && (x.Metadata.ValueGenerated == ValueGenerated.Never || x.Metadata.ValueGenerated == ValueGenerated.OnAdd)))
430430
{
431-
if (entry.Properties.Where(x => x.IsModified).All(x => x.Metadata.IsForeignKey()))
431+
var modifiedProperties = entry.Properties.Where(x => x.IsModified).ToList();
432+
if (modifiedProperties.All(x => x.Metadata.IsForeignKey() && (x.CurrentValue == null || x.OriginalValue?.ToString() == x.CurrentValue?.ToString())))
432433
{
433434
// Skip `PublishEntityDeletedEvent/PublishEntityUpdatedEvent` if only foreign keys have changed.
434435
break;

framework/test/Volo.Abp.Auditing.Tests/Volo/Abp/Auditing/App/EntityFrameworkCore/AbpAuditingTestDbContext.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ public class AbpAuditingTestDbContext : AbpDbContext<AbpAuditingTestDbContext>
2828
public DbSet<AppEntityWithValueObject> AppEntityWithValueObject { get; set; }
2929

3030
public DbSet<AppEntityWithNavigations> AppEntityWithNavigations { get; set; }
31-
31+
public DbSet<AppEntityWithNavigationChildOneToMany> AppEntityWithNavigationChildOneToMany { get; set; }
3232
public AbpAuditingTestDbContext(DbContextOptions<AbpAuditingTestDbContext> options)
3333
: base(options)
3434
{

framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/AbpEntityFrameworkCoreTestModule.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,11 @@ public override void ConfigureServices(ServiceConfigurationContext context)
5858
{
5959
opt.DefaultWithDetailsFunc = q => q.Include(p => p.BlogPosts);
6060
});
61+
62+
options.Entity<AppEntityWithNavigationsForeign>(opt =>
63+
{
64+
opt.DefaultWithDetailsFunc = q => q.Include(p => p.OneToMany);
65+
});
6166
});
6267

6368
context.Services.AddAbpDbContext<HostTestAppDbContext>(options =>

framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/DomainEvents/DomainEvents_Tests.cs

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,13 +48,15 @@ protected override void AfterAddApplication(IServiceCollection services)
4848
public class AbpEfCoreDomainEvents_Tests : EntityFrameworkCoreTestBase
4949
{
5050
protected readonly IRepository<AppEntityWithNavigations, Guid> AppEntityWithNavigationsRepository;
51+
protected readonly IRepository<AppEntityWithNavigationChildOneToMany, Guid> AppEntityWithNavigationChildOneToManyRepository;
5152
protected readonly ILocalEventBus LocalEventBus;
5253
protected readonly IRepository<Person, Guid> PersonRepository;
5354
protected bool _loadEntityWithoutDetails = false;
5455

5556
public AbpEfCoreDomainEvents_Tests()
5657
{
5758
AppEntityWithNavigationsRepository = GetRequiredService<IRepository<AppEntityWithNavigations, Guid>>();
59+
AppEntityWithNavigationChildOneToManyRepository = GetRequiredService<IRepository<AppEntityWithNavigationChildOneToMany, Guid>>();
5860
LocalEventBus = GetRequiredService<ILocalEventBus>();
5961
PersonRepository = GetRequiredService<IRepository<Person, Guid>>();
6062
}
@@ -357,6 +359,22 @@ public async Task Should_Trigger_Domain_Events_For_Aggregate_Root_When_EnsureCol
357359
var entityId = Guid.NewGuid();
358360

359361
await AppEntityWithNavigationsRepository.InsertAsync(new AppEntityWithNavigations(entityId, "TestEntity")
362+
{
363+
OneToMany = new List<AppEntityWithNavigationChildOneToMany>()
364+
{
365+
new AppEntityWithNavigationChildOneToMany(Guid.NewGuid())
366+
{
367+
ChildName = "ChildName1"
368+
},
369+
new AppEntityWithNavigationChildOneToMany(Guid.NewGuid())
370+
{
371+
ChildName = "ChildName2"
372+
}
373+
}
374+
});
375+
376+
var entityId2 = Guid.NewGuid();
377+
await AppEntityWithNavigationsRepository.InsertAsync(new AppEntityWithNavigations(entityId2, "TestEntity")
360378
{
361379
OneToMany = new List<AppEntityWithNavigationChildOneToMany>()
362380
{
@@ -367,6 +385,33 @@ await AppEntityWithNavigationsRepository.InsertAsync(new AppEntityWithNavigation
367385
}
368386
});
369387

388+
var oneToManyEntity = Guid.NewGuid();
389+
await AppEntityWithNavigationChildOneToManyRepository.InsertAsync(
390+
new AppEntityWithNavigationChildOneToMany(oneToManyEntity)
391+
{
392+
AppEntityWithNavigationId = entityId,
393+
});
394+
395+
LocalEventBus.Subscribe<EntityUpdatedEventData<AppEntityWithNavigationChildOneToMany>>(data =>
396+
{
397+
data.Entity.AppEntityWithNavigationId.ShouldBe(entityId2);
398+
return Task.CompletedTask;
399+
});
400+
401+
using (var scope = ServiceProvider.CreateScope())
402+
{
403+
var uowManager = scope.ServiceProvider.GetRequiredService<IUnitOfWorkManager>();
404+
using (var uow = uowManager.Begin())
405+
{
406+
var entity = await AppEntityWithNavigationChildOneToManyRepository.GetAsync(oneToManyEntity);
407+
408+
entity.AppEntityWithNavigationId = entityId2;
409+
await AppEntityWithNavigationChildOneToManyRepository.UpdateAsync(entity);
410+
411+
await uow.CompleteAsync();
412+
}
413+
}
414+
370415
var entityUpdatedEventTriggered = false;
371416

372417
LocalEventBus.Subscribe<EntityUpdatedEventData<AppEntityWithNavigations>>(data =>
@@ -375,6 +420,11 @@ await AppEntityWithNavigationsRepository.InsertAsync(new AppEntityWithNavigation
375420
return Task.CompletedTask;
376421
});
377422

423+
LocalEventBus.Subscribe<EntityUpdatedEventData<AppEntityWithNavigationChildOneToMany>>(data =>
424+
{
425+
throw new Exception("Should not trigger this event");
426+
});
427+
378428
using (var scope = ServiceProvider.CreateScope())
379429
{
380430
var uowManager = scope.ServiceProvider.GetRequiredService<IUnitOfWorkManager>();

framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/TestMigrationsDbContext.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ public class TestMigrationsDbContext : AbpDbContext<TestMigrationsDbContext>
2727
public DbSet<Category> Categories { get; set; }
2828

2929
public DbSet<AppEntityWithNavigations> AppEntityWithNavigations { get; set; }
30+
public DbSet<AppEntityWithNavigationChildOneToMany> AppEntityWithNavigationChildOneToMany { get; set; }
3031

3132
public DbSet<AppEntityWithNavigationsForeign> AppEntityWithNavigationsForeign { get; set; }
3233

@@ -81,7 +82,12 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)
8182
b.HasOne(x => x.OneToOne).WithOne().HasForeignKey<AppEntityWithNavigationChildOneToOne>(x => x.Id);
8283
b.HasMany(x => x.OneToMany).WithOne().HasForeignKey(x => x.AppEntityWithNavigationId);
8384
b.HasMany(x => x.ManyToMany).WithMany(x => x.ManyToMany).UsingEntity<AppEntityWithNavigationsAndAppEntityWithNavigationChildManyToMany>();
84-
b.HasOne<AppEntityWithNavigationsForeign>().WithMany().HasForeignKey(x => x.AppEntityWithNavigationForeignId).IsRequired(false);
85+
});
86+
87+
modelBuilder.Entity<AppEntityWithNavigationsForeign>(b =>
88+
{
89+
b.ConfigureByConvention();
90+
b.HasMany(x => x.OneToMany).WithOne().HasForeignKey(x => x.AppEntityWithNavigationForeignId);
8591
});
8692

8793
modelBuilder.Entity<AppEntityWithNavigationChildOneToOne>(b =>

framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/TestApp/EntityFrameworkCore/TestAppDbContext.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ public class TestAppDbContext : AbpDbContext<TestAppDbContext>, IThirdDbContext,
3434
public DbSet<Category> Categories { get; set; }
3535

3636
public DbSet<AppEntityWithNavigations> AppEntityWithNavigations { get; set; }
37+
public DbSet<AppEntityWithNavigationChildOneToMany> AppEntityWithNavigationChildOneToMany { get; set; }
3738

3839
public DbSet<AppEntityWithNavigationsForeign> AppEntityWithNavigationsForeign { get; set; }
3940

@@ -107,7 +108,12 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)
107108
b.HasOne(x => x.OneToOne).WithOne().HasForeignKey<AppEntityWithNavigationChildOneToOne>(x => x.Id);
108109
b.HasMany(x => x.OneToMany).WithOne().HasForeignKey(x => x.AppEntityWithNavigationId);
109110
b.HasMany(x => x.ManyToMany).WithMany(x => x.ManyToMany).UsingEntity<AppEntityWithNavigationsAndAppEntityWithNavigationChildManyToMany>();
110-
b.HasOne<AppEntityWithNavigationsForeign>().WithMany().HasForeignKey(x => x.AppEntityWithNavigationForeignId).IsRequired(false);
111+
});
112+
113+
modelBuilder.Entity<AppEntityWithNavigationsForeign>(b =>
114+
{
115+
b.ConfigureByConvention();
116+
b.HasMany(x => x.OneToMany).WithOne().HasForeignKey(x => x.AppEntityWithNavigationForeignId);
111117
});
112118

113119
modelBuilder.Entity<AppEntityWithNavigationChildOneToOne>(b =>

framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Domain/AppEntityWithNavigations.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,17 @@ public class AppEntityWithNavigationChildOneToOneAndOneToOne : Entity<Guid>
6565

6666
public class AppEntityWithNavigationChildOneToMany : Entity<Guid>
6767
{
68+
public AppEntityWithNavigationChildOneToMany()
69+
{
70+
71+
}
72+
73+
public AppEntityWithNavigationChildOneToMany(Guid id)
74+
: base(id)
75+
{
76+
77+
}
78+
6879
public Guid AppEntityWithNavigationId { get; set; }
6980

7081
public string ChildName { get; set; }
@@ -107,4 +118,6 @@ public AppEntityWithNavigationsForeign(Guid id, string name)
107118
}
108119

109120
public string Name { get; set; }
121+
122+
public virtual List<AppEntityWithNavigations> OneToMany { get; set; }
110123
}

framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Testing/DomainEvents_Tests.cs

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -347,17 +347,33 @@ public async Task Should_Trigger_EntityUpdatedEvent_For_Aggregate_Root_When_Prop
347347

348348
var entityWithNavigationForeignId = Guid.NewGuid();
349349
var entityWithNavigationForeignId2 = Guid.NewGuid();
350+
var entityWithNavigationForeignId3 = Guid.NewGuid();
350351
await AppEntityWithNavigationForeignRepository.InsertAsync(new AppEntityWithNavigationsForeign(entityWithNavigationForeignId, "TestEntityWithNavigationForeign"));
351352
await AppEntityWithNavigationForeignRepository.InsertAsync(new AppEntityWithNavigationsForeign(entityWithNavigationForeignId2, "TestEntityWithNavigationForeign2"));
353+
await AppEntityWithNavigationForeignRepository.InsertAsync(new AppEntityWithNavigationsForeign(entityWithNavigationForeignId3, "TestEntityWithNavigationForeign3")
354+
{
355+
OneToMany = new List<AppEntityWithNavigations>()
356+
{
357+
new AppEntityWithNavigations(Guid.NewGuid(), "TestEntity2"),
358+
new AppEntityWithNavigations(Guid.NewGuid(), "TestEntity3")
359+
}
360+
});
352361

353362
var entityUpdatedEventTriggered = false;
363+
var entityWithNavigationsForeignUpdatedEventTriggered = false;
354364

355365
LocalEventBus.Subscribe<EntityUpdatedEventData<AppEntityWithNavigations>>(data =>
356366
{
357367
entityUpdatedEventTriggered = !entityUpdatedEventTriggered;
358368
return Task.CompletedTask;
359369
});
360370

371+
LocalEventBus.Subscribe<EntityUpdatedEventData<AppEntityWithNavigationsForeign>>(data =>
372+
{
373+
entityWithNavigationsForeignUpdatedEventTriggered = !entityWithNavigationsForeignUpdatedEventTriggered;
374+
return Task.CompletedTask;
375+
});
376+
361377
// Test with simple property with foreign key
362378
await WithUnitOfWorkAsync(async () =>
363379
{
@@ -368,17 +384,39 @@ await WithUnitOfWorkAsync(async () =>
368384
});
369385
entityUpdatedEventTriggered.ShouldBeTrue();
370386

371-
// Test only foreign key changed
387+
// Test only foreign key changed to null
372388
entityUpdatedEventTriggered = false;
373389
await WithUnitOfWorkAsync(async () =>
374390
{
375391
var entity = await AppEntityWithNavigationsRepository.GetAsync(entityId);
376-
entity.AppEntityWithNavigationForeignId = entityWithNavigationForeignId2;
392+
entity.AppEntityWithNavigationForeignId = null;
377393
await AppEntityWithNavigationsRepository.UpdateAsync(entity);
378394
});
379395
entityUpdatedEventTriggered.ShouldBeFalse();
380396

397+
// Test only foreign key change to new id
398+
entityUpdatedEventTriggered = false;
399+
await WithUnitOfWorkAsync(async () =>
400+
{
401+
var entity = await AppEntityWithNavigationsRepository.GetAsync(entityId);
402+
entity.AppEntityWithNavigationForeignId = entityWithNavigationForeignId;
403+
await AppEntityWithNavigationsRepository.UpdateAsync(entity);
404+
});
405+
entityUpdatedEventTriggered.ShouldBeTrue();
406+
407+
// Test only foreign key changed
408+
entityWithNavigationsForeignUpdatedEventTriggered = false;
409+
await WithUnitOfWorkAsync(async () =>
410+
{
411+
var entity = await AppEntityWithNavigationForeignRepository.GetAsync(entityWithNavigationForeignId3);
412+
entity.OneToMany.ShouldNotBeEmpty();
413+
entity.OneToMany.Clear();
414+
await AppEntityWithNavigationForeignRepository.UpdateAsync(entity);
415+
});
416+
entityWithNavigationsForeignUpdatedEventTriggered.ShouldBeFalse();
417+
381418
// Test with simple property with value object
419+
entityUpdatedEventTriggered = false;
382420
await WithUnitOfWorkAsync(async () =>
383421
{
384422
var entity = await AppEntityWithNavigationsRepository.GetAsync(entityId);

0 commit comments

Comments
 (0)