diff --git a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/AbpDbContext.cs b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/AbpDbContext.cs index 49bd99b624..638860e742 100644 --- a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/AbpDbContext.cs +++ b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/AbpDbContext.cs @@ -192,6 +192,21 @@ public abstract class AbpDbContext : DbContext, IAbpEfCoreDbContext, { try { + if (EntityChangeOptions.Value.PublishEntityUpdatedEventWhenNavigationChanges) + { + foreach (var entityEntry in AbpEfCoreNavigationHelper.GetChangedEntityEntries()) + { + if (entityEntry.Entity is ISoftDelete && entityEntry.Entity.As().IsDeleted) + { + EntityChangeEventHelper.PublishEntityDeletedEvent(entityEntry.Entity); + } + else + { + EntityChangeEventHelper.PublishEntityUpdatedEvent(entityEntry.Entity); + } + } + } + var auditLog = AuditingManager?.Current?.Log; List? entityChangeList = null; if (auditLog != null) @@ -289,14 +304,14 @@ public abstract class AbpDbContext : DbContext, IAbpEfCoreDbContext, protected virtual void ChangeTracker_Tracked(object? sender, EntityTrackedEventArgs e) { - AbpEfCoreNavigationHelper.ChangeTracker_Tracked(ChangeTracker, sender, e); + AbpEfCoreNavigationHelper.ChangeTracker_Tracked(sender, e); FillExtraPropertiesForTrackedEntities(e); PublishEventsForTrackedEntity(e.Entry); } protected virtual void ChangeTracker_StateChanged(object? sender, EntityStateChangedEventArgs e) { - AbpEfCoreNavigationHelper.ChangeTracker_StateChanged(ChangeTracker, sender, e); + AbpEfCoreNavigationHelper.ChangeTracker_StateChanged(sender, e); PublishEventsForTrackedEntity(e.Entry); } @@ -374,8 +389,7 @@ public abstract class AbpDbContext : DbContext, IAbpEfCoreDbContext, EntityChangeEventHelper.PublishEntityUpdatedEvent(entry.Entity); } } - else if (EntityChangeOptions.Value.PublishEntityUpdatedEventWhenNavigationChanges && - (entry.Navigations.Any(x => x.IsModified) || AbpEfCoreNavigationHelper.IsEntityEntryNavigationChanged(entry))) + else if (EntityChangeOptions.Value.PublishEntityUpdatedEventWhenNavigationChanges && AbpEfCoreNavigationHelper.IsEntityEntryModified(entry)) { if (entry.Entity is ISoftDelete && entry.Entity.As().IsDeleted) { @@ -393,21 +407,6 @@ public abstract class AbpDbContext : DbContext, IAbpEfCoreDbContext, EntityChangeEventHelper.PublishEntityDeletedEvent(entry.Entity); break; } - - if (EntityChangeOptions.Value.PublishEntityUpdatedEventWhenNavigationChanges) - { - foreach (var entityEntry in AbpEfCoreNavigationHelper.GetChangedEntityEntries()) - { - if (entityEntry.Entity is ISoftDelete && entityEntry.Entity.As().IsDeleted) - { - EntityChangeEventHelper.PublishEntityDeletedEvent(entityEntry.Entity); - } - else - { - EntityChangeEventHelper.PublishEntityUpdatedEvent(entityEntry.Entity); - } - } - } } protected virtual void HandlePropertiesBeforeSave() diff --git a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/ChangeTrackers/AbpEfCoreNavigationHelper.cs b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/ChangeTrackers/AbpEfCoreNavigationHelper.cs index d145ed5316..50a8a121d3 100644 --- a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/ChangeTrackers/AbpEfCoreNavigationHelper.cs +++ b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/ChangeTrackers/AbpEfCoreNavigationHelper.cs @@ -1,9 +1,9 @@ -using System; -using System.Collections; using System.Collections.Generic; using System.Linq; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.ChangeTracking; +using Microsoft.EntityFrameworkCore.Internal; +using Microsoft.EntityFrameworkCore.Metadata; using Volo.Abp.DependencyInjection; using Volo.Abp.Domain.Entities; @@ -15,18 +15,18 @@ namespace Volo.Abp.EntityFrameworkCore.ChangeTrackers; /// public class AbpEfCoreNavigationHelper : ITransientDependency { - protected Dictionary> EntityEntryNavigationProperties { get; } = new Dictionary>(); + protected Dictionary EntityEntries { get; } = new(); - public virtual void ChangeTracker_Tracked(ChangeTracker changeTracker, object? sender, EntityTrackedEventArgs e) + public virtual void ChangeTracker_Tracked(object? sender, EntityTrackedEventArgs e) { EntityEntryTrackedOrStateChanged(e.Entry); - DetectChanges(); + DetectChanges(e.Entry); } - public virtual void ChangeTracker_StateChanged(ChangeTracker changeTracker, object? sender, EntityStateChangedEventArgs e) + public virtual void ChangeTracker_StateChanged(object? sender, EntityStateChangedEventArgs e) { EntityEntryTrackedOrStateChanged(e.Entry); - DetectChanges(); + DetectChanges(e.Entry); } protected virtual void EntityEntryTrackedOrStateChanged(EntityEntry entityEntry) @@ -36,156 +36,166 @@ public class AbpEfCoreNavigationHelper : ITransientDependency return; } - var entryId = GetEntityId(entityEntry); + var entryId = GetEntityEntryIdentity(entityEntry); if (entryId == null) { return; } - var navigationProperties = EntityEntryNavigationProperties.GetOrAdd(entryId, () => new List()); - var index = 0; - foreach (var navigationEntry in entityEntry.Navigations.Where(navigation => !navigation.IsModified)) + if (EntityEntries.ContainsKey(entryId)) { - var navigationProperty = navigationProperties.FirstOrDefault(x => x.Index == index); - if (navigationProperty != null) + return; + } + + EntityEntries.Add(entryId, new AbpEntityEntry(entryId, entityEntry)); + } + + protected virtual void DetectChanges(EntityEntry entityEntry) + { + if (entityEntry.State != EntityState.Added && + entityEntry.State != EntityState.Deleted && + entityEntry.State != EntityState.Modified) + { + return; + } + + RecursiveDetectChanges(entityEntry); + } + + protected virtual void RecursiveDetectChanges(EntityEntry entityEntry) + { + #pragma warning disable EF1001 + var stateManager = entityEntry.Context.GetDependencies().StateManager; + var internalEntityEntityEntry = stateManager.Entries.FirstOrDefault(x => x.Entity == entityEntry.Entity); + if (internalEntityEntityEntry == null) + { + return; + } + + var foreignKeys = entityEntry.Metadata.GetForeignKeys().ToList(); + foreach (var foreignKey in foreignKeys) + { + var principal = stateManager.FindPrincipal(internalEntityEntityEntry, foreignKey); + if (principal == null) { - if (navigationProperty.Value == null || IsCollectionAndEmpty(navigationProperty.Value)) - { - navigationProperty.Value = navigationEntry.CurrentValue; - } + continue; + } + + var entryId = GetEntityEntryIdentity(principal.ToEntityEntry()); + if (entryId == null || !EntityEntries.TryGetValue(entryId, out var abpEntityEntry)) + { + continue; } - else + + if (!abpEntityEntry.IsModified) { - navigationProperties.Add(new AbpEntityEntryNavigationProperty(index, navigationEntry.Metadata.Name, navigationEntry.CurrentValue, false, entityEntry, navigationEntry)); + abpEntityEntry.IsModified = true; + RecursiveDetectChanges(abpEntityEntry.EntityEntry); } - index++; + var navigationEntry = abpEntityEntry.NavigationEntries.FirstOrDefault(x => x.NavigationEntry.Metadata is INavigation navigationMetadata && navigationMetadata.ForeignKey == foreignKey) ?? + abpEntityEntry.NavigationEntries.FirstOrDefault(x => x.NavigationEntry.Metadata is ISkipNavigation skipNavigationMetadata && skipNavigationMetadata.ForeignKey == foreignKey); + if (navigationEntry != null) + { + navigationEntry.IsModified = true; + } } - } - protected virtual void DetectChanges() - { - foreach (var entityEntryNavigationProperties in EntityEntryNavigationProperties) + var skipNavigations = entityEntry.Metadata.GetSkipNavigations().ToList(); + foreach (var skipNavigation in skipNavigations) { - foreach (var navigationProperty in entityEntryNavigationProperties.Value.Where(x => !x.IsChanged && x.EntityEntry.State == EntityState.Unchanged)) + var joinEntityType = skipNavigation.JoinEntityType; + var foreignKey = skipNavigation.ForeignKey; + var inverseForeignKey = skipNavigation.Inverse.ForeignKey; + foreach (var joinEntry in stateManager.Entries) { - if (navigationProperty.NavigationEntry.IsModified) + if (joinEntry.EntityType != joinEntityType || stateManager.FindPrincipal(joinEntry, foreignKey) != internalEntityEntityEntry) { - navigationProperty.IsChanged = true; continue; } - if (navigationProperty.Value == null || IsCollectionAndEmpty(navigationProperty.Value)) + var principal = stateManager.FindPrincipal(joinEntry, inverseForeignKey); + if (principal == null) { - if (navigationProperty.NavigationEntry.CurrentValue != null || IsCollectionAndNotEmpty(navigationProperty.NavigationEntry.CurrentValue)) - { - if (navigationProperty.NavigationEntry.CurrentValue is ICollection collection) - { - navigationProperty.Value = collection.Cast().ToList(); - } - else - { - navigationProperty.Value = navigationProperty.NavigationEntry.CurrentValue; - } - } + continue; + } + + var entryId = GetEntityEntryIdentity(principal.ToEntityEntry()); + if (entryId == null || !EntityEntries.TryGetValue(entryId, out var abpEntityEntry)) + { + continue; + } + + if (!abpEntityEntry.IsModified) + { + abpEntityEntry.IsModified = true; + RecursiveDetectChanges(abpEntityEntry.EntityEntry); } - if (navigationProperty.Value != null || IsCollectionAndNotEmpty(navigationProperty.Value)) + var navigationEntry = abpEntityEntry.NavigationEntries.FirstOrDefault(x => x.NavigationEntry.Metadata is INavigation navigationMetadata && navigationMetadata.ForeignKey == inverseForeignKey) ?? + abpEntityEntry.NavigationEntries.FirstOrDefault(x => x.NavigationEntry.Metadata is ISkipNavigation skipNavigationMetadata && skipNavigationMetadata.ForeignKey == inverseForeignKey); + if (navigationEntry != null) { - if (navigationProperty.NavigationEntry.CurrentValue == null || IsCollectionAndEmpty(navigationProperty.NavigationEntry.CurrentValue)) - { - if (IsCollectionAndEmpty(navigationProperty.Value) && IsCollectionAndEmpty(navigationProperty.NavigationEntry.CurrentValue)) - { - continue; - } - - navigationProperty.IsChanged = true; - } + navigationEntry.IsModified = true; } } } +#pragma warning restore EF1001 } public virtual List GetChangedEntityEntries() { - DetectChanges(); - return EntityEntryNavigationProperties - .SelectMany(x => x.Value) - .Where(x => x.NavigationEntry.IsModified || x.IsChanged) - .Select(x => x.EntityEntry) + return EntityEntries + .Where(x => x.Value.IsModified) + .Select(x => x.Value.EntityEntry) .ToList(); } - public virtual bool IsEntityEntryNavigationChanged(EntityEntry entityEntry) + public virtual bool IsEntityEntryModified(EntityEntry entityEntry) { - DetectChanges(); if (entityEntry.State == EntityState.Modified) { return true; } - var entryId = GetEntityId(entityEntry); + var entryId = GetEntityEntryIdentity(entityEntry); if (entryId == null) { return false; } - if (EntityEntryNavigationProperties.TryGetValue(entryId, out var navigationProperties)) - { - return navigationProperties.Any(x => x.IsChanged) || - navigationProperties.Any(x => x.NavigationEntry.IsModified) || - navigationProperties.Any(x => - x.NavigationEntry is ReferenceEntry && - x.NavigationEntry.As().TargetEntry?.State == EntityState.Modified); - } - - return false; + return EntityEntries.TryGetValue(entryId, out var abpEntityEntry) && abpEntityEntry.IsModified; } - public virtual bool IsEntityEntryNavigationChanged(NavigationEntry navigationEntry, int index) + public virtual bool IsNavigationEntryModified(EntityEntry entityEntry, int navigationEntryIndex) { - if (navigationEntry.IsModified || (navigationEntry is ReferenceEntry && navigationEntry.As().TargetEntry?.State == EntityState.Modified)) - { - return true; - } - - var entryId = GetEntityId(navigationEntry.EntityEntry); + var entryId = GetEntityEntryIdentity(entityEntry); if (entryId == null) { return false; } - if (EntityEntryNavigationProperties.TryGetValue(entryId, out var navigationProperties)) + if (!EntityEntries.TryGetValue(entryId, out var abpEntityEntry)) { - var navigationProperty = navigationProperties.FirstOrDefault(x => x.Index == index); - if (navigationProperty != null && navigationProperty.IsChanged) - { - return true; - } + return false; } - return false; + var navigationEntryProperty = abpEntityEntry.NavigationEntries.ElementAtOrDefault(navigationEntryIndex); + return navigationEntryProperty != null && navigationEntryProperty.IsModified; } - public void Clear() + protected virtual string? GetEntityEntryIdentity(EntityEntry entityEntry) { - EntityEntryNavigationProperties.Clear(); - } - - private string? GetEntityId(EntityEntry entityEntry) - { - return entityEntry.Entity is IEntity entryEntity && entryEntity.GetKeys().Length == 1 - ? entryEntity.GetKeys().FirstOrDefault()?.ToString() - : null; - } + if (entityEntry.Entity is IEntity entryEntity && entryEntity.GetKeys().Length == 1) + { + return $"{entityEntry.Metadata.ClrType.FullName}:{entryEntity.GetKeys().FirstOrDefault()}"; + } - private bool IsCollectionAndEmpty(object? value) - { - return value is ICollection && value is ICollection collection && collection.Count == 0; + return null; } - private bool IsCollectionAndNotEmpty(object? value) + public void Clear() { - return value is ICollection && value is ICollection collection && collection.Count != 0; + EntityEntries.Clear(); } } diff --git a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/ChangeTrackers/AbpEntityEntry.cs b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/ChangeTrackers/AbpEntityEntry.cs new file mode 100644 index 0000000000..0aef48e0d1 --- /dev/null +++ b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/ChangeTrackers/AbpEntityEntry.cs @@ -0,0 +1,50 @@ +using System.Collections.Generic; +using System.Linq; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.ChangeTracking; + +namespace Volo.Abp.EntityFrameworkCore.ChangeTrackers; + +public class AbpEntityEntry +{ + public string Id { get; set; } + + public EntityEntry EntityEntry { get; set; } + + public List NavigationEntries { get; set; } + + private bool _isModified; + public bool IsModified + { + get + { + return _isModified || EntityEntry.State == EntityState.Modified || NavigationEntries.Any(n => n.IsModified); + } + set + { + _isModified = value; + } + } + + public AbpEntityEntry(string id, EntityEntry entityEntry) + { + Id = id; + EntityEntry = entityEntry; + NavigationEntries = EntityEntry.Navigations.Select(x => new AbpNavigationEntry(x, x.Metadata.Name)).ToList(); + } +} + +public class AbpNavigationEntry +{ + public NavigationEntry NavigationEntry { get; set; } + + public string Name { get; set; } + + public bool IsModified { get; set; } + + public AbpNavigationEntry(NavigationEntry navigationEntry, string name) + { + NavigationEntry = navigationEntry; + Name = name; + } +} diff --git a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/ChangeTrackers/AbpEntityEntryNavigationProperty.cs b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/ChangeTrackers/AbpEntityEntryNavigationProperty.cs deleted file mode 100644 index a3cec7cf64..0000000000 --- a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/ChangeTrackers/AbpEntityEntryNavigationProperty.cs +++ /dev/null @@ -1,28 +0,0 @@ -using Microsoft.EntityFrameworkCore.ChangeTracking; - -namespace Volo.Abp.EntityFrameworkCore.ChangeTrackers; - -public class AbpEntityEntryNavigationProperty -{ - public int Index { get; set; } - - public string Name { get; set; } - - public object? Value { get; set; } - - public bool IsChanged { get; set; } - - public EntityEntry EntityEntry { get; set; } - - public NavigationEntry NavigationEntry { get; set; } - - public AbpEntityEntryNavigationProperty(int index, string name, object? value, bool isChanged, EntityEntry entityEntry, NavigationEntry navigationEntry) - { - Index = index; - Name = name; - Value = value; - IsChanged = isChanged; - EntityEntry = entityEntry; - NavigationEntry = navigationEntry; - } -} diff --git a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/EntityHistory/EntityHistoryHelper.cs b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/EntityHistory/EntityHistoryHelper.cs index ef6bd230f5..a52ac2f437 100644 --- a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/EntityHistory/EntityHistoryHelper.cs +++ b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/EntityHistory/EntityHistoryHelper.cs @@ -193,21 +193,18 @@ public class EntityHistoryHelper : IEntityHistoryHelper, ITransientDependency } } - if (entityEntry.State == EntityState.Unchanged && Options.SaveEntityHistoryWhenNavigationChanges && AbpEfCoreNavigationHelper != null) + if (Options.SaveEntityHistoryWhenNavigationChanges && AbpEfCoreNavigationHelper != null) { - var index = 0; - foreach (var navigation in entityEntry.Navigations) + foreach (var (navigationEntry, index) in entityEntry.Navigations.Select((value, i) => ( value, i ))) { - if (navigation.IsModified || AbpEfCoreNavigationHelper.IsEntityEntryNavigationChanged(navigation, index)) + if (AbpEfCoreNavigationHelper.IsNavigationEntryModified(entityEntry, index)) { propertyChanges.Add(new EntityPropertyChangeInfo { - PropertyName = navigation.Metadata.Name, - PropertyTypeFullName = navigation.Metadata.ClrType.GetFirstGenericArgumentIfNullable().FullName! + PropertyName = navigationEntry.Metadata.Name, + PropertyTypeFullName = navigationEntry.Metadata.ClrType.GetFirstGenericArgumentIfNullable().FullName! }); } - - index++; } } @@ -255,10 +252,9 @@ public class EntityHistoryHelper : IEntityHistoryHelper, ITransientDependency protected virtual bool HasNavigationPropertiesChanged(EntityEntry entityEntry) { - return entityEntry.State == EntityState.Unchanged && - Options.SaveEntityHistoryWhenNavigationChanges && + return Options.SaveEntityHistoryWhenNavigationChanges && AbpEfCoreNavigationHelper != null && - AbpEfCoreNavigationHelper.IsEntityEntryNavigationChanged(entityEntry); + AbpEfCoreNavigationHelper.IsEntityEntryModified(entityEntry); } protected virtual bool ShouldSavePropertyHistory(PropertyEntry propertyEntry, bool defaultValue) diff --git a/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/AbpEntityFrameworkCoreTestModule.cs b/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/AbpEntityFrameworkCoreTestModule.cs index 0150eb6fa5..5e81aa1a7d 100644 --- a/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/AbpEntityFrameworkCoreTestModule.cs +++ b/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/AbpEntityFrameworkCoreTestModule.cs @@ -51,7 +51,7 @@ public class AbpEntityFrameworkCoreTestModule : AbpModule options.Entity(opt => { - opt.DefaultWithDetailsFunc = q => q.Include(p => p.OneToOne).Include(p => p.OneToMany).Include(p => p.ManyToMany); + opt.DefaultWithDetailsFunc = q => q.Include(p => p.OneToOne).ThenInclude(x => x.OneToOne).Include(p => p.OneToMany).ThenInclude(x => x.OneToMany).Include(p => p.ManyToMany); }); }); diff --git a/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/DomainEvents/DomainEvents_Tests.cs b/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/DomainEvents/DomainEvents_Tests.cs index 978d411634..776e7d4766 100644 --- a/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/DomainEvents/DomainEvents_Tests.cs +++ b/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/DomainEvents/DomainEvents_Tests.cs @@ -26,11 +26,14 @@ public class AbpEfCoreDomainEvents_Tests : EntityFrameworkCoreTestBase { protected readonly IRepository AppEntityWithNavigationsRepository; protected readonly ILocalEventBus LocalEventBus; + protected readonly IRepository PersonRepository; + protected bool _loadEntityWithDetails = false; public AbpEfCoreDomainEvents_Tests() { AppEntityWithNavigationsRepository = GetRequiredService>(); LocalEventBus = GetRequiredService(); + PersonRepository = GetRequiredService>(); } protected override void AfterAddApplication(IServiceCollection services) @@ -39,16 +42,279 @@ public class AbpEfCoreDomainEvents_Tests : EntityFrameworkCoreTestBase { options.Entity(opt => { - opt.DefaultWithDetailsFunc = q => q; + if (_loadEntityWithDetails) + { + opt.DefaultWithDetailsFunc = q => q; + } }); }); base.AfterAddApplication(services); } + [Fact] + public async Task Should_Trigger_Domain_Events_For_Aggregate_Root_When_Navigation_Changes_Tests() + { + _loadEntityWithDetails = false; + + var entityId = Guid.NewGuid(); + + await AppEntityWithNavigationsRepository.InsertAsync(new AppEntityWithNavigations(entityId, "TestEntity")); + + var entityUpdatedEventTriggered = false; + var personCreatedEventCount = 0; + var entityUpdatedEventTriggerCount = 0; + + LocalEventBus.Subscribe>(_ => + { + personCreatedEventCount++; + return Task.CompletedTask; + }); + + LocalEventBus.Subscribe>(async _ => + { + entityUpdatedEventTriggered = true; + await PersonRepository.InsertAsync(new Person(Guid.NewGuid(), Guid.NewGuid().ToString(), new Random().Next(1, 100))); + }); + + // Test with simple property + await WithUnitOfWorkAsync(async () => + { + var entity = await AppEntityWithNavigationsRepository.GetAsync(entityId); + entity.Name = Guid.NewGuid().ToString(); + await AppEntityWithNavigationsRepository.UpdateAsync(entity); + }); + entityUpdatedEventTriggered.ShouldBeTrue(); + personCreatedEventCount.ShouldBe(++entityUpdatedEventTriggerCount); + + // Test with value object + entityUpdatedEventTriggered = false; + await WithUnitOfWorkAsync(async () => + { + var entity = await AppEntityWithNavigationsRepository.GetAsync(entityId); + entity.AppEntityWithValueObjectAddress = new AppEntityWithValueObjectAddress("Turkey"); + await AppEntityWithNavigationsRepository.UpdateAsync(entity); + }); + entityUpdatedEventTriggered.ShouldBeTrue(); + personCreatedEventCount.ShouldBe(++entityUpdatedEventTriggerCount); + + entityUpdatedEventTriggered = false; + await WithUnitOfWorkAsync(async () => + { + var entity = await AppEntityWithNavigationsRepository.GetAsync(entityId); + entity.AppEntityWithValueObjectAddress.Country = "USA"; + await AppEntityWithNavigationsRepository.UpdateAsync(entity); + }); + entityUpdatedEventTriggered.ShouldBeTrue(); + personCreatedEventCount.ShouldBe(++entityUpdatedEventTriggerCount); + + LocalEventBus.Subscribe>(_ => + { + throw new Exception("Should not trigger this event"); + }); + + entityUpdatedEventTriggered = false; + await WithUnitOfWorkAsync(async () => + { + var entity = await AppEntityWithNavigationsRepository.GetAsync(entityId); + entity.AppEntityWithValueObjectAddress = null; + await AppEntityWithNavigationsRepository.UpdateAsync(entity); + }); + entityUpdatedEventTriggered.ShouldBeTrue(); + personCreatedEventCount.ShouldBe(++entityUpdatedEventTriggerCount); + + // Test with one to one + entityUpdatedEventTriggered = false; + await WithUnitOfWorkAsync(async () => + { + var entity = await AppEntityWithNavigationsRepository.GetAsync(entityId); + entity.OneToOne = new AppEntityWithNavigationChildOneToOne + { + ChildName = "ChildName", + OneToOne = new AppEntityWithNavigationChildOneToOneAndOneToOne + { + ChildName = "OneToOne-ChildName" + } + }; + await AppEntityWithNavigationsRepository.UpdateAsync(entity); + }); + entityUpdatedEventTriggered.ShouldBeTrue(); + personCreatedEventCount.ShouldBe(++entityUpdatedEventTriggerCount); + + var oneToOneEntityUpdatedEventTriggered = false; + var oneToOneAndOneToOneEntityUpdatedEventTriggered = false; + + LocalEventBus.Subscribe>(async _ => + { + oneToOneEntityUpdatedEventTriggered = true; + }); + LocalEventBus.Subscribe>(async _ => + { + oneToOneAndOneToOneEntityUpdatedEventTriggered = true; + }); + + entityUpdatedEventTriggered = false; + await WithUnitOfWorkAsync(async () => + { + var entity = await AppEntityWithNavigationsRepository.GetAsync(entityId); + entity.OneToOne.ChildName = "ChildName2"; + await AppEntityWithNavigationsRepository.UpdateAsync(entity); + }); + entityUpdatedEventTriggered.ShouldBeTrue(); + oneToOneEntityUpdatedEventTriggered.ShouldBeTrue(); + oneToOneAndOneToOneEntityUpdatedEventTriggered.ShouldBeFalse(); + personCreatedEventCount.ShouldBe(++entityUpdatedEventTriggerCount); + + entityUpdatedEventTriggered = false; + oneToOneEntityUpdatedEventTriggered = false; + oneToOneAndOneToOneEntityUpdatedEventTriggered = false; + await WithUnitOfWorkAsync(async () => + { + var entity = await AppEntityWithNavigationsRepository.GetAsync(entityId); + entity.OneToOne.OneToOne.ChildName = "OneToOne-ChildName2"; + await AppEntityWithNavigationsRepository.UpdateAsync(entity); + }); + entityUpdatedEventTriggered.ShouldBeTrue(); + oneToOneEntityUpdatedEventTriggered.ShouldBeTrue(); + oneToOneAndOneToOneEntityUpdatedEventTriggered.ShouldBeTrue(); + personCreatedEventCount.ShouldBe(++entityUpdatedEventTriggerCount); + + LocalEventBus.Subscribe>(_ => + { + throw new Exception("Should not trigger this event"); + }); + + entityUpdatedEventTriggered = false; + await WithUnitOfWorkAsync(async () => + { + var entity = await AppEntityWithNavigationsRepository.GetAsync(entityId); + entity.OneToOne = null; + await AppEntityWithNavigationsRepository.UpdateAsync(entity); + }); + entityUpdatedEventTriggered.ShouldBeTrue(); + personCreatedEventCount.ShouldBe(++entityUpdatedEventTriggerCount); + + // Test with one to many + entityUpdatedEventTriggered = false; + await WithUnitOfWorkAsync(async () => + { + var entity = await AppEntityWithNavigationsRepository.GetAsync(entityId); + entity.OneToMany = new List() + { + new AppEntityWithNavigationChildOneToMany + { + AppEntityWithNavigationId = entity.Id, + ChildName = "ChildName1", + OneToMany = new List() + { + new AppEntityWithNavigationChildOneToManyAndOneToMany() + { + ChildName = "OneToMany-ChildName1" + } + } + } + }; + await AppEntityWithNavigationsRepository.UpdateAsync(entity); + }); + entityUpdatedEventTriggered.ShouldBeTrue(); + personCreatedEventCount.ShouldBe(++entityUpdatedEventTriggerCount); + + var oneToManyEntityUpdatedEventTriggered = false; + var oneToManyAndOneToManyEntityUpdatedEventTriggered = false; + + LocalEventBus.Subscribe>(async _ => + { + oneToManyEntityUpdatedEventTriggered = true; + }); + LocalEventBus.Subscribe>(async _ => + { + oneToManyAndOneToManyEntityUpdatedEventTriggered = true; + }); + + entityUpdatedEventTriggered = false; + await WithUnitOfWorkAsync(async () => + { + var entity = await AppEntityWithNavigationsRepository.GetAsync(entityId); + entity.OneToMany[0].ChildName = "ChildName2"; + await AppEntityWithNavigationsRepository.UpdateAsync(entity); + }); + entityUpdatedEventTriggered.ShouldBeTrue(); + oneToManyEntityUpdatedEventTriggered.ShouldBeTrue(); + oneToManyAndOneToManyEntityUpdatedEventTriggered.ShouldBeFalse(); + personCreatedEventCount.ShouldBe(++entityUpdatedEventTriggerCount); + + entityUpdatedEventTriggered = false; + oneToManyEntityUpdatedEventTriggered = false; + oneToManyAndOneToManyEntityUpdatedEventTriggered = false; + await WithUnitOfWorkAsync(async () => + { + var entity = await AppEntityWithNavigationsRepository.GetAsync(entityId); + entity.OneToMany[0].OneToMany[0].ChildName = "OneToMany-ChildName2"; + await AppEntityWithNavigationsRepository.UpdateAsync(entity); + }); + entityUpdatedEventTriggered.ShouldBeTrue(); + oneToManyEntityUpdatedEventTriggered.ShouldBeTrue(); + oneToManyAndOneToManyEntityUpdatedEventTriggered.ShouldBeTrue(); + personCreatedEventCount.ShouldBe(++entityUpdatedEventTriggerCount); + + LocalEventBus.Subscribe>(_ => + { + throw new Exception("Should not trigger this event"); + }); + + entityUpdatedEventTriggered = false; + await WithUnitOfWorkAsync(async () => + { + var entity = await AppEntityWithNavigationsRepository.GetAsync(entityId); + entity.OneToMany.Clear(); + await AppEntityWithNavigationsRepository.UpdateAsync(entity); + }); + entityUpdatedEventTriggered.ShouldBeTrue(); + personCreatedEventCount.ShouldBe(++entityUpdatedEventTriggerCount); + + // Test with many to many + entityUpdatedEventTriggered = false; + await WithUnitOfWorkAsync(async () => + { + var entity = await AppEntityWithNavigationsRepository.GetAsync(entityId); + entity.ManyToMany = new List() + { + new AppEntityWithNavigationChildManyToMany + { + ChildName = "ChildName1" + } + }; + await AppEntityWithNavigationsRepository.UpdateAsync(entity); + }); + entityUpdatedEventTriggered.ShouldBeTrue(); + personCreatedEventCount.ShouldBe(++entityUpdatedEventTriggerCount); + + entityUpdatedEventTriggered = false; + await WithUnitOfWorkAsync(async () => + { + var entity = await AppEntityWithNavigationsRepository.GetAsync(entityId); + entity.ManyToMany[0].ChildName = "ChildName2"; + await AppEntityWithNavigationsRepository.UpdateAsync(entity); + }); + entityUpdatedEventTriggered.ShouldBeTrue(); + personCreatedEventCount.ShouldBe(++entityUpdatedEventTriggerCount); + + entityUpdatedEventTriggered = false; + await WithUnitOfWorkAsync(async () => + { + var entity = await AppEntityWithNavigationsRepository.GetAsync(entityId); + entity.ManyToMany.Clear(); + await AppEntityWithNavigationsRepository.UpdateAsync(entity); + }); + entityUpdatedEventTriggered.ShouldBeTrue(); + personCreatedEventCount.ShouldBe(++entityUpdatedEventTriggerCount); + } + [Fact] public async Task Should_Trigger_Domain_Events_For_Aggregate_Root_When_EnsureCollectionLoaded_Navigation_Changes_Tests() { + _loadEntityWithDetails = true; + var entityId = Guid.NewGuid(); await AppEntityWithNavigationsRepository.InsertAsync(new AppEntityWithNavigations(entityId, "TestEntity") diff --git a/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/EntityFrameworkCoreTestBase.cs b/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/EntityFrameworkCoreTestBase.cs index a046c5ade9..119b169638 100644 --- a/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/EntityFrameworkCoreTestBase.cs +++ b/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/EntityFrameworkCoreTestBase.cs @@ -1,8 +1,8 @@ -using Volo.Abp.Testing; +using Volo.Abp.TestApp.Testing; namespace Volo.Abp.EntityFrameworkCore; -public abstract class EntityFrameworkCoreTestBase : AbpIntegratedTest +public abstract class EntityFrameworkCoreTestBase : TestAppTestBase { protected override void SetAbpApplicationCreationOptions(AbpApplicationCreationOptions options) { diff --git a/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/TestMigrationsDbContext.cs b/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/TestMigrationsDbContext.cs index 5de4bd6369..225e767d26 100644 --- a/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/TestMigrationsDbContext.cs +++ b/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/TestMigrationsDbContext.cs @@ -70,10 +70,22 @@ public class TestMigrationsDbContext : AbpDbContext modelBuilder.Entity(b => { b.ConfigureByConvention(); - b.OwnsOne(v => v.AppEntityWithValueObjectAddress); + b.OwnsOne(x => x.AppEntityWithValueObjectAddress); b.HasOne(x => x.OneToOne).WithOne().HasForeignKey(x => x.Id); b.HasMany(x => x.OneToMany).WithOne().HasForeignKey(x => x.AppEntityWithNavigationId); b.HasMany(x => x.ManyToMany).WithMany(x => x.ManyToMany).UsingEntity(); }); + + modelBuilder.Entity(b => + { + b.ConfigureByConvention(); + b.HasOne(x => x.OneToOne).WithOne().HasForeignKey(x => x.Id); + }); + + modelBuilder.Entity(b => + { + b.ConfigureByConvention(); + b.HasMany(x => x.OneToMany).WithOne().HasForeignKey(x => x.AppEntityWithNavigationChildOneToManyId); + }); } } diff --git a/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/TestApp/EntityFrameworkCore/TestAppDbContext.cs b/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/TestApp/EntityFrameworkCore/TestAppDbContext.cs index 63b57ec147..e668a21e26 100644 --- a/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/TestApp/EntityFrameworkCore/TestAppDbContext.cs +++ b/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/TestApp/EntityFrameworkCore/TestAppDbContext.cs @@ -103,6 +103,18 @@ public class TestAppDbContext : AbpDbContext, IThirdDbContext, b.HasMany(x => x.ManyToMany).WithMany(x => x.ManyToMany).UsingEntity(); }); + modelBuilder.Entity(b => + { + b.ConfigureByConvention(); + b.HasOne(x => x.OneToOne).WithOne().HasForeignKey(x => x.Id); + }); + + modelBuilder.Entity(b => + { + b.ConfigureByConvention(); + b.HasMany(x => x.OneToMany).WithOne().HasForeignKey(x => x.AppEntityWithNavigationChildOneToManyId); + }); + modelBuilder.TryConfigureObjectExtensions(); } } diff --git a/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Domain/AppEntityWithNavigations.cs b/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Domain/AppEntityWithNavigations.cs index b0c2e4f7bf..e9b9706e0c 100644 --- a/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Domain/AppEntityWithNavigations.cs +++ b/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Domain/AppEntityWithNavigations.cs @@ -36,7 +36,6 @@ public class AppEntityWithValueObjectAddress : ValueObject { public AppEntityWithValueObjectAddress(string country) { - Country = country; } @@ -48,8 +47,14 @@ public class AppEntityWithValueObjectAddress : ValueObject } } - public class AppEntityWithNavigationChildOneToOne : Entity +{ + public string ChildName { get; set; } + + public virtual AppEntityWithNavigationChildOneToOneAndOneToOne OneToOne { get; set; } +} + +public class AppEntityWithNavigationChildOneToOneAndOneToOne : Entity { public string ChildName { get; set; } } @@ -59,6 +64,15 @@ public class AppEntityWithNavigationChildOneToMany : Entity public Guid AppEntityWithNavigationId { get; set; } public string ChildName { get; set; } + + public virtual List OneToMany { get; set; } +} + +public class AppEntityWithNavigationChildOneToManyAndOneToMany : Entity +{ + public Guid AppEntityWithNavigationChildOneToManyId { get; set; } + + public string ChildName { get; set; } } public class AppEntityWithNavigationChildManyToMany : AggregateRoot diff --git a/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Testing/DomainEvents_Tests.cs b/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Testing/DomainEvents_Tests.cs index 74f8198aa7..7db0ba180d 100644 --- a/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Testing/DomainEvents_Tests.cs +++ b/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Testing/DomainEvents_Tests.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; @@ -176,155 +176,6 @@ public abstract class DomainEvents_Tests : TestAppTestBase>(data => - { - personCreatedEventCount++; - return Task.CompletedTask; - }); - - LocalEventBus.Subscribe>(async data => - { - entityUpdatedEventTriggered = !entityUpdatedEventTriggered; - await PersonRepository.InsertAsync(new Person(Guid.NewGuid(), Guid.NewGuid().ToString(), new Random().Next(1, 100))); - }); - - // Test with simple property - await WithUnitOfWorkAsync(async () => - { - var entity = await AppEntityWithNavigationsRepository.GetAsync(entityId); - entity.Name = Guid.NewGuid().ToString(); - await AppEntityWithNavigationsRepository.UpdateAsync(entity); - }); - entityUpdatedEventTriggered.ShouldBeTrue(); - personCreatedEventCount.ShouldBe(++entityUpdatedEventTriggerCount); - - LocalEventBus.Subscribe>(data => - { - throw new Exception("Should not trigger this event"); - }); - - // Test with value object - entityUpdatedEventTriggered = false; - await WithUnitOfWorkAsync(async () => - { - var entity = await AppEntityWithNavigationsRepository.GetAsync(entityId); - entity.AppEntityWithValueObjectAddress = new AppEntityWithValueObjectAddress("Turkey"); - await AppEntityWithNavigationsRepository.UpdateAsync(entity); - }); - entityUpdatedEventTriggered.ShouldBeTrue(); - personCreatedEventCount.ShouldBe(++entityUpdatedEventTriggerCount); - - entityUpdatedEventTriggered = false; - await WithUnitOfWorkAsync(async () => - { - var entity = await AppEntityWithNavigationsRepository.GetAsync(entityId); - entity.AppEntityWithValueObjectAddress = null; - await AppEntityWithNavigationsRepository.UpdateAsync(entity); - }); - entityUpdatedEventTriggered.ShouldBeTrue(); - personCreatedEventCount.ShouldBe(++entityUpdatedEventTriggerCount); - - // Test with one to one - entityUpdatedEventTriggered = false; - await WithUnitOfWorkAsync(async () => - { - var entity = await AppEntityWithNavigationsRepository.GetAsync(entityId); - entity.OneToOne = new AppEntityWithNavigationChildOneToOne - { - ChildName = "ChildName" - }; - await AppEntityWithNavigationsRepository.UpdateAsync(entity); - }); - entityUpdatedEventTriggered.ShouldBeTrue(); - personCreatedEventCount.ShouldBe(++entityUpdatedEventTriggerCount); - - LocalEventBus.Subscribe>(data => - { - throw new Exception("Should not trigger this event"); - }); - - entityUpdatedEventTriggered = false; - await WithUnitOfWorkAsync(async () => - { - var entity = await AppEntityWithNavigationsRepository.GetAsync(entityId); - entity.OneToOne = null; - await AppEntityWithNavigationsRepository.UpdateAsync(entity); - }); - entityUpdatedEventTriggered.ShouldBeTrue(); - personCreatedEventCount.ShouldBe(++entityUpdatedEventTriggerCount); - - // Test with one to many - entityUpdatedEventTriggered = false; - await WithUnitOfWorkAsync(async () => - { - var entity = await AppEntityWithNavigationsRepository.GetAsync(entityId); - entity.OneToMany = new List() - { - new AppEntityWithNavigationChildOneToMany - { - AppEntityWithNavigationId = entity.Id, - ChildName = "ChildName1" - } - }; - await AppEntityWithNavigationsRepository.UpdateAsync(entity); - }); - entityUpdatedEventTriggered.ShouldBeTrue(); - personCreatedEventCount.ShouldBe(++entityUpdatedEventTriggerCount); - - LocalEventBus.Subscribe>(data => - { - throw new Exception("Should not trigger this event"); - }); - - entityUpdatedEventTriggered = false; - await WithUnitOfWorkAsync(async () => - { - var entity = await AppEntityWithNavigationsRepository.GetAsync(entityId); - entity.OneToMany.Clear(); - await AppEntityWithNavigationsRepository.UpdateAsync(entity); - }); - entityUpdatedEventTriggered.ShouldBeTrue(); - personCreatedEventCount.ShouldBe(++entityUpdatedEventTriggerCount); - - // Test with many to many - entityUpdatedEventTriggered = false; - await WithUnitOfWorkAsync(async () => - { - var entity = await AppEntityWithNavigationsRepository.GetAsync(entityId); - entity.ManyToMany = new List() - { - new AppEntityWithNavigationChildManyToMany - { - ChildName = "ChildName1" - } - }; - await AppEntityWithNavigationsRepository.UpdateAsync(entity); - }); - entityUpdatedEventTriggered.ShouldBeTrue(); - personCreatedEventCount.ShouldBe(++entityUpdatedEventTriggerCount); - - entityUpdatedEventTriggered = false; - await WithUnitOfWorkAsync(async () => - { - var entity = await AppEntityWithNavigationsRepository.GetAsync(entityId); - entity.ManyToMany.Clear(); - await AppEntityWithNavigationsRepository.UpdateAsync(entity); - }); - entityUpdatedEventTriggered.ShouldBeTrue(); - personCreatedEventCount.ShouldBe(++entityUpdatedEventTriggerCount); - } - private class MyCustomEventData { public string Value { get; set; } @@ -368,7 +219,7 @@ public abstract class AbpEntityChangeOptions_DomainEvents_Tests LocalEventBus.Subscribe>(data => { - entityUpdatedEventTriggered = !entityUpdatedEventTriggered; + entityUpdatedEventTriggered = true; return Task.CompletedTask; });