diff --git a/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/Events/AbpEntityChangeOptions.cs b/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/Events/AbpEntityChangeOptions.cs index 0d8d74f7b2..a42b921068 100644 --- a/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/Events/AbpEntityChangeOptions.cs +++ b/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/Events/AbpEntityChangeOptions.cs @@ -7,4 +7,11 @@ public class AbpEntityChangeOptions /// Publish the EntityUpdatedEvent when any navigation property changes. /// public bool PublishEntityUpdatedEventWhenNavigationChanges { get; set; } = true; + + public IEntitySelectorList IgnoredNavigationEntitySelectors { get; set; } + + public AbpEntityChangeOptions() + { + IgnoredNavigationEntitySelectors = new EntitySelectorList(); + } } diff --git a/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/Events/EntitySelectorList.cs b/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/Events/EntitySelectorList.cs new file mode 100644 index 0000000000..f0c89979f9 --- /dev/null +++ b/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/Events/EntitySelectorList.cs @@ -0,0 +1,8 @@ +using System.Collections.Generic; + +namespace Volo.Abp.Domain.Entities.Events; + +internal class EntitySelectorList : List, IEntitySelectorList +{ + +} diff --git a/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/Events/EntitySelectorListExtensions.cs b/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/Events/EntitySelectorListExtensions.cs new file mode 100644 index 0000000000..a05aaa616a --- /dev/null +++ b/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/Events/EntitySelectorListExtensions.cs @@ -0,0 +1,12 @@ +using System; + +namespace Volo.Abp.Domain.Entities.Events; + +public static class EntitySelectorListExtensions +{ + public static IEntitySelectorList Add(this IEntitySelectorList selectors, string name, Func predicate) + { + selectors.Add(new NamedTypeSelector(name, predicate)); + return selectors; + } +} diff --git a/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/Events/IEntitySelectorList.cs b/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/Events/IEntitySelectorList.cs new file mode 100644 index 0000000000..b3ed916dee --- /dev/null +++ b/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/Events/IEntitySelectorList.cs @@ -0,0 +1,8 @@ +using System.Collections.Generic; + +namespace Volo.Abp.Domain.Entities.Events; + +public interface IEntitySelectorList : IList +{ + +} 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 e9bf710d7d..626daf0bb4 100644 --- a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/AbpDbContext.cs +++ b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/AbpDbContext.cs @@ -202,46 +202,11 @@ public abstract class AbpDbContext : DbContext, IAbpEfCoreDbContext, } } - public override async Task SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = default) + public async override Task SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = default) { try { - foreach (var entityEntry in AbpEfCoreNavigationHelper.GetChangedEntityEntries()) - { - if (EntityChangeOptions.Value.PublishEntityUpdatedEventWhenNavigationChanges) - { - if (entityEntry.State == EntityState.Unchanged) - { - ApplyAbpConceptsForModifiedEntity(entityEntry, true); - } - - if (entityEntry.Entity is ISoftDelete && entityEntry.Entity.As().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().IsDeleted) - { - EntityChangeEventHelper.PublishEntityDeletedEvent(entityEntry.Entity); - } - else - { - EntityChangeEventHelper.PublishEntityUpdatedEvent(entityEntry.Entity); - } - } - } + await PublishEventsForChangedEntityOnSaveChangeAsync(); var auditLog = AuditingManager?.Current?.Log; List? entityChangeList = null; @@ -296,6 +261,55 @@ public abstract class AbpDbContext : DbContext, IAbpEfCoreDbContext, } } + protected virtual 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) || onlyForeignKeyModifiedEntity && ignoredEntity) + { + continue; + } + + if (entityEntry.State == EntityState.Unchanged) + { + ApplyAbpConceptsForModifiedEntity(entityEntry, true); + } + + if (entityEntry.Entity is ISoftDelete && entityEntry.Entity.As().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().IsDeleted) + { + EntityChangeEventHelper.PublishEntityDeletedEvent(entityEntry.Entity); + } + else + { + EntityChangeEventHelper.PublishEntityUpdatedEvent(entityEntry.Entity); + } + } + } + + return Task.CompletedTask; + } + protected virtual void PublishEntityEvents(EntityEventReport changeReport) { foreach (var localEvent in changeReport.DomainEvents) @@ -428,7 +442,9 @@ public abstract class AbpDbContext : DbContext, IAbpEfCoreDbContext, EntityChangeEventHelper.PublishEntityUpdatedEvent(entry.Entity); } } - else if (EntityChangeOptions.Value.PublishEntityUpdatedEventWhenNavigationChanges && AbpEfCoreNavigationHelper.IsNavigationEntryModified(entry)) + else if (EntityChangeOptions.Value.PublishEntityUpdatedEventWhenNavigationChanges && + EntityChangeOptions.Value.IgnoredNavigationEntitySelectors.All(selector => !selector.Predicate(entry.Entity.GetType())) && + AbpEfCoreNavigationHelper.IsNavigationEntryModified(entry)) { ApplyAbpConceptsForModifiedEntity(entry, true); if (entry.Entity is ISoftDelete && entry.Entity.As().IsDeleted) @@ -464,7 +480,9 @@ public abstract class AbpDbContext : DbContext, IAbpEfCoreDbContext, if (EntityChangeOptions.Value.PublishEntityUpdatedEventWhenNavigationChanges) { - foreach (var entry in AbpEfCoreNavigationHelper.GetChangedEntityEntries().Where(x => x.State == EntityState.Unchanged)) + foreach (var entry in AbpEfCoreNavigationHelper.GetChangedEntityEntries() + .Where(x => x.State == EntityState.Unchanged) + .Where(x=> EntityChangeOptions.Value.IgnoredNavigationEntitySelectors.All(selector => !selector.Predicate(x.Entity.GetType())))) { UpdateConcurrencyStamp(entry); } 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 f1b072c0ee..d82fb195ab 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 @@ -18,8 +18,31 @@ public class DomainEvents_Tests : DomainEvents_Tests +public class AbpEntityChangeOptions_DomainEvents_PublishEntityUpdatedEventWhenNavigationChanges_Tests : AbpEntityChangeOptions_DomainEvents_Tests { + protected override void AfterAddApplication(IServiceCollection services) + { + services.Configure(options => + { + options.PublishEntityUpdatedEventWhenNavigationChanges = false; + }); + + base.AfterAddApplication(services); + } +} + +public class AbpEntityChangeOptions_DomainEvents_IgnoreEntityChangeSelectorList_Tests : AbpEntityChangeOptions_DomainEvents_Tests +{ + protected override void AfterAddApplication(IServiceCollection services) + { + services.Configure(options => + { + options.PublishEntityUpdatedEventWhenNavigationChanges = true; + options.IgnoredNavigationEntitySelectors.Add("DisableAllEntity", _ => true); + }); + + base.AfterAddApplication(services); + } } public class AbpEfCoreDomainEvents_Tests : EntityFrameworkCoreTestBase 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 8ac64d40c9..032d714390 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 @@ -259,16 +259,6 @@ public abstract class AbpEntityChangeOptions_DomainEvents_Tests LocalEventBus = GetRequiredService(); } - protected override void AfterAddApplication(IServiceCollection services) - { - services.Configure(options => - { - options.PublishEntityUpdatedEventWhenNavigationChanges = false; - }); - - base.AfterAddApplication(services); - } - [Fact] public async Task Should_Not_Trigger_Domain_Events_For_Aggregate_Root_When_Navigation_Changes_Tests() { diff --git a/modules/cms-kit/src/Volo.CmsKit.Admin.Application/Volo/CmsKit/Admin/Blogs/BlogAdminAppService.cs b/modules/cms-kit/src/Volo.CmsKit.Admin.Application/Volo/CmsKit/Admin/Blogs/BlogAdminAppService.cs index e3ef03e2de..894060c966 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Admin.Application/Volo/CmsKit/Admin/Blogs/BlogAdminAppService.cs +++ b/modules/cms-kit/src/Volo.CmsKit.Admin.Application/Volo/CmsKit/Admin/Blogs/BlogAdminAppService.cs @@ -27,8 +27,8 @@ public class BlogAdminAppService : CmsKitAdminAppServiceBase, IBlogAdminAppServi public BlogAdminAppService( IBlogRepository blogRepository, - BlogManager blogManager, - IBlogPostRepository blogPostRepository, + BlogManager blogManager, + IBlogPostRepository blogPostRepository, BlogFeatureManager blogFeatureManager = null) { BlogRepository = blogRepository; @@ -56,7 +56,7 @@ public class BlogAdminAppService : CmsKitAdminAppServiceBase, IBlogAdminAppServi input.Sorting, input.MaxResultCount, input.SkipCount); - + var blogDtos = new PagedResultDto(totalCount, ObjectMapper.Map, List>(blogs.Select(x => x.Blog).ToList())); foreach (var blogDto in blogDtos.Items) @@ -66,11 +66,11 @@ public class BlogAdminAppService : CmsKitAdminAppServiceBase, IBlogAdminAppServi return blogDtos; } - + public virtual async Task> GetAllListAsync() { var blogs = await BlogRepository.GetListWithBlogPostCountAsync(maxResultCount: int.MaxValue); - + var blogDtos = new ListResultDto(ObjectMapper.Map, List>(blogs.Select(x => x.Blog).ToList())); foreach (var blogDto in blogDtos.Items) @@ -86,7 +86,7 @@ public class BlogAdminAppService : CmsKitAdminAppServiceBase, IBlogAdminAppServi { var blog = await BlogManager.CreateAsync(input.Name, input.Slug); input.MapExtraPropertiesTo(blog); - + await BlogRepository.InsertAsync(blog, autoSave: true); await BlogFeatureManager.SetDefaultsAsync(blog.Id); @@ -107,7 +107,7 @@ public class BlogAdminAppService : CmsKitAdminAppServiceBase, IBlogAdminAppServi return ObjectMapper.Map(blog); } - + [Authorize(CmsKitAdminPermissions.Blogs.Delete)] public virtual async Task MoveAllBlogPostsAsync(Guid blogId, Guid? assignToBlogId) { @@ -116,9 +116,9 @@ public class BlogAdminAppService : CmsKitAdminAppServiceBase, IBlogAdminAppServi } [Authorize(CmsKitAdminPermissions.Blogs.Delete)] - public virtual Task DeleteAsync(Guid id) + public virtual async Task DeleteAsync(Guid id) { - - return BlogRepository.DeleteAsync(id); + await BlogPostRepository.DeleteByBlogIdAsync(id); + await BlogRepository.DeleteAsync(id); } } diff --git a/modules/cms-kit/src/Volo.CmsKit.Domain/Volo/CmsKit/Blogs/IBlogPostRepository.cs b/modules/cms-kit/src/Volo.CmsKit.Domain/Volo/CmsKit/Blogs/IBlogPostRepository.cs index e4d8980196..9612c0e5e5 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Domain/Volo/CmsKit/Blogs/IBlogPostRepository.cs +++ b/modules/cms-kit/src/Volo.CmsKit.Domain/Volo/CmsKit/Blogs/IBlogPostRepository.cs @@ -48,4 +48,6 @@ public interface IBlogPostRepository : IBasicRepository Task HasBlogPostWaitingForReviewAsync(CancellationToken cancellationToken = default); Task UpdateBlogAsync(Guid sourceBlogId, Guid? targetBlogId = null, CancellationToken cancellationToken = default); -} \ No newline at end of file + + Task DeleteByBlogIdAsync(Guid blogId, CancellationToken cancellationToken = default); +} diff --git a/modules/cms-kit/src/Volo.CmsKit.EntityFrameworkCore/Volo/CmsKit/Blogs/EfCoreBlogPostRepository.cs b/modules/cms-kit/src/Volo.CmsKit.EntityFrameworkCore/Volo/CmsKit/Blogs/EfCoreBlogPostRepository.cs index 02085f5dc5..d54256bd8d 100644 --- a/modules/cms-kit/src/Volo.CmsKit.EntityFrameworkCore/Volo/CmsKit/Blogs/EfCoreBlogPostRepository.cs +++ b/modules/cms-kit/src/Volo.CmsKit.EntityFrameworkCore/Volo/CmsKit/Blogs/EfCoreBlogPostRepository.cs @@ -176,27 +176,32 @@ public class EfCoreBlogPostRepository : EfCoreRepository> CreateAuthorsQueryableAsync() + protected virtual async Task> CreateAuthorsQueryableAsync() { return (await GetDbContextAsync()).BlogPosts.Select(x => x.Author).Distinct(); } - + public virtual async Task HasBlogPostWaitingForReviewAsync(CancellationToken cancellationToken = default) { return await (await GetDbSetAsync()) .AnyAsync(x => x.Status == BlogPostStatus.WaitingForReview, GetCancellationToken(cancellationToken)); } - - public async Task UpdateBlogAsync(Guid sourceBlogId, Guid? targetBlogId = null, CancellationToken cancellationToken = default) + + public virtual async Task UpdateBlogAsync(Guid sourceBlogId, Guid? targetBlogId = null, CancellationToken cancellationToken = default) { if (targetBlogId != null) { await (await GetDbSetAsync()).Where(x => x.BlogId == sourceBlogId).ExecuteUpdateAsync(x => x.SetProperty(b => b.BlogId, targetBlogId.Value), GetCancellationToken(cancellationToken)); - + } else { await (await GetDbSetAsync()).Where(x => x.BlogId == sourceBlogId).ExecuteDeleteAsync(GetCancellationToken(cancellationToken)); } } -} \ No newline at end of file + + public virtual async Task DeleteByBlogIdAsync(Guid blogId, CancellationToken cancellationToken = default) + { + await DeleteAsync(x => x.BlogId == blogId, cancellationToken: cancellationToken); + } +} diff --git a/modules/cms-kit/src/Volo.CmsKit.MongoDB/Volo/CmsKit/MongoDB/Blogs/MongoBlogPostRepository.cs b/modules/cms-kit/src/Volo.CmsKit.MongoDB/Volo/CmsKit/MongoDB/Blogs/MongoBlogPostRepository.cs index b47abe1115..78afc5a871 100644 --- a/modules/cms-kit/src/Volo.CmsKit.MongoDB/Volo/CmsKit/MongoDB/Blogs/MongoBlogPostRepository.cs +++ b/modules/cms-kit/src/Volo.CmsKit.MongoDB/Volo/CmsKit/MongoDB/Blogs/MongoBlogPostRepository.cs @@ -23,7 +23,7 @@ public class MongoBlogPostRepository : MongoDbRepository dbContextProvider, + IMongoDbContextProvider dbContextProvider, MarkedItemManager markedItemManager, EntityTagManager entityTagManager) : base( dbContextProvider) @@ -63,7 +63,6 @@ public class MongoBlogPostRepository : MongoDbRepository tagFilteredEntityIds.Contains(x.Id)) .WhereIf(favoriteUserFilteredEntityIds.Any(), x => favoriteUserFilteredEntityIds.Contains(x.Id)) @@ -89,7 +88,7 @@ public class MongoBlogPostRepository : MongoDbRepository> CreateAuthorsQueryableAsync(CancellationToken cancellationToken = default) + protected virtual async Task> CreateAuthorsQueryableAsync(CancellationToken cancellationToken = default) { cancellationToken = GetCancellationToken(cancellationToken); - + var blogPostQueryable = (await GetQueryableAsync()) .Where(x => x.Status == BlogPostStatus.Published); @@ -229,7 +228,7 @@ public class MongoBlogPostRepository : MongoDbRepository x.Status == BlogPostStatus.WaitingForReview, cancellationToken); } - public async Task UpdateBlogAsync(Guid sourceBlogId, Guid? targetBlogId, CancellationToken cancellationToken = default) + public virtual async Task UpdateBlogAsync(Guid sourceBlogId, Guid? targetBlogId, CancellationToken cancellationToken = default) { cancellationToken = GetCancellationToken(cancellationToken); var blogPosts = await (await GetQueryableAsync(cancellationToken)).Where(x => x.BlogId == sourceBlogId).ToListAsync(cancellationToken); @@ -239,13 +238,18 @@ public class MongoBlogPostRepository : MongoDbRepository x.BlogId == blogId, cancellationToken: cancellationToken); + } } diff --git a/modules/cms-kit/test/Volo.CmsKit.Application.Tests/Blogs/BlogAdminAppService_Tests.cs b/modules/cms-kit/test/Volo.CmsKit.Application.Tests/Blogs/BlogAdminAppService_Tests.cs index 5be7d312b5..731191a251 100644 --- a/modules/cms-kit/test/Volo.CmsKit.Application.Tests/Blogs/BlogAdminAppService_Tests.cs +++ b/modules/cms-kit/test/Volo.CmsKit.Application.Tests/Blogs/BlogAdminAppService_Tests.cs @@ -12,12 +12,14 @@ public class BlogAdminAppService_Tests : CmsKitApplicationTestBase protected IBlogAdminAppService BlogAdminAppService { get; } protected CmsKitTestData CmsKitTestData { get; } protected IBlogRepository BlogRepository { get; } + protected IBlogPostRepository BlogPostRepository { get; } public BlogAdminAppService_Tests() { BlogAdminAppService = GetRequiredService(); CmsKitTestData = GetRequiredService(); BlogRepository = GetRequiredService(); + BlogPostRepository = GetRequiredService(); } [Fact] @@ -81,11 +83,17 @@ public class BlogAdminAppService_Tests : CmsKitApplicationTestBase [Fact] public async Task DeleteAsync_ShouldWork() { + var posts = await BlogPostRepository.GetListAsync(); + posts.ShouldContain(x => x.BlogId == CmsKitTestData.Blog_Id); + await BlogAdminAppService.DeleteAsync(CmsKitTestData.Blog_Id); await Should.ThrowAsync( async () => await BlogAdminAppService.GetAsync(CmsKitTestData.Blog_Id) ); + + posts = await BlogPostRepository.GetListAsync(); + posts.ShouldNotContain(x => x.BlogId == CmsKitTestData.Blog_Id); } } diff --git a/modules/openiddict/src/Volo.Abp.OpenIddict.EntityFrameworkCore/Volo/Abp/OpenIddict/EntityFrameworkCore/AbpOpenIddictEntityFrameworkCoreModule.cs b/modules/openiddict/src/Volo.Abp.OpenIddict.EntityFrameworkCore/Volo/Abp/OpenIddict/EntityFrameworkCore/AbpOpenIddictEntityFrameworkCoreModule.cs index 9ef08d5f2e..a12f4161c5 100644 --- a/modules/openiddict/src/Volo.Abp.OpenIddict.EntityFrameworkCore/Volo/Abp/OpenIddict/EntityFrameworkCore/AbpOpenIddictEntityFrameworkCoreModule.cs +++ b/modules/openiddict/src/Volo.Abp.OpenIddict.EntityFrameworkCore/Volo/Abp/OpenIddict/EntityFrameworkCore/AbpOpenIddictEntityFrameworkCoreModule.cs @@ -1,4 +1,5 @@ using Microsoft.Extensions.DependencyInjection; +using Volo.Abp.Domain.Entities.Events; using Volo.Abp.EntityFrameworkCore; using Volo.Abp.Modularity; using Volo.Abp.OpenIddict.Applications; @@ -25,5 +26,10 @@ public class AbpOpenIddictEntityFrameworkCoreModule : AbpModule options.AddRepository(); options.AddRepository(); }); + + Configure(options => + { + options.IgnoredNavigationEntitySelectors.Add("DisableOpenIddictApplication", type => type == typeof(OpenIddictApplication)); + }); } }