diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Reflection/TypeHelper.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Reflection/TypeHelper.cs
index 83182fc965..83c2c4c38e 100644
--- a/framework/src/Volo.Abp.Core/Volo/Abp/Reflection/TypeHelper.cs
+++ b/framework/src/Volo.Abp.Core/Volo/Abp/Reflection/TypeHelper.cs
@@ -359,5 +359,15 @@ namespace Volo.Abp.Reflection
? type.GenericTypeArguments[0]
: type;
}
+
+ public static bool IsDefaultValue([CanBeNull] object obj)
+ {
+ if (obj == null)
+ {
+ return true;
+ }
+
+ return obj.Equals(GetDefaultValue(obj.GetType()));
+ }
}
}
diff --git a/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/Entity.cs b/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/Entity.cs
index b78a5aa400..cf1b643305 100644
--- a/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/Entity.cs
+++ b/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/Entity.cs
@@ -1,7 +1,5 @@
using System;
using System.Collections.Generic;
-using System.Reflection;
-using Volo.Abp.MultiTenancy;
namespace Volo.Abp.Domain.Entities
{
@@ -16,6 +14,11 @@ namespace Volo.Abp.Domain.Entities
}
public abstract object[] GetKeys();
+
+ public bool EntityEquals(IEntity other)
+ {
+ return EntityHelper.EntityEquals(this, other);
+ }
}
///
@@ -35,44 +38,6 @@ namespace Volo.Abp.Domain.Entities
Id = id;
}
- public bool EntityEquals(object obj)
- {
- if (obj == null || !(obj is Entity))
- {
- return false;
- }
-
- //Same instances must be considered as equal
- if (ReferenceEquals(this, obj))
- {
- return true;
- }
-
- //Transient objects are not considered as equal
- var other = (Entity)obj;
- if (EntityHelper.HasDefaultId(this) && EntityHelper.HasDefaultId(other))
- {
- return false;
- }
-
- //Must have a IS-A relation of types or must be same type
- var typeOfThis = GetType().GetTypeInfo();
- var typeOfOther = other.GetType().GetTypeInfo();
- if (!typeOfThis.IsAssignableFrom(typeOfOther) && !typeOfOther.IsAssignableFrom(typeOfThis))
- {
- return false;
- }
-
- //Different tenants may have an entity with same Id.
- if (this is IMultiTenant && other is IMultiTenant &&
- this.As().TenantId != other.As().TenantId)
- {
- return false;
- }
-
- return Id.Equals(other.Id);
- }
-
public override object[] GetKeys()
{
return new object[] {Id};
diff --git a/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/EntityHelper.cs b/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/EntityHelper.cs
index ad082c3fb0..65f2992257 100644
--- a/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/EntityHelper.cs
+++ b/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/EntityHelper.cs
@@ -5,6 +5,8 @@ using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using JetBrains.Annotations;
+using Volo.Abp.MultiTenancy;
+using Volo.Abp.Reflection;
namespace Volo.Abp.Domain.Entities
{
@@ -16,6 +18,98 @@ namespace Volo.Abp.Domain.Entities
private static readonly ConcurrentDictionary CachedIdProperties =
new ConcurrentDictionary();
+ public static bool EntityEquals(IEntity entity1, IEntity entity2)
+ {
+ if (entity1 == null || entity2 == null)
+ {
+ return false;
+ }
+
+ //Same instances must be considered as equal
+ if (ReferenceEquals(entity1, entity2))
+ {
+ return true;
+ }
+
+ //Must have a IS-A relation of types or must be same type
+ var typeOfEntity1 = entity1.GetType();
+ var typeOfEntity2 = entity2.GetType();
+ if (!typeOfEntity1.IsAssignableFrom(typeOfEntity2) && !typeOfEntity2.IsAssignableFrom(typeOfEntity1))
+ {
+ return false;
+ }
+
+ //Different tenants may have an entity with same Id.
+ if (entity1 is IMultiTenant && entity2 is IMultiTenant)
+ {
+ var tenant1Id = ((IMultiTenant) entity1).TenantId;
+ var tenant2Id = ((IMultiTenant) entity2).TenantId;
+
+ if (tenant1Id != tenant2Id)
+ {
+ if (tenant1Id == null || tenant2Id == null)
+ {
+ return false;
+ }
+
+ if (!tenant1Id.Equals(tenant2Id))
+ {
+ return false;
+ }
+ }
+ }
+
+ //Transient objects are not considered as equal
+ if (HasDefaultKeys(entity1) && HasDefaultKeys(entity2))
+ {
+ return false;
+ }
+
+ var entity1Keys = entity1.GetKeys();
+ var entity2Keys = entity2.GetKeys();
+
+ if (entity1Keys.Length != entity2Keys.Length)
+ {
+ return false;
+ }
+
+ for (var i = 0; i < entity1Keys.Length; i++)
+ {
+ var entity1Key = entity1Keys[i];
+ var entity2Key = entity2Keys[i];
+
+ if (entity1Key == null)
+ {
+ if (entity2Key == null)
+ {
+ //Both null, so considered as equals
+ continue;
+ }
+
+ //entity2Key is not null!
+ return false;
+ }
+
+ if (entity2Key == null)
+ {
+ //entity1Key was not null!
+ return false;
+ }
+
+ if (TypeHelper.IsDefaultValue(entity1Key) && TypeHelper.IsDefaultValue(entity2Key))
+ {
+ return false;
+ }
+
+ if (!entity1Key.Equals(entity2Key))
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
public static bool IsEntity([NotNull] Type type)
{
return typeof(IEntity).IsAssignableFrom(type);
@@ -25,7 +119,8 @@ namespace Volo.Abp.Domain.Entities
{
foreach (var interfaceType in type.GetInterfaces())
{
- if (interfaceType.GetTypeInfo().IsGenericType && interfaceType.GetGenericTypeDefinition() == typeof(IEntity<>))
+ if (interfaceType.GetTypeInfo().IsGenericType &&
+ interfaceType.GetGenericTypeDefinition() == typeof(IEntity<>))
{
return true;
}
@@ -55,6 +150,44 @@ namespace Volo.Abp.Domain.Entities
return false;
}
+ private static bool IsDefaultKeyValue(object value)
+ {
+ if (value == null)
+ {
+ return true;
+ }
+
+ var type = value.GetType();
+
+ //Workaround for EF Core since it sets int/long to min value when attaching to DbContext
+ if (type == typeof(int))
+ {
+ return Convert.ToInt32(value) <= 0;
+ }
+
+ if (type == typeof(long))
+ {
+ return Convert.ToInt64(value) <= 0;
+ }
+
+ return TypeHelper.IsDefaultValue(value);
+ }
+
+ public static bool HasDefaultKeys([NotNull] IEntity entity)
+ {
+ Check.NotNull(entity, nameof(entity));
+
+ foreach (var key in entity.GetKeys())
+ {
+ if (!IsDefaultKeyValue(key))
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
///
/// Tries to find the primary key type of the given entity type.
/// May return null if given type does not implement
@@ -75,12 +208,14 @@ namespace Volo.Abp.Domain.Entities
{
if (!typeof(IEntity).IsAssignableFrom(entityType))
{
- throw new AbpException($"Given {nameof(entityType)} is not an entity. It should implement {typeof(IEntity).AssemblyQualifiedName}!");
+ throw new AbpException(
+ $"Given {nameof(entityType)} is not an entity. It should implement {typeof(IEntity).AssemblyQualifiedName}!");
}
foreach (var interfaceType in entityType.GetTypeInfo().GetInterfaces())
{
- if (interfaceType.GetTypeInfo().IsGenericType && interfaceType.GetGenericTypeDefinition() == typeof(IEntity<>))
+ if (interfaceType.GetTypeInfo().IsGenericType &&
+ interfaceType.GetGenericTypeDefinition() == typeof(IEntity<>))
{
return interfaceType.GenericTypeArguments[0];
}
@@ -132,4 +267,4 @@ namespace Volo.Abp.Domain.Entities
property?.SetValue(entity, idFactory());
}
}
-}
+}
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/Events/EntityChangeEventHelper.cs b/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/Events/EntityChangeEventHelper.cs
index cad12b1450..5de3b22c77 100644
--- a/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/Events/EntityChangeEventHelper.cs
+++ b/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/Events/EntityChangeEventHelper.cs
@@ -1,6 +1,9 @@
using System;
using System.Collections.Generic;
+using System.Linq;
using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Volo.Abp.Auditing;
using Volo.Abp.DependencyInjection;
@@ -18,6 +21,7 @@ namespace Volo.Abp.Domain.Entities.Events
///
public class EntityChangeEventHelper : IEntityChangeEventHelper, ITransientDependency
{
+ public ILogger Logger { get; set; }
public ILocalEventBus LocalEventBus { get; set; }
public IDistributedEventBus DistributedEventBus { get; set; }
@@ -36,6 +40,7 @@ namespace Volo.Abp.Domain.Entities.Events
LocalEventBus = NullLocalEventBus.Instance;
DistributedEventBus = NullDistributedEventBus.Instance;
+ Logger = NullLogger.Instance;
}
public async Task TriggerEventsAsync(EntityChangeReport changeReport)
@@ -56,6 +61,7 @@ namespace Volo.Abp.Domain.Entities.Events
LocalEventBus,
typeof(EntityCreatingEventData<>),
entity,
+ entity,
true
);
}
@@ -66,6 +72,7 @@ namespace Volo.Abp.Domain.Entities.Events
LocalEventBus,
typeof(EntityCreatedEventData<>),
entity,
+ entity,
false
);
@@ -78,6 +85,7 @@ namespace Volo.Abp.Domain.Entities.Events
DistributedEventBus,
typeof(EntityCreatedEto<>),
eto,
+ entity,
false
);
}
@@ -101,6 +109,7 @@ namespace Volo.Abp.Domain.Entities.Events
LocalEventBus,
typeof(EntityUpdatingEventData<>),
entity,
+ entity,
true
);
}
@@ -111,6 +120,7 @@ namespace Volo.Abp.Domain.Entities.Events
LocalEventBus,
typeof(EntityUpdatedEventData<>),
entity,
+ entity,
false
);
@@ -123,6 +133,7 @@ namespace Volo.Abp.Domain.Entities.Events
DistributedEventBus,
typeof(EntityUpdatedEto<>),
eto,
+ entity,
false
);
}
@@ -135,6 +146,7 @@ namespace Volo.Abp.Domain.Entities.Events
LocalEventBus,
typeof(EntityDeletingEventData<>),
entity,
+ entity,
true
);
}
@@ -145,6 +157,7 @@ namespace Volo.Abp.Domain.Entities.Events
LocalEventBus,
typeof(EntityDeletedEventData<>),
entity,
+ entity,
false
);
@@ -157,6 +170,7 @@ namespace Volo.Abp.Domain.Entities.Events
DistributedEventBus,
typeof(EntityDeletedEto<>),
eto,
+ entity,
false
);
}
@@ -206,22 +220,129 @@ namespace Volo.Abp.Domain.Entities.Events
{
foreach (var distributedEvent in distributedEvents)
{
- await DistributedEventBus.PublishAsync(distributedEvent.EventData.GetType(), distributedEvent.EventData);
+ await DistributedEventBus.PublishAsync(distributedEvent.EventData.GetType(),
+ distributedEvent.EventData);
}
}
- protected virtual async Task TriggerEventWithEntity(IEventBus eventPublisher, Type genericEventType, object entity, bool triggerInCurrentUnitOfWork)
+ protected virtual async Task TriggerEventWithEntity(
+ IEventBus eventPublisher,
+ Type genericEventType,
+ object entityOrEto,
+ object originalEntity,
+ bool triggerInCurrentUnitOfWork)
{
- var entityType = ProxyHelper.UnProxy(entity).GetType();
+ var entityType = ProxyHelper.UnProxy(entityOrEto).GetType();
var eventType = genericEventType.MakeGenericType(entityType);
+ var currentUow = UnitOfWorkManager.Current;
- if (triggerInCurrentUnitOfWork || UnitOfWorkManager.Current == null)
+ if (triggerInCurrentUnitOfWork || currentUow == null)
{
- await eventPublisher.PublishAsync(eventType, Activator.CreateInstance(eventType, entity));
+ await eventPublisher.PublishAsync(
+ eventType,
+ Activator.CreateInstance(eventType, entityOrEto)
+ );
+
return;
}
- UnitOfWorkManager.Current.OnCompleted(() => eventPublisher.PublishAsync(eventType, Activator.CreateInstance(eventType, entity)));
+ var eventList = GetEventList(currentUow);
+ var isFirstEvent = !eventList.Any();
+
+ eventList.AddUniqueEvent(eventPublisher, eventType, entityOrEto, originalEntity);
+
+ /* Register to OnCompleted if this is the first item.
+ * Other items will already be in the list once the UOW completes.
+ */
+ if (isFirstEvent)
+ {
+ currentUow.OnCompleted(
+ async () =>
+ {
+ foreach (var eventEntry in eventList)
+ {
+ try
+ {
+ await eventPublisher.PublishAsync(
+ eventEntry.EventType,
+ Activator.CreateInstance(eventEntry.EventType, eventEntry.EntityOrEto)
+ );
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError(
+ $"Caught an exception while publishing the event '{eventType.FullName}' for the entity '{entityOrEto}'");
+ Logger.LogException(ex);
+ }
+ }
+ }
+ );
+ }
+ }
+
+ private EntityChangeEventList GetEventList(IUnitOfWork currentUow)
+ {
+ return (EntityChangeEventList) currentUow.Items.GetOrAdd(
+ "AbpEntityChangeEventList",
+ () => new EntityChangeEventList()
+ );
+ }
+
+ private class EntityChangeEventList : List
+ {
+ public void AddUniqueEvent(IEventBus eventBus, Type eventType, object entityOrEto, object originalEntity)
+ {
+ var newEntry = new EntityChangeEventEntry(eventBus, eventType, entityOrEto, originalEntity);
+
+ //Latest "same" event overrides the previous events.
+ for (var i = 0; i < Count; i++)
+ {
+ if (this[i].IsSameEvent(newEntry))
+ {
+ this[i] = newEntry;
+ return;
+ }
+ }
+
+ //If this is a "new" event, add to the end
+ Add(newEntry);
+ }
+ }
+
+ private class EntityChangeEventEntry
+ {
+ public IEventBus EventBus { get; }
+
+ public Type EventType { get; }
+
+ public object EntityOrEto { get; }
+
+ public object OriginalEntity { get; }
+
+ public EntityChangeEventEntry(IEventBus eventBus, Type eventType, object entityOrEto, object originalEntity)
+ {
+ EventType = eventType;
+ EntityOrEto = entityOrEto;
+ OriginalEntity = originalEntity;
+ EventBus = eventBus;
+ }
+
+ public bool IsSameEvent(EntityChangeEventEntry otherEntry)
+ {
+ if (EventBus != otherEntry.EventBus || EventType != otherEntry.EventType)
+ {
+ return false;
+ }
+
+ var originalEntityRef = OriginalEntity as IEntity;
+ var otherOriginalEntityRef = otherEntry.OriginalEntity as IEntity;
+ if (originalEntityRef == null || otherOriginalEntityRef == null)
+ {
+ return false;
+ }
+
+ return EntityHelper.EntityEquals(originalEntityRef, otherOriginalEntityRef);
+ }
}
}
}
\ No newline at end of file
diff --git a/framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Entities/Entity_Tests.cs b/framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Entities/Entity_Tests.cs
new file mode 100644
index 0000000000..779ac3fe49
--- /dev/null
+++ b/framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Entities/Entity_Tests.cs
@@ -0,0 +1,139 @@
+using System;
+using Shouldly;
+using Volo.Abp.MultiTenancy;
+using Xunit;
+
+namespace Volo.Abp.Domain.Entities
+{
+ public class Entity_Tests
+ {
+ [Fact]
+ public void EntityEquals_Should_Return_True_For_Same_Keys()
+ {
+ var idValue1 = Guid.NewGuid();
+ var idValue2 = Guid.NewGuid();
+
+ new Person(idValue1).EntityEquals(new Person(idValue1)).ShouldBeTrue();
+
+ new Car(42).EntityEquals(new Car(42)).ShouldBeTrue();
+
+ new Product("a").EntityEquals(new Product("a")).ShouldBeTrue();
+
+ new Phone(idValue1, "123").EntityEquals(new Phone(idValue1, "123")).ShouldBeTrue();
+ }
+
+ [Fact]
+ public void EntityEquals_Should_Return_False_For_Different_Keys()
+ {
+ var idValue1 = Guid.NewGuid();
+ var idValue2 = Guid.NewGuid();
+
+ new Person(idValue1).EntityEquals(new Person()).ShouldBeFalse();
+ new Person(idValue1).EntityEquals(new Person(idValue2)).ShouldBeFalse();
+
+ new Car(42).EntityEquals(new Car()).ShouldBeFalse();
+ new Car(42).EntityEquals(new Car(43)).ShouldBeFalse();
+
+ new Product("a").EntityEquals(new Product()).ShouldBeFalse();
+ new Product("a").EntityEquals(new Product("b")).ShouldBeFalse();
+
+ new Phone(idValue1, "123").EntityEquals(new Phone()).ShouldBeFalse();
+ new Phone(idValue1, "123").EntityEquals(new Phone(idValue1, null)).ShouldBeFalse();
+ new Phone(idValue1, "123").EntityEquals(new Phone(idValue1, "321")).ShouldBeFalse();
+ }
+
+ [Fact]
+ public void EntityEquals_Should_Return_False_For_Both_Default_Keys()
+ {
+ new Person().EntityEquals(new Person()).ShouldBeFalse();
+
+ new Car().EntityEquals(new Car()).ShouldBeFalse();
+
+ new Product().EntityEquals(new Product()).ShouldBeFalse();
+
+ new Phone().EntityEquals(new Phone()).ShouldBeFalse();
+ }
+
+ [Fact]
+ public void Different_Tenants_With_Same_Keys_Considered_As_Different_Objects()
+ {
+ var tenantId1 = Guid.NewGuid();
+ var tenantId2 = Guid.NewGuid();
+
+ new Car(42, tenantId1).EntityEquals(new Car(42)).ShouldBeFalse();
+ new Car(42).EntityEquals(new Car(42, tenantId2)).ShouldBeFalse();
+ new Car(42, tenantId1).EntityEquals(new Car(42, tenantId2)).ShouldBeFalse();
+ }
+
+ [Fact]
+ public void Same_Tenants_With_Same_Keys_Considered_As_Same_Objects()
+ {
+ var tenantId1 = Guid.NewGuid();
+
+ new Car(42).EntityEquals(new Car(42)).ShouldBeTrue();
+ new Car(42, tenantId1).EntityEquals(new Car(42, tenantId1)).ShouldBeTrue();
+ }
+
+ public class Person : Entity
+ {
+ public Person()
+ {
+ }
+
+ public Person(Guid id)
+ : base(id)
+ {
+ }
+ }
+
+ public class Car : Entity, IMultiTenant
+ {
+ public Guid? TenantId { get; }
+
+ public Car()
+ {
+ }
+
+ public Car(int id, Guid? tenantId = null)
+ : base(id)
+ {
+ TenantId = tenantId;
+ }
+ }
+
+ public class Product : Entity
+ {
+ public Product()
+ {
+ }
+
+ public Product(string id)
+ : base(id)
+ {
+ }
+ }
+
+ public class Phone : Entity
+ {
+ public Guid PersonId { get; set; }
+
+ public string Number { get; set; }
+
+ public Phone()
+ {
+
+ }
+
+ public Phone(Guid personId, string number)
+ {
+ PersonId = personId;
+ Number = number;
+ }
+
+ public override object[] GetKeys()
+ {
+ return new Object[] {PersonId, Number};
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/DomainEvents/EntityChangeEvents_Tests.cs b/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/DomainEvents/EntityChangeEvents_Tests.cs
index 2b94ee1ed9..9f4b67d089 100644
--- a/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/DomainEvents/EntityChangeEvents_Tests.cs
+++ b/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/DomainEvents/EntityChangeEvents_Tests.cs
@@ -4,6 +4,6 @@ namespace Volo.Abp.EntityFrameworkCore.DomainEvents
{
public class EntityChangeEvents_Tests : EntityChangeEvents_Tests
{
-
+
}
}
diff --git a/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Testing/EntityChangeEvents_Tests.cs b/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Testing/EntityChangeEvents_Tests.cs
index fe4e0862f3..cb16a46439 100644
--- a/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Testing/EntityChangeEvents_Tests.cs
+++ b/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Testing/EntityChangeEvents_Tests.cs
@@ -84,5 +84,44 @@ namespace Volo.Abp.TestApp.Testing
createdEventTriggered.ShouldBeTrue();
createdEtoTriggered.ShouldBeTrue();
}
+
+ [Fact]
+ public async Task Multiple_Update_Should_Result_With_Single_Updated_Event_In_The_Same_Uow()
+ {
+ var personId = Guid.NewGuid();
+ await PersonRepository.InsertAsync(new Person(personId, Guid.NewGuid().ToString("D"), 42));
+
+ var updateEventCount = 0;
+ var updatedAge = 0;
+
+ DistributedEventBus.Subscribe>(eto =>
+ {
+ updateEventCount++;
+ updatedAge = eto.Entity.Age;
+ return Task.CompletedTask;
+ });
+
+ using (var uow = GetRequiredService().Begin())
+ {
+ var person = await PersonRepository.GetAsync(personId);
+
+ person.Age = 43;
+ await PersonRepository.UpdateAsync(person, autoSave: true);
+ updateEventCount.ShouldBe(0);
+
+ person.Age = 44;
+ await PersonRepository.UpdateAsync(person, autoSave: true);
+ updateEventCount.ShouldBe(0);
+
+ person.Age = 45;
+ await PersonRepository.UpdateAsync(person, autoSave: true);
+ updateEventCount.ShouldBe(0);
+
+ await uow.CompleteAsync();
+ }
+
+ updateEventCount.ShouldBe(1);
+ updatedAge.ShouldBe(45);
+ }
}
}