From d5c191a4c7c3f00887baf3fddbf09aff227f96f5 Mon Sep 17 00:00:00 2001 From: gdlcf88 Date: Sat, 1 Oct 2022 23:26:38 +0800 Subject: [PATCH 1/8] Introduce `ExternalEntitySynchronizer` Resolve #14196 --- .../Distributed/ExternalEntitySynchronizer.cs | 157 +++++++++++++++ .../Distributed/IHasRemoteModificationTime.cs | 11 ++ .../Volo.Abp.Ddd.Tests.csproj | 3 + .../ExternalEntitySynchronizers/Book.cs | 19 ++ .../BookEntityJsonConverter.cs | 42 ++++ .../BookSynchronizer.cs | 13 ++ .../ExternalEntitySynchronizer_Tests.cs | 181 ++++++++++++++++++ .../RemoteBookEto.cs | 11 ++ 8 files changed, 437 insertions(+) create mode 100644 framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/Events/Distributed/ExternalEntitySynchronizer.cs create mode 100644 framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/Events/Distributed/IHasRemoteModificationTime.cs create mode 100644 framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Entities/Events/Distributed/ExternalEntitySynchronizers/Book.cs create mode 100644 framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Entities/Events/Distributed/ExternalEntitySynchronizers/BookEntityJsonConverter.cs create mode 100644 framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Entities/Events/Distributed/ExternalEntitySynchronizers/BookSynchronizer.cs create mode 100644 framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Entities/Events/Distributed/ExternalEntitySynchronizers/ExternalEntitySynchronizer_Tests.cs create mode 100644 framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Entities/Events/Distributed/ExternalEntitySynchronizers/RemoteBookEto.cs diff --git a/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/Events/Distributed/ExternalEntitySynchronizer.cs b/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/Events/Distributed/ExternalEntitySynchronizer.cs new file mode 100644 index 0000000000..7d87d07cff --- /dev/null +++ b/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/Events/Distributed/ExternalEntitySynchronizer.cs @@ -0,0 +1,157 @@ +using System; +using System.ComponentModel; +using System.Globalization; +using System.Threading.Tasks; +using JetBrains.Annotations; +using Volo.Abp.Auditing; +using Volo.Abp.Domain.Repositories; +using Volo.Abp.EventBus.Distributed; +using Volo.Abp.ObjectMapping; +using Volo.Abp.Uow; + +namespace Volo.Abp.Domain.Entities.Events.Distributed; + +public abstract class ExternalEntitySynchronizer : + ExternalEntitySynchronizer + where TEntity : class, IEntity, IHasRemoteModificationTime + where TExternalEntityEto : EntityEto, IHasModificationTime +{ + private readonly IRepository _repository; + + protected ExternalEntitySynchronizer(IObjectMapper objectMapper, IRepository repository) : + base(objectMapper, repository) + { + _repository = repository; + } + + protected override Task FindLocalEntityAsync(TExternalEntityEto eto) + { + return _repository.FindAsync(GetExternalEntityId(eto)); + } + + protected virtual TKey GetExternalEntityId(TExternalEntityEto eto) + { + var keyType = typeof(TKey); + var keyValue = Check.NotNullOrEmpty(eto.KeysAsString, nameof(eto.KeysAsString)); + + if (keyType == typeof(Guid)) + { + return (TKey)TypeDescriptor.GetConverter(keyType).ConvertFromInvariantString(keyValue); + } + + return (TKey)Convert.ChangeType(keyValue, keyType, CultureInfo.InvariantCulture); + } +} + +public abstract class ExternalEntitySynchronizer : + IDistributedEventHandler>, + IDistributedEventHandler>, + IDistributedEventHandler>, + IUnitOfWorkEnabled + where TEntity : class, IEntity, IHasRemoteModificationTime + where TExternalEntityEto : EntityEto, IHasModificationTime +{ + protected IObjectMapper ObjectMapper { get; } + private readonly IRepository _repository; + + protected virtual bool IgnoreEntityCreatedEvent { get; set; } + protected virtual bool IgnoreEntityUpdatedEvent { get; set; } + protected virtual bool IgnoreEntityDeletedEvent { get; set; } + + public ExternalEntitySynchronizer( + IObjectMapper objectMapper, + IRepository repository) + { + ObjectMapper = objectMapper; + _repository = repository; + } + + public virtual async Task HandleEventAsync(EntityCreatedEto eventData) + { + if (IgnoreEntityCreatedEvent) + { + return; + } + + await CreateOrUpdateEntityAsync(eventData.Entity); + } + + public virtual async Task HandleEventAsync(EntityUpdatedEto eventData) + { + if (IgnoreEntityUpdatedEvent) + { + return; + } + + await CreateOrUpdateEntityAsync(eventData.Entity); + } + + public virtual async Task HandleEventAsync(EntityDeletedEto eventData) + { + if (IgnoreEntityDeletedEvent) + { + return; + } + + await TryDeleteEntityAsync(eventData.Entity); + } + + protected virtual async Task CreateOrUpdateEntityAsync(TExternalEntityEto eto) + { + var localEntity = await FindLocalEntityAsync(eto); + + if (!await IsEtoNewerAsync(eto, localEntity)) + { + return; + } + + if (localEntity == null) + { + localEntity = await MapToEntityAsync(eto); + ObjectHelper.TrySetProperty(localEntity, x => x.RemoteLastModificationTime, () => eto.LastModificationTime); + + await _repository.InsertAsync(localEntity, true); + } + else + { + await MapToEntityAsync(eto, localEntity); + ObjectHelper.TrySetProperty(localEntity, x => x.RemoteLastModificationTime, () => eto.LastModificationTime); + + await _repository.UpdateAsync(localEntity, true); + } + } + + protected virtual Task MapToEntityAsync(TExternalEntityEto eto) + { + return Task.FromResult(ObjectMapper.Map(eto)); + } + + protected virtual Task MapToEntityAsync(TExternalEntityEto eto, TEntity localEntity) + { + ObjectMapper.Map(eto, localEntity); + return Task.CompletedTask; + } + + protected virtual async Task TryDeleteEntityAsync(TExternalEntityEto eto) + { + var localEntity = await FindLocalEntityAsync(eto); + + if (localEntity == null) + { + return; + } + + await _repository.DeleteAsync(localEntity, true); + } + + [ItemCanBeNull] + protected abstract Task FindLocalEntityAsync(TExternalEntityEto eto); + + protected virtual Task IsEtoNewerAsync(TExternalEntityEto eto, [CanBeNull] TEntity localEntity) + { + return Task.FromResult( + localEntity?.RemoteLastModificationTime == null || + eto.LastModificationTime > localEntity.RemoteLastModificationTime + ); + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/Events/Distributed/IHasRemoteModificationTime.cs b/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/Events/Distributed/IHasRemoteModificationTime.cs new file mode 100644 index 0000000000..2f1cb1fc69 --- /dev/null +++ b/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/Events/Distributed/IHasRemoteModificationTime.cs @@ -0,0 +1,11 @@ +using System; + +namespace Volo.Abp.Domain.Entities.Events.Distributed; + +public interface IHasRemoteModificationTime +{ + /// + /// The last modified time for the synchronized remote entity. + /// + DateTime? RemoteLastModificationTime { get; } +} \ No newline at end of file diff --git a/framework/test/Volo.Abp.Ddd.Tests/Volo.Abp.Ddd.Tests.csproj b/framework/test/Volo.Abp.Ddd.Tests/Volo.Abp.Ddd.Tests.csproj index b6a478866f..ea331dcac3 100644 --- a/framework/test/Volo.Abp.Ddd.Tests/Volo.Abp.Ddd.Tests.csproj +++ b/framework/test/Volo.Abp.Ddd.Tests/Volo.Abp.Ddd.Tests.csproj @@ -8,7 +8,10 @@ + + + diff --git a/framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Entities/Events/Distributed/ExternalEntitySynchronizers/Book.cs b/framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Entities/Events/Distributed/ExternalEntitySynchronizers/Book.cs new file mode 100644 index 0000000000..91f4b40d8c --- /dev/null +++ b/framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Entities/Events/Distributed/ExternalEntitySynchronizers/Book.cs @@ -0,0 +1,19 @@ +using System; + +namespace Volo.Abp.Domain.Entities.Events.Distributed.ExternalEntitySynchronizers; + +public class Book : Entity, IHasRemoteModificationTime +{ + public virtual DateTime? RemoteLastModificationTime { get; protected set; } + + public virtual int Sold { get; set; } + + protected Book() + { + } + + public Book(Guid id, int sold) : base(id) + { + Sold = sold; + } +} \ No newline at end of file diff --git a/framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Entities/Events/Distributed/ExternalEntitySynchronizers/BookEntityJsonConverter.cs b/framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Entities/Events/Distributed/ExternalEntitySynchronizers/BookEntityJsonConverter.cs new file mode 100644 index 0000000000..a49e5f7048 --- /dev/null +++ b/framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Entities/Events/Distributed/ExternalEntitySynchronizers/BookEntityJsonConverter.cs @@ -0,0 +1,42 @@ +using System; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Volo.Abp.Domain.Entities.Events.Distributed.ExternalEntitySynchronizers; + +public class BookEntityJsonConverter : JsonConverter +{ + private JsonSerializerOptions _writeJsonSerializerOptions; + + public override Book Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var jsonDocument = JsonDocument.ParseValue(ref reader); + + if (jsonDocument.RootElement.ValueKind != JsonValueKind.Object) + { + throw new JsonException("RootElement's ValueKind is not Object!"); + } + + var entity = (Book)jsonDocument.RootElement.Deserialize(typeToConvert); + + if (entity == null) + { + throw new JsonException("RootElement's ValueKind is not Object!"); + } + + ObjectHelper.TrySetProperty(entity, x => x.RemoteLastModificationTime, () => + { + var property = jsonDocument.RootElement.GetProperty("RemoteLastModificationTime"); + + return property.ValueKind == JsonValueKind.Null ? null : property.GetDateTime(); + }); + + return entity; + } + + public override void Write(Utf8JsonWriter writer, Book value, JsonSerializerOptions options) + { + _writeJsonSerializerOptions ??= JsonSerializerOptionsHelper.Create(options, this); + JsonSerializer.Serialize(writer, value, _writeJsonSerializerOptions); + } +} \ No newline at end of file diff --git a/framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Entities/Events/Distributed/ExternalEntitySynchronizers/BookSynchronizer.cs b/framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Entities/Events/Distributed/ExternalEntitySynchronizers/BookSynchronizer.cs new file mode 100644 index 0000000000..492e7d81f0 --- /dev/null +++ b/framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Entities/Events/Distributed/ExternalEntitySynchronizers/BookSynchronizer.cs @@ -0,0 +1,13 @@ +using System; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Domain.Repositories; +using Volo.Abp.ObjectMapping; + +namespace Volo.Abp.Domain.Entities.Events.Distributed.ExternalEntitySynchronizers; + +public class BookSynchronizer : ExternalEntitySynchronizer, ITransientDependency +{ + public BookSynchronizer(IObjectMapper objectMapper, IRepository repository) : base(objectMapper, repository) + { + } +} \ No newline at end of file diff --git a/framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Entities/Events/Distributed/ExternalEntitySynchronizers/ExternalEntitySynchronizer_Tests.cs b/framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Entities/Events/Distributed/ExternalEntitySynchronizers/ExternalEntitySynchronizer_Tests.cs new file mode 100644 index 0000000000..73b7f2c1c7 --- /dev/null +++ b/framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Entities/Events/Distributed/ExternalEntitySynchronizers/ExternalEntitySynchronizer_Tests.cs @@ -0,0 +1,181 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using AutoMapper; +using Microsoft.Extensions.DependencyInjection; +using Shouldly; +using Volo.Abp.Autofac; +using Volo.Abp.AutoMapper; +using Volo.Abp.Data; +using Volo.Abp.Domain.Repositories; +using Volo.Abp.Domain.Repositories.MemoryDb; +using Volo.Abp.MemoryDb; +using Volo.Abp.Modularity; +using Volo.Abp.Testing; +using Volo.Abp.Uow; +using Xunit; + +namespace Volo.Abp.Domain.Entities.Events.Distributed.ExternalEntitySynchronizers; + +public class ExternalEntitySynchronizer_Tests : AbpIntegratedTest +{ + [Fact] + public async Task Should_Handle_Entity_Created_Event() + { + var bookId = Guid.NewGuid(); + + var uowManager = GetRequiredService(); + using var uow = uowManager.Begin(); + + var bookSynchronizer = GetRequiredService(); + var repository = GetRequiredService>(); + + (await repository.FindAsync(bookId)).ShouldBeNull(); + + var remoteBookEto = new RemoteBookEto { + KeysAsString = bookId.ToString(), LastModificationTime = DateTime.Now, Sold = 1 + }; + + await bookSynchronizer.HandleEventAsync(new EntityCreatedEto(remoteBookEto)); + + var book = await repository.FindAsync(bookId); + book.ShouldNotBeNull(); + book.RemoteLastModificationTime.ShouldBe(remoteBookEto.LastModificationTime); + book.Sold.ShouldBe(1); + } + + [Fact] + public async Task Should_Handle_Entity_Update_Event() + { + var bookId = Guid.NewGuid(); + + var uowManager = GetRequiredService(); + using var uow = uowManager.Begin(); + + var bookSynchronizer = GetRequiredService(); + var repository = GetRequiredService>(); + + (await repository.FindAsync(bookId)).ShouldBeNull(); + + var remoteBookEto = new RemoteBookEto { + KeysAsString = bookId.ToString(), LastModificationTime = DateTime.Now, Sold = 1 + }; + + await bookSynchronizer.HandleEventAsync(new EntityUpdatedEto(remoteBookEto)); + + var book = await repository.FindAsync(bookId); + book.ShouldNotBeNull(); + book.RemoteLastModificationTime.ShouldBe(remoteBookEto.LastModificationTime); + book.Sold.ShouldBe(1); + + remoteBookEto.LastModificationTime = DateTime.Now; + remoteBookEto.Sold = 2; + + await bookSynchronizer.HandleEventAsync(new EntityUpdatedEto(remoteBookEto)); + + book = await repository.FindAsync(bookId); + book.ShouldNotBeNull(); + book.RemoteLastModificationTime.ShouldBe(remoteBookEto.LastModificationTime); + book.Sold.ShouldBe(2); + + // Should skip synchronizing older remote entities. + var originalLastModificationTime = remoteBookEto.LastModificationTime; + remoteBookEto.LastModificationTime = remoteBookEto.LastModificationTime.Value.AddTicks(-1); + remoteBookEto.Sold = 3; + + await bookSynchronizer.HandleEventAsync(new EntityUpdatedEto(remoteBookEto)); + + book = await repository.FindAsync(bookId); + book.ShouldNotBeNull(); + book.RemoteLastModificationTime.ShouldBe(originalLastModificationTime); + book.Sold.ShouldBe(2); + } + + [Fact] + public async Task Should_Handle_Entity_Deleted_Event() + { + var bookId = Guid.NewGuid(); + + var uowManager = GetRequiredService(); + using var uow = uowManager.Begin(); + + var bookSynchronizer = GetRequiredService(); + var repository = GetRequiredService>(); + + await repository.InsertAsync(new Book(bookId, 1), true); + + var book = await repository.FindAsync(bookId); + book.ShouldNotBeNull(); + book.Id.ShouldBe(bookId); + book.RemoteLastModificationTime.ShouldBeNull(); + + var remoteBookEto = new RemoteBookEto { + KeysAsString = bookId.ToString(), LastModificationTime = DateTime.Now, Sold = 1 + }; + + await bookSynchronizer.HandleEventAsync(new EntityDeletedEto(remoteBookEto)); + + (await repository.FindAsync(bookId)).ShouldBeNull(); + + await bookSynchronizer.HandleEventAsync(new EntityDeletedEto(remoteBookEto)); + + (await repository.FindAsync(bookId)).ShouldBeNull(); + } + + protected override void SetAbpApplicationCreationOptions(AbpApplicationCreationOptions options) + { + options.UseAutofac(); + } + + [DependsOn( + typeof(AbpAutofacModule), + typeof(AbpMemoryDbModule), + typeof(AbpDddDomainModule), + typeof(AbpAutoMapperModule) + )] + public class TestModule : AbpModule + { + public override void ConfigureServices(ServiceConfigurationContext context) + { + var connStr = Guid.NewGuid().ToString(); + + Configure(options => + { + options.ConnectionStrings.Default = connStr; + }); + + context.Services.AddMemoryDbContext(options => + { + options.AddDefaultRepositories(includeAllEntities: true); + }); + + Configure(options => + { + options.JsonSerializerOptions.Converters.Add(new BookEntityJsonConverter()); + }); + + context.Services.AddAutoMapperObjectMapper(); + Configure(options => + { + options.AddMaps(validate: true); + }); + } + } + + public class MyMemoryDbContext : MemoryDbContext + { + public override IReadOnlyList GetEntityTypes() + { + return new List { typeof(Book) }; + } + } + + public class MyAutoMapperProfile : Profile + { + public MyAutoMapperProfile() + { + CreateMap(MemberList.None) + .ForMember(x => x.Id, options => options.MapFrom(x => Guid.Parse(x.KeysAsString))); + } + } +} \ No newline at end of file diff --git a/framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Entities/Events/Distributed/ExternalEntitySynchronizers/RemoteBookEto.cs b/framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Entities/Events/Distributed/ExternalEntitySynchronizers/RemoteBookEto.cs new file mode 100644 index 0000000000..4741c8d3de --- /dev/null +++ b/framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Entities/Events/Distributed/ExternalEntitySynchronizers/RemoteBookEto.cs @@ -0,0 +1,11 @@ +using System; +using Volo.Abp.Auditing; + +namespace Volo.Abp.Domain.Entities.Events.Distributed.ExternalEntitySynchronizers; + +public class RemoteBookEto : EntityEto, IHasModificationTime +{ + public DateTime? LastModificationTime { get; set; } + + public int Sold { get; set; } +} \ No newline at end of file From 336ddf51787b77c7151025441722e7f8ffb23170 Mon Sep 17 00:00:00 2001 From: gdlcf88 Date: Wed, 2 Nov 2022 21:46:32 +0800 Subject: [PATCH 2/8] Introduce `IHasEntityVersion` audit property --- .../Volo/Abp/Auditing/IHasEntityVersion.cs | 12 ++++++ .../Volo/Abp/Auditing/AuditPropertySetter.cs | 10 ++++- .../Volo/Abp/Auditing/IAuditPropertySetter.cs | 2 + .../Distributed/ExternalEntitySynchronizer.cs | 33 ++++++++------- .../Distributed/IHasRemoteModificationTime.cs | 11 ----- .../Abp/EntityFrameworkCore/AbpDbContext.cs | 6 +++ .../MemoryDb/MemoryDbRepository.cs | 6 +++ .../Repositories/MongoDB/MongoDbRepository.cs | 6 +++ .../Auditing/AuditPropertySetterTestBase.cs | 3 +- ...AuditPropertySetter_EntityVersion_Tests.cs | 21 ++++++++++ .../ExternalEntitySynchronizers/Book.cs | 10 +++-- .../BookEntityJsonConverter.cs | 42 ------------------- .../BookSynchronizer.cs | 3 +- .../ExternalEntitySynchronizer_Tests.cs | 37 ++++++---------- .../RemoteBookEto.cs | 7 ++-- .../Volo/Abp/TestApp/Domain/Person.cs | 5 ++- .../Abp/TestApp/Testing/Auditing_Tests.cs | 16 +++++++ 17 files changed, 124 insertions(+), 106 deletions(-) create mode 100644 framework/src/Volo.Abp.Auditing.Contracts/Volo/Abp/Auditing/IHasEntityVersion.cs delete mode 100644 framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/Events/Distributed/IHasRemoteModificationTime.cs create mode 100644 framework/test/Volo.Abp.Auditing.Tests/Volo/Abp/Auditing/AuditPropertySetter_EntityVersion_Tests.cs delete mode 100644 framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Entities/Events/Distributed/ExternalEntitySynchronizers/BookEntityJsonConverter.cs diff --git a/framework/src/Volo.Abp.Auditing.Contracts/Volo/Abp/Auditing/IHasEntityVersion.cs b/framework/src/Volo.Abp.Auditing.Contracts/Volo/Abp/Auditing/IHasEntityVersion.cs new file mode 100644 index 0000000000..a9315be9b5 --- /dev/null +++ b/framework/src/Volo.Abp.Auditing.Contracts/Volo/Abp/Auditing/IHasEntityVersion.cs @@ -0,0 +1,12 @@ +namespace Volo.Abp.Auditing; + +/// +/// An entity version property that auto-increments when the entity changes. +/// +public interface IHasEntityVersion +{ + /// + /// An entity version property that auto-increments when the entity changes. + /// + int EntityVersion { get; } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Auditing/Volo/Abp/Auditing/AuditPropertySetter.cs b/framework/src/Volo.Abp.Auditing/Volo/Abp/Auditing/AuditPropertySetter.cs index 16632420d7..335916f068 100644 --- a/framework/src/Volo.Abp.Auditing/Volo/Abp/Auditing/AuditPropertySetter.cs +++ b/framework/src/Volo.Abp.Auditing/Volo/Abp/Auditing/AuditPropertySetter.cs @@ -39,6 +39,14 @@ public class AuditPropertySetter : IAuditPropertySetter, ITransientDependency SetDeleterId(targetObject); } + public virtual void IncrementEntityVersionProperty(object targetObject) + { + if (targetObject is IHasEntityVersion objectWithEntityVersion) + { + ObjectHelper.TrySetProperty(objectWithEntityVersion, x => x.EntityVersion, x => x.EntityVersion + 1); + } + } + protected virtual void SetCreationTime(object targetObject) { if (!(targetObject is IHasCreationTime objectWithCreationTime)) @@ -177,4 +185,4 @@ public class AuditPropertySetter : IAuditPropertySetter, ITransientDependency ObjectHelper.TrySetProperty(deletionAuditedObject, x => x.DeleterId, () => CurrentUser.Id); } -} +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Auditing/Volo/Abp/Auditing/IAuditPropertySetter.cs b/framework/src/Volo.Abp.Auditing/Volo/Abp/Auditing/IAuditPropertySetter.cs index 80d1b89fe5..9e5cd0120d 100644 --- a/framework/src/Volo.Abp.Auditing/Volo/Abp/Auditing/IAuditPropertySetter.cs +++ b/framework/src/Volo.Abp.Auditing/Volo/Abp/Auditing/IAuditPropertySetter.cs @@ -7,4 +7,6 @@ public interface IAuditPropertySetter void SetModificationProperties(object targetObject); void SetDeletionProperties(object targetObject); + + void IncrementEntityVersionProperty(object targetObject); } diff --git a/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/Events/Distributed/ExternalEntitySynchronizer.cs b/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/Events/Distributed/ExternalEntitySynchronizer.cs index 7d87d07cff..df8806d9d7 100644 --- a/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/Events/Distributed/ExternalEntitySynchronizer.cs +++ b/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/Events/Distributed/ExternalEntitySynchronizer.cs @@ -13,8 +13,8 @@ namespace Volo.Abp.Domain.Entities.Events.Distributed; public abstract class ExternalEntitySynchronizer : ExternalEntitySynchronizer - where TEntity : class, IEntity, IHasRemoteModificationTime - where TExternalEntityEto : EntityEto, IHasModificationTime + where TEntity : class, IEntity, IHasEntityVersion + where TExternalEntityEto : EntityEto, IHasEntityVersion { private readonly IRepository _repository; @@ -48,8 +48,8 @@ public abstract class ExternalEntitySynchronizer : IDistributedEventHandler>, IDistributedEventHandler>, IUnitOfWorkEnabled - where TEntity : class, IEntity, IHasRemoteModificationTime - where TExternalEntityEto : EntityEto, IHasModificationTime + where TEntity : class, IEntity, IHasEntityVersion + where TExternalEntityEto : EntityEto, IHasEntityVersion { protected IObjectMapper ObjectMapper { get; } private readonly IRepository _repository; @@ -73,7 +73,7 @@ public abstract class ExternalEntitySynchronizer : return; } - await CreateOrUpdateEntityAsync(eventData.Entity); + await TryCreateOrUpdateEntityAsync(eventData.Entity); } public virtual async Task HandleEventAsync(EntityUpdatedEto eventData) @@ -83,7 +83,7 @@ public abstract class ExternalEntitySynchronizer : return; } - await CreateOrUpdateEntityAsync(eventData.Entity); + await TryCreateOrUpdateEntityAsync(eventData.Entity); } public virtual async Task HandleEventAsync(EntityDeletedEto eventData) @@ -96,29 +96,31 @@ public abstract class ExternalEntitySynchronizer : await TryDeleteEntityAsync(eventData.Entity); } - protected virtual async Task CreateOrUpdateEntityAsync(TExternalEntityEto eto) + protected virtual async Task TryCreateOrUpdateEntityAsync(TExternalEntityEto eto) { var localEntity = await FindLocalEntityAsync(eto); if (!await IsEtoNewerAsync(eto, localEntity)) { - return; + return false; } if (localEntity == null) { localEntity = await MapToEntityAsync(eto); - ObjectHelper.TrySetProperty(localEntity, x => x.RemoteLastModificationTime, () => eto.LastModificationTime); + ObjectHelper.TrySetProperty(localEntity, x => x.EntityVersion, () => eto.EntityVersion); await _repository.InsertAsync(localEntity, true); } else { await MapToEntityAsync(eto, localEntity); - ObjectHelper.TrySetProperty(localEntity, x => x.RemoteLastModificationTime, () => eto.LastModificationTime); + ObjectHelper.TrySetProperty(localEntity, x => x.EntityVersion, () => eto.EntityVersion); await _repository.UpdateAsync(localEntity, true); } + + return true; } protected virtual Task MapToEntityAsync(TExternalEntityEto eto) @@ -132,16 +134,18 @@ public abstract class ExternalEntitySynchronizer : return Task.CompletedTask; } - protected virtual async Task TryDeleteEntityAsync(TExternalEntityEto eto) + protected virtual async Task TryDeleteEntityAsync(TExternalEntityEto eto) { var localEntity = await FindLocalEntityAsync(eto); if (localEntity == null) { - return; + return false; } await _repository.DeleteAsync(localEntity, true); + + return true; } [ItemCanBeNull] @@ -149,9 +153,6 @@ public abstract class ExternalEntitySynchronizer : protected virtual Task IsEtoNewerAsync(TExternalEntityEto eto, [CanBeNull] TEntity localEntity) { - return Task.FromResult( - localEntity?.RemoteLastModificationTime == null || - eto.LastModificationTime > localEntity.RemoteLastModificationTime - ); + return Task.FromResult(localEntity == null || eto.EntityVersion > localEntity.EntityVersion); } } \ No newline at end of file diff --git a/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/Events/Distributed/IHasRemoteModificationTime.cs b/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/Events/Distributed/IHasRemoteModificationTime.cs deleted file mode 100644 index 2f1cb1fc69..0000000000 --- a/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/Events/Distributed/IHasRemoteModificationTime.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System; - -namespace Volo.Abp.Domain.Entities.Events.Distributed; - -public interface IHasRemoteModificationTime -{ - /// - /// The last modified time for the synchronized remote entity. - /// - DateTime? RemoteLastModificationTime { get; } -} \ No newline at end of file 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 fd998510af..103dcd3c2e 100644 --- a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/AbpDbContext.cs +++ b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/AbpDbContext.cs @@ -459,6 +459,7 @@ public abstract class AbpDbContext : DbContext, IAbpEfCoreDbContext, { if (entry.State == EntityState.Modified && entry.Properties.Any(x => x.IsModified && x.Metadata.ValueGenerated == ValueGenerated.Never)) { + IncrementEntityVersionProperty(entry); SetModificationAuditProperties(entry); if (entry.Entity is ISoftDelete && entry.Entity.As().IsDeleted) @@ -574,6 +575,11 @@ public abstract class AbpDbContext : DbContext, IAbpEfCoreDbContext, AuditPropertySetter?.SetDeletionProperties(entry.Entity); } + protected virtual void IncrementEntityVersionProperty(EntityEntry entry) + { + AuditPropertySetter?.IncrementEntityVersionProperty(entry.Entity); + } + protected virtual void ConfigureBaseProperties(ModelBuilder modelBuilder, IMutableEntityType mutableEntityType) where TEntity : class { diff --git a/framework/src/Volo.Abp.MemoryDb/Volo/Abp/Domain/Repositories/MemoryDb/MemoryDbRepository.cs b/framework/src/Volo.Abp.MemoryDb/Volo/Abp/Domain/Repositories/MemoryDb/MemoryDbRepository.cs index 61bb9745e2..7ad7f95392 100644 --- a/framework/src/Volo.Abp.MemoryDb/Volo/Abp/Domain/Repositories/MemoryDb/MemoryDbRepository.cs +++ b/framework/src/Volo.Abp.MemoryDb/Volo/Abp/Domain/Repositories/MemoryDb/MemoryDbRepository.cs @@ -157,6 +157,11 @@ public class MemoryDbRepository : RepositoryBase : RepositoryBase { cancellationToken = GetCancellationToken(cancellationToken); + IncrementEntityVersionProperty(entity); SetModificationAuditProperties(entity); if (entity is ISoftDelete softDeleteEntity && softDeleteEntity.IsDeleted) @@ -654,6 +655,11 @@ public class MongoDbRepository AuditPropertySetter.SetDeletionProperties(entity); } + protected virtual void IncrementEntityVersionProperty(TEntity entity) + { + AuditPropertySetter.IncrementEntityVersionProperty(entity); + } + protected virtual void TriggerDomainEvents(object entity) { var generatesDomainEventsEntity = entity as IGeneratesDomainEvents; diff --git a/framework/test/Volo.Abp.Auditing.Tests/Volo/Abp/Auditing/AuditPropertySetterTestBase.cs b/framework/test/Volo.Abp.Auditing.Tests/Volo/Abp/Auditing/AuditPropertySetterTestBase.cs index 184bbeaa34..f9c860ca6d 100644 --- a/framework/test/Volo.Abp.Auditing.Tests/Volo/Abp/Auditing/AuditPropertySetterTestBase.cs +++ b/framework/test/Volo.Abp.Auditing.Tests/Volo/Abp/Auditing/AuditPropertySetterTestBase.cs @@ -48,7 +48,7 @@ public class AuditPropertySetterTestBase } - public class MyAuditedObject : IMultiTenant, IFullAuditedObject + public class MyAuditedObject : IMultiTenant, IFullAuditedObject, IHasEntityVersion { public Guid? TenantId { get; set; } public DateTime CreationTime { get; set; } @@ -58,5 +58,6 @@ public class AuditPropertySetterTestBase public bool IsDeleted { get; set; } public DateTime? DeletionTime { get; set; } public Guid? DeleterId { get; set; } + public int EntityVersion { get; set; } } } diff --git a/framework/test/Volo.Abp.Auditing.Tests/Volo/Abp/Auditing/AuditPropertySetter_EntityVersion_Tests.cs b/framework/test/Volo.Abp.Auditing.Tests/Volo/Abp/Auditing/AuditPropertySetter_EntityVersion_Tests.cs new file mode 100644 index 0000000000..51985ea5f9 --- /dev/null +++ b/framework/test/Volo.Abp.Auditing.Tests/Volo/Abp/Auditing/AuditPropertySetter_EntityVersion_Tests.cs @@ -0,0 +1,21 @@ +using Shouldly; +using Xunit; + +namespace Volo.Abp.Auditing; + +public class AuditPropertySetter_EntityVersion_Tests : AuditPropertySetterTestBase +{ + [Fact] + public void Should_Do_Nothing_For_Non_Audited_Entity() + { + AuditPropertySetter.IncrementEntityVersionProperty(new MyEmptyObject()); + } + + [Fact] + public void Should_Increment_EntityVersion() + { + AuditPropertySetter.IncrementEntityVersionProperty(TargetObject); + + TargetObject.EntityVersion.ShouldBe(1); + } +} diff --git a/framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Entities/Events/Distributed/ExternalEntitySynchronizers/Book.cs b/framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Entities/Events/Distributed/ExternalEntitySynchronizers/Book.cs index 91f4b40d8c..d263a03fe9 100644 --- a/framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Entities/Events/Distributed/ExternalEntitySynchronizers/Book.cs +++ b/framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Entities/Events/Distributed/ExternalEntitySynchronizers/Book.cs @@ -1,19 +1,21 @@ using System; +using Volo.Abp.Auditing; namespace Volo.Abp.Domain.Entities.Events.Distributed.ExternalEntitySynchronizers; -public class Book : Entity, IHasRemoteModificationTime +public class Book : Entity, IHasEntityVersion { - public virtual DateTime? RemoteLastModificationTime { get; protected set; } - public virtual int Sold { get; set; } + public virtual int EntityVersion { get; protected set; } + protected Book() { } - public Book(Guid id, int sold) : base(id) + public Book(Guid id, int sold, int entityVersion) : base(id) { Sold = sold; + EntityVersion = entityVersion; } } \ No newline at end of file diff --git a/framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Entities/Events/Distributed/ExternalEntitySynchronizers/BookEntityJsonConverter.cs b/framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Entities/Events/Distributed/ExternalEntitySynchronizers/BookEntityJsonConverter.cs deleted file mode 100644 index a49e5f7048..0000000000 --- a/framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Entities/Events/Distributed/ExternalEntitySynchronizers/BookEntityJsonConverter.cs +++ /dev/null @@ -1,42 +0,0 @@ -using System; -using System.Text.Json; -using System.Text.Json.Serialization; - -namespace Volo.Abp.Domain.Entities.Events.Distributed.ExternalEntitySynchronizers; - -public class BookEntityJsonConverter : JsonConverter -{ - private JsonSerializerOptions _writeJsonSerializerOptions; - - public override Book Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - var jsonDocument = JsonDocument.ParseValue(ref reader); - - if (jsonDocument.RootElement.ValueKind != JsonValueKind.Object) - { - throw new JsonException("RootElement's ValueKind is not Object!"); - } - - var entity = (Book)jsonDocument.RootElement.Deserialize(typeToConvert); - - if (entity == null) - { - throw new JsonException("RootElement's ValueKind is not Object!"); - } - - ObjectHelper.TrySetProperty(entity, x => x.RemoteLastModificationTime, () => - { - var property = jsonDocument.RootElement.GetProperty("RemoteLastModificationTime"); - - return property.ValueKind == JsonValueKind.Null ? null : property.GetDateTime(); - }); - - return entity; - } - - public override void Write(Utf8JsonWriter writer, Book value, JsonSerializerOptions options) - { - _writeJsonSerializerOptions ??= JsonSerializerOptionsHelper.Create(options, this); - JsonSerializer.Serialize(writer, value, _writeJsonSerializerOptions); - } -} \ No newline at end of file diff --git a/framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Entities/Events/Distributed/ExternalEntitySynchronizers/BookSynchronizer.cs b/framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Entities/Events/Distributed/ExternalEntitySynchronizers/BookSynchronizer.cs index 492e7d81f0..8470343b5d 100644 --- a/framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Entities/Events/Distributed/ExternalEntitySynchronizers/BookSynchronizer.cs +++ b/framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Entities/Events/Distributed/ExternalEntitySynchronizers/BookSynchronizer.cs @@ -7,7 +7,8 @@ namespace Volo.Abp.Domain.Entities.Events.Distributed.ExternalEntitySynchronizer public class BookSynchronizer : ExternalEntitySynchronizer, ITransientDependency { - public BookSynchronizer(IObjectMapper objectMapper, IRepository repository) : base(objectMapper, repository) + public BookSynchronizer(IObjectMapper objectMapper, IRepository repository) + : base(objectMapper, repository) { } } \ No newline at end of file diff --git a/framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Entities/Events/Distributed/ExternalEntitySynchronizers/ExternalEntitySynchronizer_Tests.cs b/framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Entities/Events/Distributed/ExternalEntitySynchronizers/ExternalEntitySynchronizer_Tests.cs index 73b7f2c1c7..e0b86c82ea 100644 --- a/framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Entities/Events/Distributed/ExternalEntitySynchronizers/ExternalEntitySynchronizer_Tests.cs +++ b/framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Entities/Events/Distributed/ExternalEntitySynchronizers/ExternalEntitySynchronizer_Tests.cs @@ -8,7 +8,6 @@ using Volo.Abp.Autofac; using Volo.Abp.AutoMapper; using Volo.Abp.Data; using Volo.Abp.Domain.Repositories; -using Volo.Abp.Domain.Repositories.MemoryDb; using Volo.Abp.MemoryDb; using Volo.Abp.Modularity; using Volo.Abp.Testing; @@ -32,15 +31,13 @@ public class ExternalEntitySynchronizer_Tests : AbpIntegratedTest(remoteBookEto)); var book = await repository.FindAsync(bookId); book.ShouldNotBeNull(); - book.RemoteLastModificationTime.ShouldBe(remoteBookEto.LastModificationTime); + book.EntityVersion.ShouldBe(remoteBookEto.EntityVersion); book.Sold.ShouldBe(1); } @@ -57,37 +54,34 @@ public class ExternalEntitySynchronizer_Tests : AbpIntegratedTest(remoteBookEto)); var book = await repository.FindAsync(bookId); book.ShouldNotBeNull(); - book.RemoteLastModificationTime.ShouldBe(remoteBookEto.LastModificationTime); + book.EntityVersion.ShouldBe(remoteBookEto.EntityVersion); book.Sold.ShouldBe(1); - remoteBookEto.LastModificationTime = DateTime.Now; + remoteBookEto.EntityVersion = 1; remoteBookEto.Sold = 2; await bookSynchronizer.HandleEventAsync(new EntityUpdatedEto(remoteBookEto)); book = await repository.FindAsync(bookId); book.ShouldNotBeNull(); - book.RemoteLastModificationTime.ShouldBe(remoteBookEto.LastModificationTime); + book.EntityVersion.ShouldBe(remoteBookEto.EntityVersion); book.Sold.ShouldBe(2); - // Should skip synchronizing older remote entities. - var originalLastModificationTime = remoteBookEto.LastModificationTime; - remoteBookEto.LastModificationTime = remoteBookEto.LastModificationTime.Value.AddTicks(-1); + remoteBookEto.EntityVersion = 0; remoteBookEto.Sold = 3; await bookSynchronizer.HandleEventAsync(new EntityUpdatedEto(remoteBookEto)); + // Should skip synchronizing older remote entities. book = await repository.FindAsync(bookId); book.ShouldNotBeNull(); - book.RemoteLastModificationTime.ShouldBe(originalLastModificationTime); + book.EntityVersion.ShouldBe(1); book.Sold.ShouldBe(2); } @@ -102,16 +96,14 @@ public class ExternalEntitySynchronizer_Tests : AbpIntegratedTest(); var repository = GetRequiredService>(); - await repository.InsertAsync(new Book(bookId, 1), true); + await repository.InsertAsync(new Book(bookId, 1, 0), true); var book = await repository.FindAsync(bookId); book.ShouldNotBeNull(); book.Id.ShouldBe(bookId); - book.RemoteLastModificationTime.ShouldBeNull(); + book.EntityVersion.ShouldBe(0); - var remoteBookEto = new RemoteBookEto { - KeysAsString = bookId.ToString(), LastModificationTime = DateTime.Now, Sold = 1 - }; + var remoteBookEto = new RemoteBookEto { KeysAsString = bookId.ToString(), EntityVersion = 0, Sold = 1 }; await bookSynchronizer.HandleEventAsync(new EntityDeletedEto(remoteBookEto)); @@ -149,11 +141,6 @@ public class ExternalEntitySynchronizer_Tests : AbpIntegratedTest(options => - { - options.JsonSerializerOptions.Converters.Add(new BookEntityJsonConverter()); - }); - context.Services.AddAutoMapperObjectMapper(); Configure(options => { diff --git a/framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Entities/Events/Distributed/ExternalEntitySynchronizers/RemoteBookEto.cs b/framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Entities/Events/Distributed/ExternalEntitySynchronizers/RemoteBookEto.cs index 4741c8d3de..886bca36bc 100644 --- a/framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Entities/Events/Distributed/ExternalEntitySynchronizers/RemoteBookEto.cs +++ b/framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Entities/Events/Distributed/ExternalEntitySynchronizers/RemoteBookEto.cs @@ -1,11 +1,10 @@ -using System; -using Volo.Abp.Auditing; +using Volo.Abp.Auditing; namespace Volo.Abp.Domain.Entities.Events.Distributed.ExternalEntitySynchronizers; -public class RemoteBookEto : EntityEto, IHasModificationTime +public class RemoteBookEto : EntityEto, IHasEntityVersion { - public DateTime? LastModificationTime { get; set; } + public int EntityVersion { get; set; } public int Sold { get; set; } } \ No newline at end of file diff --git a/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Domain/Person.cs b/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Domain/Person.cs index f3bd13f1b4..41d48780ae 100644 --- a/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Domain/Person.cs +++ b/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Domain/Person.cs @@ -1,12 +1,13 @@ using System; using System.Collections.ObjectModel; +using Volo.Abp.Auditing; using Volo.Abp.Domain.Entities.Auditing; using Volo.Abp.MultiTenancy; using Volo.Abp.Timing; namespace Volo.Abp.TestApp.Domain; -public class Person : FullAuditedAggregateRoot, IMultiTenant +public class Person : FullAuditedAggregateRoot, IMultiTenant, IHasEntityVersion { public virtual Guid? TenantId { get; set; } @@ -25,6 +26,8 @@ public class Person : FullAuditedAggregateRoot, IMultiTenant public virtual DateTime LastActiveTime { get; set; } + public int EntityVersion { get; set; } + private Person() { } diff --git a/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Testing/Auditing_Tests.cs b/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Testing/Auditing_Tests.cs index 39cbcd63b2..5653987024 100644 --- a/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Testing/Auditing_Tests.cs +++ b/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Testing/Auditing_Tests.cs @@ -111,4 +111,20 @@ public abstract class Auditing_Tests : TestAppTestBase Date: Thu, 3 Nov 2022 12:30:47 +0800 Subject: [PATCH 3/8] Fix a bug of entity version updating in `ExternalEntitySynchronizer` --- .../Events/Distributed/ExternalEntitySynchronizer.cs | 5 ++++- .../ExternalEntitySynchronizer_Tests.cs | 4 ++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/Events/Distributed/ExternalEntitySynchronizer.cs b/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/Events/Distributed/ExternalEntitySynchronizer.cs index df8806d9d7..27260bcee7 100644 --- a/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/Events/Distributed/ExternalEntitySynchronizer.cs +++ b/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/Events/Distributed/ExternalEntitySynchronizer.cs @@ -114,8 +114,11 @@ public abstract class ExternalEntitySynchronizer : } else { + // The version will auto-increment by one when the repository updates the entity. + var entityVersion = eto.EntityVersion - 1; + await MapToEntityAsync(eto, localEntity); - ObjectHelper.TrySetProperty(localEntity, x => x.EntityVersion, () => eto.EntityVersion); + ObjectHelper.TrySetProperty(localEntity, x => x.EntityVersion, () => entityVersion); await _repository.UpdateAsync(localEntity, true); } diff --git a/framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Entities/Events/Distributed/ExternalEntitySynchronizers/ExternalEntitySynchronizer_Tests.cs b/framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Entities/Events/Distributed/ExternalEntitySynchronizers/ExternalEntitySynchronizer_Tests.cs index e0b86c82ea..c32ea0e513 100644 --- a/framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Entities/Events/Distributed/ExternalEntitySynchronizers/ExternalEntitySynchronizer_Tests.cs +++ b/framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Entities/Events/Distributed/ExternalEntitySynchronizers/ExternalEntitySynchronizer_Tests.cs @@ -60,7 +60,7 @@ public class ExternalEntitySynchronizer_Tests : AbpIntegratedTest Date: Thu, 8 Dec 2022 18:26:24 +0800 Subject: [PATCH 4/8] Make `ExternalEntitySynchronizer` support non-versioned entities --- .../Distributed/ExternalEntitySynchronizer.cs | 34 ++++-- .../ExternalEntitySynchronizer_Tests.cs | 106 ++++++++++++++++-- .../{ => WithEntityVersion}/Book.cs | 7 +- .../BookSynchronizer.cs | 2 +- .../{ => WithEntityVersion}/RemoteBookEto.cs | 2 +- .../WithoutEntityVersion/Author.cs | 17 +++ .../AuthorSynchronizer.cs | 14 +++ .../WithoutEntityVersion/RemoteAuthorEto.cs | 6 + 8 files changed, 163 insertions(+), 25 deletions(-) rename framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Entities/Events/Distributed/ExternalEntitySynchronizers/{ => WithEntityVersion}/Book.cs (63%) rename framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Entities/Events/Distributed/ExternalEntitySynchronizers/{ => WithEntityVersion}/BookSynchronizer.cs (92%) rename framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Entities/Events/Distributed/ExternalEntitySynchronizers/{ => WithEntityVersion}/RemoteBookEto.cs (86%) create mode 100644 framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Entities/Events/Distributed/ExternalEntitySynchronizers/WithoutEntityVersion/Author.cs create mode 100644 framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Entities/Events/Distributed/ExternalEntitySynchronizers/WithoutEntityVersion/AuthorSynchronizer.cs create mode 100644 framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Entities/Events/Distributed/ExternalEntitySynchronizers/WithoutEntityVersion/RemoteAuthorEto.cs diff --git a/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/Events/Distributed/ExternalEntitySynchronizer.cs b/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/Events/Distributed/ExternalEntitySynchronizer.cs index 27260bcee7..58582912de 100644 --- a/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/Events/Distributed/ExternalEntitySynchronizer.cs +++ b/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/Events/Distributed/ExternalEntitySynchronizer.cs @@ -13,8 +13,8 @@ namespace Volo.Abp.Domain.Entities.Events.Distributed; public abstract class ExternalEntitySynchronizer : ExternalEntitySynchronizer - where TEntity : class, IEntity, IHasEntityVersion - where TExternalEntityEto : EntityEto, IHasEntityVersion + where TEntity : class, IEntity + where TExternalEntityEto : EntityEto { private readonly IRepository _repository; @@ -48,8 +48,8 @@ public abstract class ExternalEntitySynchronizer : IDistributedEventHandler>, IDistributedEventHandler>, IUnitOfWorkEnabled - where TEntity : class, IEntity, IHasEntityVersion - where TExternalEntityEto : EntityEto, IHasEntityVersion + where TEntity : class, IEntity + where TExternalEntityEto : EntityEto { protected IObjectMapper ObjectMapper { get; } private readonly IRepository _repository; @@ -108,17 +108,26 @@ public abstract class ExternalEntitySynchronizer : if (localEntity == null) { localEntity = await MapToEntityAsync(eto); - ObjectHelper.TrySetProperty(localEntity, x => x.EntityVersion, () => eto.EntityVersion); + + if (localEntity is IHasEntityVersion versionedLocalEntity && eto is IHasEntityVersion versionedEto) + { + ObjectHelper.TrySetProperty(versionedLocalEntity, x => x.EntityVersion, + () => versionedEto.EntityVersion); + } await _repository.InsertAsync(localEntity, true); } else { - // The version will auto-increment by one when the repository updates the entity. - var entityVersion = eto.EntityVersion - 1; - await MapToEntityAsync(eto, localEntity); - ObjectHelper.TrySetProperty(localEntity, x => x.EntityVersion, () => entityVersion); + + if (localEntity is IHasEntityVersion versionedLocalEntity && eto is IHasEntityVersion versionedEto) + { + // The version will auto-increment by one when the repository updates the entity. + var entityVersion = versionedEto.EntityVersion - 1; + + ObjectHelper.TrySetProperty(versionedLocalEntity, x => x.EntityVersion, () => entityVersion); + } await _repository.UpdateAsync(localEntity, true); } @@ -156,6 +165,11 @@ public abstract class ExternalEntitySynchronizer : protected virtual Task IsEtoNewerAsync(TExternalEntityEto eto, [CanBeNull] TEntity localEntity) { - return Task.FromResult(localEntity == null || eto.EntityVersion > localEntity.EntityVersion); + if (localEntity is IHasEntityVersion versionedLocalEntity && eto is IHasEntityVersion versionedEto) + { + return Task.FromResult(versionedEto.EntityVersion > versionedLocalEntity.EntityVersion); + } + + return Task.FromResult(true); } } \ No newline at end of file diff --git a/framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Entities/Events/Distributed/ExternalEntitySynchronizers/ExternalEntitySynchronizer_Tests.cs b/framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Entities/Events/Distributed/ExternalEntitySynchronizers/ExternalEntitySynchronizer_Tests.cs index c32ea0e513..65ee9bcf9f 100644 --- a/framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Entities/Events/Distributed/ExternalEntitySynchronizers/ExternalEntitySynchronizer_Tests.cs +++ b/framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Entities/Events/Distributed/ExternalEntitySynchronizers/ExternalEntitySynchronizer_Tests.cs @@ -7,6 +7,8 @@ using Shouldly; using Volo.Abp.Autofac; using Volo.Abp.AutoMapper; using Volo.Abp.Data; +using Volo.Abp.Domain.Entities.Events.Distributed.ExternalEntitySynchronizers.WithEntityVersion; +using Volo.Abp.Domain.Entities.Events.Distributed.ExternalEntitySynchronizers.WithoutEntityVersion; using Volo.Abp.Domain.Repositories; using Volo.Abp.MemoryDb; using Volo.Abp.Modularity; @@ -20,6 +22,84 @@ public class ExternalEntitySynchronizer_Tests : AbpIntegratedTest(); + using var uow = uowManager.Begin(); + + var authorSynchronizer = GetRequiredService(); + var repository = GetRequiredService>(); + + (await repository.FindAsync(authorId)).ShouldBeNull(); + + var remoteAuthorEto = new RemoteAuthorEto { KeysAsString = authorId.ToString(), Name = "New" }; + + await authorSynchronizer.HandleEventAsync(new EntityCreatedEto(remoteAuthorEto)); + + var author = await repository.FindAsync(authorId); + author.ShouldNotBeNull(); + author.Name.ShouldBe("New"); + } + + [Fact] + public async Task Should_Handle_Entity_Update_Event() + { + var authorId = Guid.NewGuid(); + + var uowManager = GetRequiredService(); + using var uow = uowManager.Begin(); + + var authorSynchronizer = GetRequiredService(); + var repository = GetRequiredService>(); + + await repository.InsertAsync(new Author(authorId, "Old"), true); + + var author = await repository.FindAsync(authorId); + author.ShouldNotBeNull(); + author.Id.ShouldBe(authorId); + author.Name.ShouldBe("Old"); + + var remoteAuthorEto = new RemoteAuthorEto { KeysAsString = authorId.ToString(), Name = "New" }; + + await authorSynchronizer.HandleEventAsync(new EntityUpdatedEto(remoteAuthorEto)); + + author = await repository.FindAsync(authorId); + author.ShouldNotBeNull(); + author.Id.ShouldBe(authorId); + author.Name.ShouldBe("New"); + } + + [Fact] + public async Task Should_Handle_Entity_Deleted_Event() + { + var authorId = Guid.NewGuid(); + + var uowManager = GetRequiredService(); + using var uow = uowManager.Begin(); + + var authorSynchronizer = GetRequiredService(); + var repository = GetRequiredService>(); + + await repository.InsertAsync(new Author(authorId, "Old"), true); + + var author = await repository.FindAsync(authorId); + author.ShouldNotBeNull(); + author.Id.ShouldBe(authorId); + author.Name.ShouldBe("Old"); + + var remoteAuthorEto = new RemoteAuthorEto { KeysAsString = authorId.ToString(), Name = "New" }; + + await authorSynchronizer.HandleEventAsync(new EntityDeletedEto(remoteAuthorEto)); + + (await repository.FindAsync(authorId)).ShouldBeNull(); + + await Should.NotThrowAsync(() => + authorSynchronizer.HandleEventAsync(new EntityDeletedEto(remoteAuthorEto))); + } + + [Fact] + public async Task Should_Handle_Versioned_Entity_Created_Event() { var bookId = Guid.NewGuid(); @@ -42,7 +122,7 @@ public class ExternalEntitySynchronizer_Tests : AbpIntegratedTest(); var repository = GetRequiredService>(); - (await repository.FindAsync(bookId)).ShouldBeNull(); + await repository.InsertAsync(new Book(bookId, 1), true); - var remoteBookEto = new RemoteBookEto { KeysAsString = bookId.ToString(), EntityVersion = 0, Sold = 1 }; + var book = await repository.FindAsync(bookId); + book.ShouldNotBeNull(); + book.Id.ShouldBe(bookId); + book.EntityVersion.ShouldBe(0); + + var remoteBookEto = new RemoteBookEto { KeysAsString = bookId.ToString(), EntityVersion = 0, Sold = 10 }; await bookSynchronizer.HandleEventAsync(new EntityUpdatedEto(remoteBookEto)); - var book = await repository.FindAsync(bookId); + book = await repository.FindAsync(bookId); book.ShouldNotBeNull(); book.EntityVersion.ShouldBe(0); book.Sold.ShouldBe(1); @@ -86,7 +171,7 @@ public class ExternalEntitySynchronizer_Tests : AbpIntegratedTest(); var repository = GetRequiredService>(); - await repository.InsertAsync(new Book(bookId, 1, 0), true); + await repository.InsertAsync(new Book(bookId, 1), true); var book = await repository.FindAsync(bookId); book.ShouldNotBeNull(); @@ -109,9 +194,8 @@ public class ExternalEntitySynchronizer_Tests : AbpIntegratedTest(remoteBookEto)); - - (await repository.FindAsync(bookId)).ShouldBeNull(); + await Should.NotThrowAsync(() => + bookSynchronizer.HandleEventAsync(new EntityDeletedEto(remoteBookEto))); } protected override void SetAbpApplicationCreationOptions(AbpApplicationCreationOptions options) @@ -153,7 +237,7 @@ public class ExternalEntitySynchronizer_Tests : AbpIntegratedTest GetEntityTypes() { - return new List { typeof(Book) }; + return new List { typeof(Book), typeof(Author) }; } } @@ -163,6 +247,8 @@ public class ExternalEntitySynchronizer_Tests : AbpIntegratedTest(MemberList.None) .ForMember(x => x.Id, options => options.MapFrom(x => Guid.Parse(x.KeysAsString))); + CreateMap(MemberList.None) + .ForMember(x => x.Id, options => options.MapFrom(x => Guid.Parse(x.KeysAsString))); } } } \ No newline at end of file diff --git a/framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Entities/Events/Distributed/ExternalEntitySynchronizers/Book.cs b/framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Entities/Events/Distributed/ExternalEntitySynchronizers/WithEntityVersion/Book.cs similarity index 63% rename from framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Entities/Events/Distributed/ExternalEntitySynchronizers/Book.cs rename to framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Entities/Events/Distributed/ExternalEntitySynchronizers/WithEntityVersion/Book.cs index d263a03fe9..fd2facfe19 100644 --- a/framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Entities/Events/Distributed/ExternalEntitySynchronizers/Book.cs +++ b/framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Entities/Events/Distributed/ExternalEntitySynchronizers/WithEntityVersion/Book.cs @@ -1,21 +1,22 @@ using System; +using System.Text.Json.Serialization; using Volo.Abp.Auditing; -namespace Volo.Abp.Domain.Entities.Events.Distributed.ExternalEntitySynchronizers; +namespace Volo.Abp.Domain.Entities.Events.Distributed.ExternalEntitySynchronizers.WithEntityVersion; public class Book : Entity, IHasEntityVersion { public virtual int Sold { get; set; } + [JsonInclude] // the memory DB repository requires this or a ctor arg public virtual int EntityVersion { get; protected set; } protected Book() { } - public Book(Guid id, int sold, int entityVersion) : base(id) + public Book(Guid id, int sold) : base(id) { Sold = sold; - EntityVersion = entityVersion; } } \ No newline at end of file diff --git a/framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Entities/Events/Distributed/ExternalEntitySynchronizers/BookSynchronizer.cs b/framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Entities/Events/Distributed/ExternalEntitySynchronizers/WithEntityVersion/BookSynchronizer.cs similarity index 92% rename from framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Entities/Events/Distributed/ExternalEntitySynchronizers/BookSynchronizer.cs rename to framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Entities/Events/Distributed/ExternalEntitySynchronizers/WithEntityVersion/BookSynchronizer.cs index 8470343b5d..94e49d571e 100644 --- a/framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Entities/Events/Distributed/ExternalEntitySynchronizers/BookSynchronizer.cs +++ b/framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Entities/Events/Distributed/ExternalEntitySynchronizers/WithEntityVersion/BookSynchronizer.cs @@ -3,7 +3,7 @@ using Volo.Abp.DependencyInjection; using Volo.Abp.Domain.Repositories; using Volo.Abp.ObjectMapping; -namespace Volo.Abp.Domain.Entities.Events.Distributed.ExternalEntitySynchronizers; +namespace Volo.Abp.Domain.Entities.Events.Distributed.ExternalEntitySynchronizers.WithEntityVersion; public class BookSynchronizer : ExternalEntitySynchronizer, ITransientDependency { diff --git a/framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Entities/Events/Distributed/ExternalEntitySynchronizers/RemoteBookEto.cs b/framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Entities/Events/Distributed/ExternalEntitySynchronizers/WithEntityVersion/RemoteBookEto.cs similarity index 86% rename from framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Entities/Events/Distributed/ExternalEntitySynchronizers/RemoteBookEto.cs rename to framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Entities/Events/Distributed/ExternalEntitySynchronizers/WithEntityVersion/RemoteBookEto.cs index 886bca36bc..26c0c13965 100644 --- a/framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Entities/Events/Distributed/ExternalEntitySynchronizers/RemoteBookEto.cs +++ b/framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Entities/Events/Distributed/ExternalEntitySynchronizers/WithEntityVersion/RemoteBookEto.cs @@ -1,6 +1,6 @@ using Volo.Abp.Auditing; -namespace Volo.Abp.Domain.Entities.Events.Distributed.ExternalEntitySynchronizers; +namespace Volo.Abp.Domain.Entities.Events.Distributed.ExternalEntitySynchronizers.WithEntityVersion; public class RemoteBookEto : EntityEto, IHasEntityVersion { diff --git a/framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Entities/Events/Distributed/ExternalEntitySynchronizers/WithoutEntityVersion/Author.cs b/framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Entities/Events/Distributed/ExternalEntitySynchronizers/WithoutEntityVersion/Author.cs new file mode 100644 index 0000000000..74627ab2ad --- /dev/null +++ b/framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Entities/Events/Distributed/ExternalEntitySynchronizers/WithoutEntityVersion/Author.cs @@ -0,0 +1,17 @@ +using System; + +namespace Volo.Abp.Domain.Entities.Events.Distributed.ExternalEntitySynchronizers.WithoutEntityVersion; + +public class Author : Entity +{ + public virtual string Name { get; set; } + + protected Author() + { + } + + public Author(Guid id, string name) : base(id) + { + Name = name; + } +} \ No newline at end of file diff --git a/framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Entities/Events/Distributed/ExternalEntitySynchronizers/WithoutEntityVersion/AuthorSynchronizer.cs b/framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Entities/Events/Distributed/ExternalEntitySynchronizers/WithoutEntityVersion/AuthorSynchronizer.cs new file mode 100644 index 0000000000..38d26fa6bf --- /dev/null +++ b/framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Entities/Events/Distributed/ExternalEntitySynchronizers/WithoutEntityVersion/AuthorSynchronizer.cs @@ -0,0 +1,14 @@ +using System; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Domain.Repositories; +using Volo.Abp.ObjectMapping; + +namespace Volo.Abp.Domain.Entities.Events.Distributed.ExternalEntitySynchronizers.WithoutEntityVersion; + +public class AuthorSynchronizer : ExternalEntitySynchronizer, ITransientDependency +{ + public AuthorSynchronizer(IObjectMapper objectMapper, IRepository repository) + : base(objectMapper, repository) + { + } +} \ No newline at end of file diff --git a/framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Entities/Events/Distributed/ExternalEntitySynchronizers/WithoutEntityVersion/RemoteAuthorEto.cs b/framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Entities/Events/Distributed/ExternalEntitySynchronizers/WithoutEntityVersion/RemoteAuthorEto.cs new file mode 100644 index 0000000000..dd3f10635b --- /dev/null +++ b/framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Entities/Events/Distributed/ExternalEntitySynchronizers/WithoutEntityVersion/RemoteAuthorEto.cs @@ -0,0 +1,6 @@ +namespace Volo.Abp.Domain.Entities.Events.Distributed.ExternalEntitySynchronizers.WithoutEntityVersion; + +public class RemoteAuthorEto : EntityEto +{ + public string Name { get; set; } +} \ No newline at end of file From 1dd30af987eec2d3bb6b10ec55953900c4b60bb1 Mon Sep 17 00:00:00 2001 From: gdlcf88 Date: Thu, 8 Dec 2022 20:06:09 +0800 Subject: [PATCH 5/8] Rename to `EntitySynchronizer` --- ...rnalEntitySynchronizer.cs => EntitySynchronizer.cs} | 10 +++++----- .../EntitySynchronizer_Tests.cs} | 8 ++++---- .../WithEntityVersion/Book.cs | 2 +- .../WithEntityVersion/BookSynchronizer.cs | 4 ++-- .../WithEntityVersion/RemoteBookEto.cs | 2 +- .../WithoutEntityVersion/Author.cs | 2 +- .../WithoutEntityVersion/AuthorSynchronizer.cs | 4 ++-- .../WithoutEntityVersion/RemoteAuthorEto.cs | 6 ++++++ .../WithoutEntityVersion/RemoteAuthorEto.cs | 6 ------ 9 files changed, 22 insertions(+), 22 deletions(-) rename framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/Events/Distributed/{ExternalEntitySynchronizer.cs => EntitySynchronizer.cs} (93%) rename framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Entities/Events/Distributed/{ExternalEntitySynchronizers/ExternalEntitySynchronizer_Tests.cs => EntitySynchronizers/EntitySynchronizer_Tests.cs} (95%) rename framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Entities/Events/Distributed/{ExternalEntitySynchronizers => EntitySynchronizers}/WithEntityVersion/Book.cs (80%) rename framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Entities/Events/Distributed/{ExternalEntitySynchronizers => EntitySynchronizers}/WithEntityVersion/BookSynchronizer.cs (56%) rename framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Entities/Events/Distributed/{ExternalEntitySynchronizers => EntitySynchronizers}/WithEntityVersion/RemoteBookEto.cs (62%) rename framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Entities/Events/Distributed/{ExternalEntitySynchronizers => EntitySynchronizers}/WithoutEntityVersion/Author.cs (67%) rename framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Entities/Events/Distributed/{ExternalEntitySynchronizers => EntitySynchronizers}/WithoutEntityVersion/AuthorSynchronizer.cs (55%) create mode 100644 framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Entities/Events/Distributed/EntitySynchronizers/WithoutEntityVersion/RemoteAuthorEto.cs delete mode 100644 framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Entities/Events/Distributed/ExternalEntitySynchronizers/WithoutEntityVersion/RemoteAuthorEto.cs diff --git a/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/Events/Distributed/ExternalEntitySynchronizer.cs b/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/Events/Distributed/EntitySynchronizer.cs similarity index 93% rename from framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/Events/Distributed/ExternalEntitySynchronizer.cs rename to framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/Events/Distributed/EntitySynchronizer.cs index 58582912de..91ec77583f 100644 --- a/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/Events/Distributed/ExternalEntitySynchronizer.cs +++ b/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/Events/Distributed/EntitySynchronizer.cs @@ -11,14 +11,14 @@ using Volo.Abp.Uow; namespace Volo.Abp.Domain.Entities.Events.Distributed; -public abstract class ExternalEntitySynchronizer : - ExternalEntitySynchronizer +public abstract class EntitySynchronizer : + EntitySynchronizer where TEntity : class, IEntity where TExternalEntityEto : EntityEto { private readonly IRepository _repository; - protected ExternalEntitySynchronizer(IObjectMapper objectMapper, IRepository repository) : + protected EntitySynchronizer(IObjectMapper objectMapper, IRepository repository) : base(objectMapper, repository) { _repository = repository; @@ -43,7 +43,7 @@ public abstract class ExternalEntitySynchronizer : +public abstract class EntitySynchronizer : IDistributedEventHandler>, IDistributedEventHandler>, IDistributedEventHandler>, @@ -58,7 +58,7 @@ public abstract class ExternalEntitySynchronizer : protected virtual bool IgnoreEntityUpdatedEvent { get; set; } protected virtual bool IgnoreEntityDeletedEvent { get; set; } - public ExternalEntitySynchronizer( + public EntitySynchronizer( IObjectMapper objectMapper, IRepository repository) { diff --git a/framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Entities/Events/Distributed/ExternalEntitySynchronizers/ExternalEntitySynchronizer_Tests.cs b/framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Entities/Events/Distributed/EntitySynchronizers/EntitySynchronizer_Tests.cs similarity index 95% rename from framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Entities/Events/Distributed/ExternalEntitySynchronizers/ExternalEntitySynchronizer_Tests.cs rename to framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Entities/Events/Distributed/EntitySynchronizers/EntitySynchronizer_Tests.cs index 65ee9bcf9f..77c50d2aca 100644 --- a/framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Entities/Events/Distributed/ExternalEntitySynchronizers/ExternalEntitySynchronizer_Tests.cs +++ b/framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Entities/Events/Distributed/EntitySynchronizers/EntitySynchronizer_Tests.cs @@ -7,8 +7,8 @@ using Shouldly; using Volo.Abp.Autofac; using Volo.Abp.AutoMapper; using Volo.Abp.Data; -using Volo.Abp.Domain.Entities.Events.Distributed.ExternalEntitySynchronizers.WithEntityVersion; -using Volo.Abp.Domain.Entities.Events.Distributed.ExternalEntitySynchronizers.WithoutEntityVersion; +using Volo.Abp.Domain.Entities.Events.Distributed.EntitySynchronizers.WithEntityVersion; +using Volo.Abp.Domain.Entities.Events.Distributed.EntitySynchronizers.WithoutEntityVersion; using Volo.Abp.Domain.Repositories; using Volo.Abp.MemoryDb; using Volo.Abp.Modularity; @@ -16,9 +16,9 @@ using Volo.Abp.Testing; using Volo.Abp.Uow; using Xunit; -namespace Volo.Abp.Domain.Entities.Events.Distributed.ExternalEntitySynchronizers; +namespace Volo.Abp.Domain.Entities.Events.Distributed.EntitySynchronizers; -public class ExternalEntitySynchronizer_Tests : AbpIntegratedTest +public class EntitySynchronizer_Tests : AbpIntegratedTest { [Fact] public async Task Should_Handle_Entity_Created_Event() diff --git a/framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Entities/Events/Distributed/ExternalEntitySynchronizers/WithEntityVersion/Book.cs b/framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Entities/Events/Distributed/EntitySynchronizers/WithEntityVersion/Book.cs similarity index 80% rename from framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Entities/Events/Distributed/ExternalEntitySynchronizers/WithEntityVersion/Book.cs rename to framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Entities/Events/Distributed/EntitySynchronizers/WithEntityVersion/Book.cs index fd2facfe19..de70f6b2d1 100644 --- a/framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Entities/Events/Distributed/ExternalEntitySynchronizers/WithEntityVersion/Book.cs +++ b/framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Entities/Events/Distributed/EntitySynchronizers/WithEntityVersion/Book.cs @@ -2,7 +2,7 @@ using System.Text.Json.Serialization; using Volo.Abp.Auditing; -namespace Volo.Abp.Domain.Entities.Events.Distributed.ExternalEntitySynchronizers.WithEntityVersion; +namespace Volo.Abp.Domain.Entities.Events.Distributed.EntitySynchronizers.WithEntityVersion; public class Book : Entity, IHasEntityVersion { diff --git a/framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Entities/Events/Distributed/ExternalEntitySynchronizers/WithEntityVersion/BookSynchronizer.cs b/framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Entities/Events/Distributed/EntitySynchronizers/WithEntityVersion/BookSynchronizer.cs similarity index 56% rename from framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Entities/Events/Distributed/ExternalEntitySynchronizers/WithEntityVersion/BookSynchronizer.cs rename to framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Entities/Events/Distributed/EntitySynchronizers/WithEntityVersion/BookSynchronizer.cs index 94e49d571e..86e7a92bdd 100644 --- a/framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Entities/Events/Distributed/ExternalEntitySynchronizers/WithEntityVersion/BookSynchronizer.cs +++ b/framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Entities/Events/Distributed/EntitySynchronizers/WithEntityVersion/BookSynchronizer.cs @@ -3,9 +3,9 @@ using Volo.Abp.DependencyInjection; using Volo.Abp.Domain.Repositories; using Volo.Abp.ObjectMapping; -namespace Volo.Abp.Domain.Entities.Events.Distributed.ExternalEntitySynchronizers.WithEntityVersion; +namespace Volo.Abp.Domain.Entities.Events.Distributed.EntitySynchronizers.WithEntityVersion; -public class BookSynchronizer : ExternalEntitySynchronizer, ITransientDependency +public class BookSynchronizer : EntitySynchronizer, ITransientDependency { public BookSynchronizer(IObjectMapper objectMapper, IRepository repository) : base(objectMapper, repository) diff --git a/framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Entities/Events/Distributed/ExternalEntitySynchronizers/WithEntityVersion/RemoteBookEto.cs b/framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Entities/Events/Distributed/EntitySynchronizers/WithEntityVersion/RemoteBookEto.cs similarity index 62% rename from framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Entities/Events/Distributed/ExternalEntitySynchronizers/WithEntityVersion/RemoteBookEto.cs rename to framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Entities/Events/Distributed/EntitySynchronizers/WithEntityVersion/RemoteBookEto.cs index 26c0c13965..48e532222d 100644 --- a/framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Entities/Events/Distributed/ExternalEntitySynchronizers/WithEntityVersion/RemoteBookEto.cs +++ b/framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Entities/Events/Distributed/EntitySynchronizers/WithEntityVersion/RemoteBookEto.cs @@ -1,6 +1,6 @@ using Volo.Abp.Auditing; -namespace Volo.Abp.Domain.Entities.Events.Distributed.ExternalEntitySynchronizers.WithEntityVersion; +namespace Volo.Abp.Domain.Entities.Events.Distributed.EntitySynchronizers.WithEntityVersion; public class RemoteBookEto : EntityEto, IHasEntityVersion { diff --git a/framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Entities/Events/Distributed/ExternalEntitySynchronizers/WithoutEntityVersion/Author.cs b/framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Entities/Events/Distributed/EntitySynchronizers/WithoutEntityVersion/Author.cs similarity index 67% rename from framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Entities/Events/Distributed/ExternalEntitySynchronizers/WithoutEntityVersion/Author.cs rename to framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Entities/Events/Distributed/EntitySynchronizers/WithoutEntityVersion/Author.cs index 74627ab2ad..49b6e14cf7 100644 --- a/framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Entities/Events/Distributed/ExternalEntitySynchronizers/WithoutEntityVersion/Author.cs +++ b/framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Entities/Events/Distributed/EntitySynchronizers/WithoutEntityVersion/Author.cs @@ -1,6 +1,6 @@ using System; -namespace Volo.Abp.Domain.Entities.Events.Distributed.ExternalEntitySynchronizers.WithoutEntityVersion; +namespace Volo.Abp.Domain.Entities.Events.Distributed.EntitySynchronizers.WithoutEntityVersion; public class Author : Entity { diff --git a/framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Entities/Events/Distributed/ExternalEntitySynchronizers/WithoutEntityVersion/AuthorSynchronizer.cs b/framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Entities/Events/Distributed/EntitySynchronizers/WithoutEntityVersion/AuthorSynchronizer.cs similarity index 55% rename from framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Entities/Events/Distributed/ExternalEntitySynchronizers/WithoutEntityVersion/AuthorSynchronizer.cs rename to framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Entities/Events/Distributed/EntitySynchronizers/WithoutEntityVersion/AuthorSynchronizer.cs index 38d26fa6bf..de650061d5 100644 --- a/framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Entities/Events/Distributed/ExternalEntitySynchronizers/WithoutEntityVersion/AuthorSynchronizer.cs +++ b/framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Entities/Events/Distributed/EntitySynchronizers/WithoutEntityVersion/AuthorSynchronizer.cs @@ -3,9 +3,9 @@ using Volo.Abp.DependencyInjection; using Volo.Abp.Domain.Repositories; using Volo.Abp.ObjectMapping; -namespace Volo.Abp.Domain.Entities.Events.Distributed.ExternalEntitySynchronizers.WithoutEntityVersion; +namespace Volo.Abp.Domain.Entities.Events.Distributed.EntitySynchronizers.WithoutEntityVersion; -public class AuthorSynchronizer : ExternalEntitySynchronizer, ITransientDependency +public class AuthorSynchronizer : EntitySynchronizer, ITransientDependency { public AuthorSynchronizer(IObjectMapper objectMapper, IRepository repository) : base(objectMapper, repository) diff --git a/framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Entities/Events/Distributed/EntitySynchronizers/WithoutEntityVersion/RemoteAuthorEto.cs b/framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Entities/Events/Distributed/EntitySynchronizers/WithoutEntityVersion/RemoteAuthorEto.cs new file mode 100644 index 0000000000..3923b2eeff --- /dev/null +++ b/framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Entities/Events/Distributed/EntitySynchronizers/WithoutEntityVersion/RemoteAuthorEto.cs @@ -0,0 +1,6 @@ +namespace Volo.Abp.Domain.Entities.Events.Distributed.EntitySynchronizers.WithoutEntityVersion; + +public class RemoteAuthorEto : EntityEto +{ + public string Name { get; set; } +} \ No newline at end of file diff --git a/framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Entities/Events/Distributed/ExternalEntitySynchronizers/WithoutEntityVersion/RemoteAuthorEto.cs b/framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Entities/Events/Distributed/ExternalEntitySynchronizers/WithoutEntityVersion/RemoteAuthorEto.cs deleted file mode 100644 index dd3f10635b..0000000000 --- a/framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Entities/Events/Distributed/ExternalEntitySynchronizers/WithoutEntityVersion/RemoteAuthorEto.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Volo.Abp.Domain.Entities.Events.Distributed.ExternalEntitySynchronizers.WithoutEntityVersion; - -public class RemoteAuthorEto : EntityEto -{ - public string Name { get; set; } -} \ No newline at end of file From 3ad093f696753da5f83c6b8f2dcbb0b076a8859f Mon Sep 17 00:00:00 2001 From: gdlcf88 Date: Thu, 8 Dec 2022 20:47:44 +0800 Subject: [PATCH 6/8] Update Distributed-Event-Bus.md --- docs/en/Distributed-Event-Bus.md | 50 ++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/docs/en/Distributed-Event-Bus.md b/docs/en/Distributed-Event-Bus.md index 21b1b07256..a3c1237cd7 100644 --- a/docs/en/Distributed-Event-Bus.md +++ b/docs/en/Distributed-Event-Bus.md @@ -499,6 +499,56 @@ Configure(options => }); ```` +## Entity Synchronizer + +Todo: introdution. + +### Create a Synchronizer Class + +Todo. + +```csharp +public class BlogUserSynchronizer : EntitySynchronizer, ITransientDependency +{ + public BlogUserSynchronizer(IObjectMapper objectMapper, IRepository repository) : + base(objectMapper, repository) + { + } +} +``` + +### Advanced Usages + +We may want to skip synchronizing the entity data on the external entity created, updated, or deleted. The `EntitySynchronizer` has three bool properties to control the handling behaviors. + +```csharp +public class BlogUserSynchronizer : EntitySynchronizer, ITransientDependency +{ + protected override bool IgnoreEntityCreatedEvent => true; + protected override bool IgnoreEntityUpdatedEvent => true; + protected override bool IgnoreEntityDeletedEvent => true; + + // ctor ... +} +``` + +### Eventual Consistency Guarantee + +Developers should always handle the distributed events disordering. ABP framework has an `EntityVersion` audit property to avoid an old version of entity data overriding a new one. + +The only thing we need to do is make the entity implement the `IHasEntityVersion` interface. + +```csharp +public class User : Entity, IHasEntityVersion +{ + public int EntityVersion { get; set; } +} +``` + +After that, the entity synchronizer will know the entity version number and skip handling the stale events. + +> See the community post [Notice and Solve ABP Distributed Events Disordering](https://community.abp.io/posts/notice-and-solve-abp-distributed-events-disordering-yi9vq3p4) for more if you are interested or worried. + ## See Also * [Local Event Bus](Local-Event-Bus.md) From 425c281c34c2a7b51dde064b8a6be57cd83c14d2 Mon Sep 17 00:00:00 2001 From: gdlcf88 Date: Thu, 8 Dec 2022 20:54:50 +0800 Subject: [PATCH 7/8] Fix Distributed-Event-Bus.md --- docs/en/Distributed-Event-Bus.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/en/Distributed-Event-Bus.md b/docs/en/Distributed-Event-Bus.md index a3c1237cd7..9664458aae 100644 --- a/docs/en/Distributed-Event-Bus.md +++ b/docs/en/Distributed-Event-Bus.md @@ -536,13 +536,18 @@ public class BlogUserSynchronizer : EntitySynchronizer, Developers should always handle the distributed events disordering. ABP framework has an `EntityVersion` audit property to avoid an old version of entity data overriding a new one. -The only thing we need to do is make the entity implement the `IHasEntityVersion` interface. +The only thing we need to do is make the entity class and the ETO class implement the `IHasEntityVersion` interface. ```csharp public class User : Entity, IHasEntityVersion { public int EntityVersion { get; set; } } + +public class UserEto : EntityEto, IHasEntityVersion +{ + public int EntityVersion { get; set; } +} ``` After that, the entity synchronizer will know the entity version number and skip handling the stale events. From 0948b34344ea3a8a819e4aacc494107de3f63268 Mon Sep 17 00:00:00 2001 From: Super Date: Thu, 8 Dec 2022 20:57:53 +0800 Subject: [PATCH 8/8] Update Distributed-Event-Bus.md --- docs/en/Distributed-Event-Bus.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/en/Distributed-Event-Bus.md b/docs/en/Distributed-Event-Bus.md index 9664458aae..5504aeffd3 100644 --- a/docs/en/Distributed-Event-Bus.md +++ b/docs/en/Distributed-Event-Bus.md @@ -501,7 +501,7 @@ Configure(options => ## Entity Synchronizer -Todo: introdution. +Todo: introduction. ### Create a Synchronizer Class