Browse Source

Added support for aggregate root domain events.

pull/216/head
Halil İbrahim Kalkan 9 years ago
parent
commit
d4e93e9da3
  1. 13
      src/Volo.Abp.Ddd/Volo/Abp/Domain/Entities/AggregateRoot.cs
  2. 2
      src/Volo.Abp.Ddd/Volo/Abp/Domain/Entities/IAggregateRoot.cs
  3. 9
      src/Volo.Abp.Ddd/Volo/Abp/Domain/Entities/IGeneratesDomainEvents.cs
  4. 107
      src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/AbpDbContext.cs
  5. 52
      test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/DomainEvents_Tests.cs
  6. 95
      test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/EntityChangeEvents_Tests.cs
  7. 11
      test/Volo.Abp.TestApp/Volo/Abp/TestApp/Domain/Person.cs
  8. 9
      test/Volo.Abp.TestApp/Volo/Abp/TestApp/Domain/PersonNameChangedEvent.cs

13
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
{
/// <inheritdoc cref="IAggregateRoot" />
public abstract class AggregateRoot : IAggregateRoot
{
[NotMapped] //TODO: Better to handle in EF Core layer?
public virtual ICollection<object> DomainEvents => _domainEvents ?? (_domainEvents = new Collection<object>());
private ICollection<object> _domainEvents;
}
/// <inheritdoc cref="IAggregateRoot{TKey}" />
public abstract class AggregateRoot<TKey> : Entity<TKey>, IAggregateRoot<TKey>
{
[NotMapped] //TODO: Better to handle in EF Core layer?
public virtual ICollection<object> DomainEvents => _domainEvents ?? (_domainEvents = new Collection<object>());
private ICollection<object> _domainEvents;
}
}

2
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 <see cref="IAggregateRoot{TKey}"/> where possible for better integration to repositories and other structures in the framework.
/// </summary>
public interface IAggregateRoot : IEntity
public interface IAggregateRoot : IEntity, IGeneratesDomainEvents
{
}

9
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<object> DomainEvents { get; }
}
}

107
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<TDbContext>).GetMethod(nameof(ConfigureGlobalFilters), BindingFlags.Instance | BindingFlags.NonPublic);
protected AbpDbContext(DbContextOptions<TDbContext> 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<int> 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<ISoftDelete>().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<DomainEventEntry> 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<Func<TEntity, bool>> multiTenantFilter = e => ((IMultiTenant)e).TenantId == CurrentTenantId || (((IMultiTenant)e).TenantId == CurrentTenantId) == IsMayHaveTenantFilterEnabled;
expression = expression == null ? multiTenantFilter : CombineExpressions(expression, multiTenantFilter);
}
return expression;
}

52
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<Person, Guid> _personRepository;
private readonly IEventBus _eventBus;
public DomainEvents_Tests()
{
_personRepository = GetRequiredService<IRepository<Person, Guid>>();
_eventBus = GetRequiredService<IEventBus>();
}
[Fact]
public async Task Should_Trigger_Domain_Events_For_Aggregate_Root()
{
//Arrange
var isTriggered = false;
_eventBus.Register<PersonNameChangedEvent>((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();
}
}
}

95
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<Person, Guid> _personRepository;
private readonly IEventBus _eventBus;
public EntityChangeEvents_Tests()
{
_personRepository = GetRequiredService<IRepository<Person, Guid>>();
_eventBus = GetRequiredService<IEventBus>();
}
[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<EntityCreatingEventData<Person>>(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<EntityCreatedEventData<Person>>(data =>
{
creatingEventTriggered.ShouldBeTrue();
createdEventTriggered.ShouldBeFalse();
updatingEventTriggered.ShouldBeTrue();
updatedEventTriggered.ShouldBeFalse();
createdEventTriggered = true;
data.Entity.Name.ShouldBe(personName);
});
_eventBus.Register<EntityUpdatingEventData<Person>>(data =>
{
creatingEventTriggered.ShouldBeTrue();
createdEventTriggered.ShouldBeFalse();
updatingEventTriggered.ShouldBeFalse();
updatedEventTriggered.ShouldBeFalse();
updatingEventTriggered = true;
data.Entity.Name.ShouldBe(personName);
data.Entity.Age.ShouldBe(18);
});
_eventBus.Register<EntityUpdatedEventData<Person>>(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();
}
}
}

11
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<Phone>();
}
public void ChangeName(string name)
{
Check.NotNullOrWhiteSpace(name, nameof(name));
var oldName = Name;
Name = name;
DomainEvents.Add(new PersonNameChangedEvent{Person = this, OldName = oldName});
}
}
}

9
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; }
}
}
Loading…
Cancel
Save