From d4e93e9da3392c30042af4fbd93ec4ca7bb68b07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halil=20=C4=B0brahim=20Kalkan?= Date: Wed, 21 Feb 2018 11:37:17 +0300 Subject: [PATCH] Added support for aggregate root domain events. --- .../Volo/Abp/Domain/Entities/AggregateRoot.cs | 13 ++- .../Abp/Domain/Entities/IAggregateRoot.cs | 2 +- .../Domain/Entities/IGeneratesDomainEvents.cs | 9 ++ .../Abp/EntityFrameworkCore/AbpDbContext.cs | 107 ++++++++++++++---- .../EntityFrameworkCore/DomainEvents_Tests.cs | 52 +++++++++ .../EntityChangeEvents_Tests.cs | 95 ++++++++++++++++ .../Volo/Abp/TestApp/Domain/Person.cs | 11 +- .../TestApp/Domain/PersonNameChangedEvent.cs | 9 ++ 8 files changed, 272 insertions(+), 26 deletions(-) create mode 100644 src/Volo.Abp.Ddd/Volo/Abp/Domain/Entities/IGeneratesDomainEvents.cs create mode 100644 test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/DomainEvents_Tests.cs create mode 100644 test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/EntityChangeEvents_Tests.cs create mode 100644 test/Volo.Abp.TestApp/Volo/Abp/TestApp/Domain/PersonNameChangedEvent.cs diff --git a/src/Volo.Abp.Ddd/Volo/Abp/Domain/Entities/AggregateRoot.cs b/src/Volo.Abp.Ddd/Volo/Abp/Domain/Entities/AggregateRoot.cs index a2b233406f..cb6f0f4de1 100644 --- a/src/Volo.Abp.Ddd/Volo/Abp/Domain/Entities/AggregateRoot.cs +++ b/src/Volo.Abp.Ddd/Volo/Abp/Domain/Entities/AggregateRoot.cs @@ -1,15 +1,24 @@ -namespace Volo.Abp.Domain.Entities -{ +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.ComponentModel.DataAnnotations.Schema; +namespace Volo.Abp.Domain.Entities +{ /// public abstract class AggregateRoot : IAggregateRoot { + [NotMapped] //TODO: Better to handle in EF Core layer? + public virtual ICollection DomainEvents => _domainEvents ?? (_domainEvents = new Collection()); + private ICollection _domainEvents; } /// public abstract class AggregateRoot : Entity, IAggregateRoot { + [NotMapped] //TODO: Better to handle in EF Core layer? + public virtual ICollection DomainEvents => _domainEvents ?? (_domainEvents = new Collection()); + private ICollection _domainEvents; } } \ No newline at end of file diff --git a/src/Volo.Abp.Ddd/Volo/Abp/Domain/Entities/IAggregateRoot.cs b/src/Volo.Abp.Ddd/Volo/Abp/Domain/Entities/IAggregateRoot.cs index bdb436c51d..ec5d681685 100644 --- a/src/Volo.Abp.Ddd/Volo/Abp/Domain/Entities/IAggregateRoot.cs +++ b/src/Volo.Abp.Ddd/Volo/Abp/Domain/Entities/IAggregateRoot.cs @@ -4,7 +4,7 @@ /// Defines an aggregate root. It's primary key may not be "Id" or it may have a composite primary key. /// Use where possible for better integration to repositories and other structures in the framework. /// - public interface IAggregateRoot : IEntity + public interface IAggregateRoot : IEntity, IGeneratesDomainEvents { } diff --git a/src/Volo.Abp.Ddd/Volo/Abp/Domain/Entities/IGeneratesDomainEvents.cs b/src/Volo.Abp.Ddd/Volo/Abp/Domain/Entities/IGeneratesDomainEvents.cs new file mode 100644 index 0000000000..4fdfe54ee7 --- /dev/null +++ b/src/Volo.Abp.Ddd/Volo/Abp/Domain/Entities/IGeneratesDomainEvents.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; + +namespace Volo.Abp.Domain.Entities +{ + public interface IGeneratesDomainEvents + { + ICollection DomainEvents { get; } + } +} diff --git a/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/AbpDbContext.cs b/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/AbpDbContext.cs index bb5f9673ce..b7aabf47db 100644 --- a/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/AbpDbContext.cs +++ b/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/AbpDbContext.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.ComponentModel.DataAnnotations.Schema; using System.Linq; using System.Linq.Expressions; @@ -10,6 +11,7 @@ using Microsoft.EntityFrameworkCore.ChangeTracking; using Microsoft.EntityFrameworkCore.Metadata; using Volo.Abp.Data; using Volo.Abp.Domain.Entities; +using Volo.Abp.Domain.Entities.Events; using Volo.Abp.Guids; using Volo.Abp.MultiTenancy; using Volo.Abp.Reflection; @@ -31,12 +33,15 @@ namespace Volo.Abp.EntityFrameworkCore public IDataFilter DataFilter { get; set; } + public IEntityChangeEventHelper EntityChangeEventHelper { get; set; } + private static readonly MethodInfo ConfigureGlobalFiltersMethodInfo = typeof(AbpDbContext).GetMethod(nameof(ConfigureGlobalFilters), BindingFlags.Instance | BindingFlags.NonPublic); protected AbpDbContext(DbContextOptions options) : base(options) { GuidGenerator = SimpleGuidGenerator.Instance; + EntityChangeEventHelper = NullEntityChangeEventHelper.Instance; } protected override void OnModelCreating(ModelBuilder modelBuilder) @@ -56,12 +61,14 @@ namespace Volo.Abp.EntityFrameworkCore public override int SaveChanges(bool acceptAllChangesOnSuccess) { ChangeTracker.DetectChanges(); - ApplyAbpConcepts(); try { - ChangeTracker.AutoDetectChangesEnabled = false; - return base.SaveChanges(acceptAllChangesOnSuccess); + ChangeTracker.AutoDetectChangesEnabled = false; //TODO: Why this is needed? + var changeReport = ApplyAbpConcepts(); + var result = base.SaveChanges(acceptAllChangesOnSuccess); + EntityChangeEventHelper.TriggerEvents(changeReport); + return result; } catch (DbUpdateConcurrencyException ex) { @@ -76,12 +83,14 @@ namespace Volo.Abp.EntityFrameworkCore public override async Task SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = default) { ChangeTracker.DetectChanges(); - ApplyAbpConcepts(); try { - ChangeTracker.AutoDetectChangesEnabled = false; - return await base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken); + ChangeTracker.AutoDetectChangesEnabled = false; //TODO: Why this is needed? + var changeReport = ApplyAbpConcepts(); + var result = await base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken); + await EntityChangeEventHelper.TriggerEventsAsync(changeReport); + return result; } catch (DbUpdateConcurrencyException ex) { @@ -106,26 +115,80 @@ namespace Volo.Abp.EntityFrameworkCore .IsConcurrencyToken = true; } - protected virtual void ApplyAbpConcepts() + protected virtual EntityChangeReport ApplyAbpConcepts() { /* Implement other concepts from ABP v1.x */ - foreach (var entry in ChangeTracker.Entries()) + var changeReport = new EntityChangeReport(); + + foreach (var entry in ChangeTracker.Entries().ToList()) { - switch (entry.State) - { - case EntityState.Added: - CheckAndSetId(entry); - break; - case EntityState.Modified: - HandleConcurrencyStamp(entry); - break; - case EntityState.Deleted: - CancelDeletionForSoftDelete(entry); - HandleConcurrencyStamp(entry); - break; - } + ApplyAbpConcepts(entry, changeReport); } + + return changeReport; + } + + protected virtual void ApplyAbpConcepts(EntityEntry entry, EntityChangeReport changeReport) + { + switch (entry.State) + { + case EntityState.Added: + ApplyAbpConceptsForAddedEntity(entry, changeReport); + break; + case EntityState.Modified: + ApplyAbpConceptsForModifiedEntity(entry, changeReport); + break; + case EntityState.Deleted: + ApplyAbpConceptsForDeletedEntity(entry, changeReport); + break; + } + + AddDomainEvents(changeReport.DomainEvents, entry.Entity); + } + + protected virtual void ApplyAbpConceptsForAddedEntity(EntityEntry entry, EntityChangeReport changeReport) + { + CheckAndSetId(entry); + changeReport.ChangedEntities.Add(new EntityChangeEntry(entry.Entity, EntityChangeType.Created)); + } + + protected virtual void ApplyAbpConceptsForModifiedEntity(EntityEntry entry, EntityChangeReport changeReport) + { + HandleConcurrencyStamp(entry); + + if (entry.Entity is ISoftDelete && entry.Entity.As().IsDeleted) + { + changeReport.ChangedEntities.Add(new EntityChangeEntry(entry.Entity, EntityChangeType.Deleted)); + } + else + { + changeReport.ChangedEntities.Add(new EntityChangeEntry(entry.Entity, EntityChangeType.Updated)); + } + } + + protected virtual void ApplyAbpConceptsForDeletedEntity(EntityEntry entry, EntityChangeReport changeReport) + { + CancelDeletionForSoftDelete(entry); + HandleConcurrencyStamp(entry); + changeReport.ChangedEntities.Add(new EntityChangeEntry(entry.Entity, EntityChangeType.Deleted)); + } + + protected virtual void AddDomainEvents(List domainEvents, object entityAsObj) + { + var generatesDomainEventsEntity = entityAsObj as IGeneratesDomainEvents; + if (generatesDomainEventsEntity == null) + { + return; + } + + if (generatesDomainEventsEntity.DomainEvents.IsNullOrEmpty()) + { + return; + } + + domainEvents.AddRange(generatesDomainEventsEntity.DomainEvents.Select(eventData => new DomainEventEntry(entityAsObj, eventData))); + generatesDomainEventsEntity.DomainEvents.Clear(); } protected virtual void HandleConcurrencyStamp(EntityEntry entry) @@ -219,7 +282,7 @@ namespace Volo.Abp.EntityFrameworkCore Expression> multiTenantFilter = e => ((IMultiTenant)e).TenantId == CurrentTenantId || (((IMultiTenant)e).TenantId == CurrentTenantId) == IsMayHaveTenantFilterEnabled; expression = expression == null ? multiTenantFilter : CombineExpressions(expression, multiTenantFilter); } - + return expression; } diff --git a/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/DomainEvents_Tests.cs b/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/DomainEvents_Tests.cs new file mode 100644 index 0000000000..0b56ec03a5 --- /dev/null +++ b/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/DomainEvents_Tests.cs @@ -0,0 +1,52 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Shouldly; +using Volo.Abp.Domain.Repositories; +using Volo.Abp.EventBus; +using Volo.Abp.TestApp.Domain; +using Xunit; + +namespace Volo.Abp.EntityFrameworkCore +{ + public class DomainEvents_Tests : EntityFrameworkCoreTestBase + { + private readonly IRepository _personRepository; + private readonly IEventBus _eventBus; + + public DomainEvents_Tests() + { + _personRepository = GetRequiredService>(); + _eventBus = GetRequiredService(); + } + + [Fact] + public async Task Should_Trigger_Domain_Events_For_Aggregate_Root() + { + //Arrange + + var isTriggered = false; + + _eventBus.Register((data) => + { + data.OldName.ShouldBe("Douglas"); + data.Person.Name.ShouldBe("Douglas-Changed"); + isTriggered = true; + }); + + //Act + + await WithUnitOfWorkAsync(async () => + { + var dougles = await _personRepository.SingleAsync(b => b.Name == "Douglas"); + dougles.ChangeName("Douglas-Changed"); + await _personRepository.UpdateAsync(dougles); + }); + + //Assert + + isTriggered.ShouldBeTrue(); + } + } +} \ No newline at end of file diff --git a/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/EntityChangeEvents_Tests.cs b/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/EntityChangeEvents_Tests.cs new file mode 100644 index 0000000000..287e44ea3c --- /dev/null +++ b/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/EntityChangeEvents_Tests.cs @@ -0,0 +1,95 @@ +using System; +using Shouldly; +using Volo.Abp.Domain.Entities.Events; +using Volo.Abp.Domain.Repositories; +using Volo.Abp.EventBus; +using Volo.Abp.TestApp.Domain; +using Xunit; + +namespace Volo.Abp.EntityFrameworkCore +{ + public class EntityChangeEvents_Tests : EntityFrameworkCoreTestBase + { + private readonly IRepository _personRepository; + private readonly IEventBus _eventBus; + + public EntityChangeEvents_Tests() + { + _personRepository = GetRequiredService>(); + _eventBus = GetRequiredService(); + } + + [Fact] + public void Complex_Event_Test() + { + var personName = Guid.NewGuid().ToString("N"); + + var creatingEventTriggered = false; + var createdEventTriggered = false; + var updatingEventTriggered = false; + var updatedEventTriggered = false; + + _eventBus.Register>(data => + { + creatingEventTriggered.ShouldBeFalse(); + createdEventTriggered.ShouldBeFalse(); + updatingEventTriggered.ShouldBeFalse(); + updatedEventTriggered.ShouldBeFalse(); + + creatingEventTriggered = true; + + data.Entity.Name.ShouldBe(personName); + + /* Want to change age from 15 to 18 + * Expect to trigger EntityUpdatingEventData, EntityUpdatedEventData events */ + data.Entity.Age.ShouldBe(15); + data.Entity.Age = 18; + }); + + _eventBus.Register>(data => + { + creatingEventTriggered.ShouldBeTrue(); + createdEventTriggered.ShouldBeFalse(); + updatingEventTriggered.ShouldBeTrue(); + updatedEventTriggered.ShouldBeFalse(); + + createdEventTriggered = true; + + data.Entity.Name.ShouldBe(personName); + }); + + _eventBus.Register>(data => + { + creatingEventTriggered.ShouldBeTrue(); + createdEventTriggered.ShouldBeFalse(); + updatingEventTriggered.ShouldBeFalse(); + updatedEventTriggered.ShouldBeFalse(); + + updatingEventTriggered = true; + + data.Entity.Name.ShouldBe(personName); + data.Entity.Age.ShouldBe(18); + }); + + _eventBus.Register>(data => + { + creatingEventTriggered.ShouldBeTrue(); + createdEventTriggered.ShouldBeTrue(); + updatingEventTriggered.ShouldBeTrue(); + updatedEventTriggered.ShouldBeFalse(); + + updatedEventTriggered = true; + + data.Entity.Name.ShouldBe(personName); + data.Entity.Age.ShouldBe(18); + }); + + _personRepository.Insert(new Person(Guid.NewGuid(), personName, 15)); + + creatingEventTriggered.ShouldBeTrue(); + createdEventTriggered.ShouldBeTrue(); + updatingEventTriggered.ShouldBeTrue(); + updatedEventTriggered.ShouldBeTrue(); + } + } +} diff --git a/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Domain/Person.cs b/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Domain/Person.cs index 553ca30abd..9f2e02d3b4 100644 --- a/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Domain/Person.cs +++ b/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Domain/Person.cs @@ -9,7 +9,7 @@ namespace Volo.Abp.TestApp.Domain { public virtual Guid? TenantId { get; set; } - public virtual string Name { get; set; } + public virtual string Name { get; private set; } public virtual int Age { get; set; } @@ -31,5 +31,14 @@ namespace Volo.Abp.TestApp.Domain Phones = new Collection(); } + + public void ChangeName(string name) + { + Check.NotNullOrWhiteSpace(name, nameof(name)); + + var oldName = Name; + Name = name; + DomainEvents.Add(new PersonNameChangedEvent{Person = this, OldName = oldName}); + } } } \ No newline at end of file diff --git a/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Domain/PersonNameChangedEvent.cs b/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Domain/PersonNameChangedEvent.cs new file mode 100644 index 0000000000..4ccab766cb --- /dev/null +++ b/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Domain/PersonNameChangedEvent.cs @@ -0,0 +1,9 @@ +namespace Volo.Abp.TestApp.Domain +{ + public class PersonNameChangedEvent + { + public Person Person { get; set; } + + public string OldName { get; set; } + } +} \ No newline at end of file