Browse Source

Resolved #4433: Unify multiple update events for a single entity in the same unit of work.

pull/4443/head
Halil İbrahim Kalkan 6 years ago
parent
commit
a6208fcca0
  1. 10
      framework/src/Volo.Abp.Core/Volo/Abp/Reflection/TypeHelper.cs
  2. 45
      framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/Entity.cs
  3. 143
      framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/EntityHelper.cs
  4. 133
      framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/Events/EntityChangeEventHelper.cs
  5. 139
      framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Entities/Entity_Tests.cs
  6. 2
      framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/DomainEvents/EntityChangeEvents_Tests.cs
  7. 39
      framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Testing/EntityChangeEvents_Tests.cs

10
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()));
}
}
}

45
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);
}
}
/// <inheritdoc cref="IEntity{TKey}" />
@ -35,44 +38,6 @@ namespace Volo.Abp.Domain.Entities
Id = id;
}
public bool EntityEquals(object obj)
{
if (obj == null || !(obj is Entity<TKey>))
{
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<TKey>)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<IMultiTenant>().TenantId != other.As<IMultiTenant>().TenantId)
{
return false;
}
return Id.Equals(other.Id);
}
public override object[] GetKeys()
{
return new object[] {Id};

143
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<string, PropertyInfo> CachedIdProperties =
new ConcurrentDictionary<string, PropertyInfo>();
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;
}
/// <summary>
/// Tries to find the primary key type of the given entity type.
/// May return null if given type does not implement <see cref="IEntity{TKey}"/>
@ -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());
}
}
}
}

133
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
/// </summary>
public class EntityChangeEventHelper : IEntityChangeEventHelper, ITransientDependency
{
public ILogger<EntityChangeEventHelper> 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<EntityChangeEventHelper>.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<EntityChangeEventEntry>
{
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);
}
}
}
}

139
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<Guid>
{
public Person()
{
}
public Person(Guid id)
: base(id)
{
}
}
public class Car : Entity<int>, IMultiTenant
{
public Guid? TenantId { get; }
public Car()
{
}
public Car(int id, Guid? tenantId = null)
: base(id)
{
TenantId = tenantId;
}
}
public class Product : Entity<string>
{
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};
}
}
}
}

2
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<AbpEntityFrameworkCoreTestModule>
{
}
}

39
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<EntityUpdatedEto<PersonEto>>(eto =>
{
updateEventCount++;
updatedAge = eto.Entity.Age;
return Task.CompletedTask;
});
using (var uow = GetRequiredService<IUnitOfWorkManager>().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);
}
}
}

Loading…
Cancel
Save