diff --git a/docs/en/Distributed-Event-Bus.md b/docs/en/Distributed-Event-Bus.md index cc63e98fcd..e40568a9c0 100644 --- a/docs/en/Distributed-Event-Bus.md +++ b/docs/en/Distributed-Event-Bus.md @@ -522,6 +522,61 @@ Configure(options => }); ```` +## Entity Synchronizer + +Todo: introduction. + +### 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 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. + +> 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) 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/EntitySynchronizer.cs b/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/Events/Distributed/EntitySynchronizer.cs new file mode 100644 index 0000000000..91ec77583f --- /dev/null +++ b/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/Events/Distributed/EntitySynchronizer.cs @@ -0,0 +1,175 @@ +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 EntitySynchronizer : + EntitySynchronizer + where TEntity : class, IEntity + where TExternalEntityEto : EntityEto +{ + private readonly IRepository _repository; + + protected EntitySynchronizer(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 EntitySynchronizer : + IDistributedEventHandler>, + IDistributedEventHandler>, + IDistributedEventHandler>, + IUnitOfWorkEnabled + where TEntity : class, IEntity + where TExternalEntityEto : EntityEto +{ + 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 EntitySynchronizer( + IObjectMapper objectMapper, + IRepository repository) + { + ObjectMapper = objectMapper; + _repository = repository; + } + + public virtual async Task HandleEventAsync(EntityCreatedEto eventData) + { + if (IgnoreEntityCreatedEvent) + { + return; + } + + await TryCreateOrUpdateEntityAsync(eventData.Entity); + } + + public virtual async Task HandleEventAsync(EntityUpdatedEto eventData) + { + if (IgnoreEntityUpdatedEvent) + { + return; + } + + await TryCreateOrUpdateEntityAsync(eventData.Entity); + } + + public virtual async Task HandleEventAsync(EntityDeletedEto eventData) + { + if (IgnoreEntityDeletedEvent) + { + return; + } + + await TryDeleteEntityAsync(eventData.Entity); + } + + protected virtual async Task TryCreateOrUpdateEntityAsync(TExternalEntityEto eto) + { + var localEntity = await FindLocalEntityAsync(eto); + + if (!await IsEtoNewerAsync(eto, localEntity)) + { + return false; + } + + if (localEntity == null) + { + localEntity = await MapToEntityAsync(eto); + + if (localEntity is IHasEntityVersion versionedLocalEntity && eto is IHasEntityVersion versionedEto) + { + ObjectHelper.TrySetProperty(versionedLocalEntity, x => x.EntityVersion, + () => versionedEto.EntityVersion); + } + + await _repository.InsertAsync(localEntity, true); + } + else + { + await MapToEntityAsync(eto, localEntity); + + 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); + } + + return 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 false; + } + + await _repository.DeleteAsync(localEntity, true); + + return true; + } + + [ItemCanBeNull] + protected abstract Task FindLocalEntityAsync(TExternalEntityEto eto); + + protected virtual Task IsEtoNewerAsync(TExternalEntityEto eto, [CanBeNull] TEntity localEntity) + { + 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/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/AbpDbContext.cs b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/AbpDbContext.cs index 411c0a4519..77cc348e80 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 de7afbe3c6..cfddbc6464 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) @@ -668,6 +669,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.Ddd.Tests.csproj b/framework/test/Volo.Abp.Ddd.Tests/Volo.Abp.Ddd.Tests.csproj index ce26f12583..5472aa96b2 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/EntitySynchronizers/EntitySynchronizer_Tests.cs b/framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Entities/Events/Distributed/EntitySynchronizers/EntitySynchronizer_Tests.cs new file mode 100644 index 0000000000..77c50d2aca --- /dev/null +++ b/framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Entities/Events/Distributed/EntitySynchronizers/EntitySynchronizer_Tests.cs @@ -0,0 +1,254 @@ +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.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; +using Volo.Abp.Testing; +using Volo.Abp.Uow; +using Xunit; + +namespace Volo.Abp.Domain.Entities.Events.Distributed.EntitySynchronizers; + +public class EntitySynchronizer_Tests : AbpIntegratedTest +{ + [Fact] + public async Task Should_Handle_Entity_Created_Event() + { + var authorId = Guid.NewGuid(); + + var uowManager = GetRequiredService(); + 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(); + + 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(), EntityVersion = 0, Sold = 1 }; + + await bookSynchronizer.HandleEventAsync(new EntityCreatedEto(remoteBookEto)); + + var book = await repository.FindAsync(bookId); + book.ShouldNotBeNull(); + book.EntityVersion.ShouldBe(remoteBookEto.EntityVersion); + book.Sold.ShouldBe(1); + } + + [Fact] + public async Task Should_Handle_Versioned_Entity_Update_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.EntityVersion.ShouldBe(0); + + var remoteBookEto = new RemoteBookEto { KeysAsString = bookId.ToString(), EntityVersion = 0, Sold = 10 }; + + await bookSynchronizer.HandleEventAsync(new EntityUpdatedEto(remoteBookEto)); + + book = await repository.FindAsync(bookId); + book.ShouldNotBeNull(); + book.EntityVersion.ShouldBe(0); + book.Sold.ShouldBe(1); + + remoteBookEto.EntityVersion = 1; + remoteBookEto.Sold = 2; + + await bookSynchronizer.HandleEventAsync(new EntityUpdatedEto(remoteBookEto)); + + book = await repository.FindAsync(bookId); + book.ShouldNotBeNull(); + book.EntityVersion.ShouldBe(1); + book.Sold.ShouldBe(2); + + 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.EntityVersion.ShouldBe(1); + book.Sold.ShouldBe(2); + } + + [Fact] + public async Task Should_Handle_Versioned_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.EntityVersion.ShouldBe(0); + + var remoteBookEto = new RemoteBookEto { KeysAsString = bookId.ToString(), EntityVersion = 0, Sold = 1 }; + + await bookSynchronizer.HandleEventAsync(new EntityDeletedEto(remoteBookEto)); + + (await repository.FindAsync(bookId)).ShouldBeNull(); + + await Should.NotThrowAsync(() => + bookSynchronizer.HandleEventAsync(new EntityDeletedEto(remoteBookEto))); + } + + 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); + }); + + context.Services.AddAutoMapperObjectMapper(); + Configure(options => + { + options.AddMaps(validate: true); + }); + } + } + + public class MyMemoryDbContext : MemoryDbContext + { + public override IReadOnlyList GetEntityTypes() + { + return new List { typeof(Book), typeof(Author) }; + } + } + + public class MyAutoMapperProfile : Profile + { + public MyAutoMapperProfile() + { + CreateMap(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/EntitySynchronizers/WithEntityVersion/Book.cs b/framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Entities/Events/Distributed/EntitySynchronizers/WithEntityVersion/Book.cs new file mode 100644 index 0000000000..de70f6b2d1 --- /dev/null +++ b/framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Entities/Events/Distributed/EntitySynchronizers/WithEntityVersion/Book.cs @@ -0,0 +1,22 @@ +using System; +using System.Text.Json.Serialization; +using Volo.Abp.Auditing; + +namespace Volo.Abp.Domain.Entities.Events.Distributed.EntitySynchronizers.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) : 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/EntitySynchronizers/WithEntityVersion/BookSynchronizer.cs b/framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Entities/Events/Distributed/EntitySynchronizers/WithEntityVersion/BookSynchronizer.cs new file mode 100644 index 0000000000..86e7a92bdd --- /dev/null +++ b/framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Entities/Events/Distributed/EntitySynchronizers/WithEntityVersion/BookSynchronizer.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.EntitySynchronizers.WithEntityVersion; + +public class BookSynchronizer : EntitySynchronizer, 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/EntitySynchronizers/WithEntityVersion/RemoteBookEto.cs b/framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Entities/Events/Distributed/EntitySynchronizers/WithEntityVersion/RemoteBookEto.cs new file mode 100644 index 0000000000..48e532222d --- /dev/null +++ b/framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Entities/Events/Distributed/EntitySynchronizers/WithEntityVersion/RemoteBookEto.cs @@ -0,0 +1,10 @@ +using Volo.Abp.Auditing; + +namespace Volo.Abp.Domain.Entities.Events.Distributed.EntitySynchronizers.WithEntityVersion; + +public class RemoteBookEto : EntityEto, IHasEntityVersion +{ + public int EntityVersion { get; set; } + + public int Sold { get; set; } +} \ No newline at end of file diff --git a/framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Entities/Events/Distributed/EntitySynchronizers/WithoutEntityVersion/Author.cs b/framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Entities/Events/Distributed/EntitySynchronizers/WithoutEntityVersion/Author.cs new file mode 100644 index 0000000000..49b6e14cf7 --- /dev/null +++ b/framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Entities/Events/Distributed/EntitySynchronizers/WithoutEntityVersion/Author.cs @@ -0,0 +1,17 @@ +using System; + +namespace Volo.Abp.Domain.Entities.Events.Distributed.EntitySynchronizers.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/EntitySynchronizers/WithoutEntityVersion/AuthorSynchronizer.cs b/framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Entities/Events/Distributed/EntitySynchronizers/WithoutEntityVersion/AuthorSynchronizer.cs new file mode 100644 index 0000000000..de650061d5 --- /dev/null +++ b/framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Entities/Events/Distributed/EntitySynchronizers/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.EntitySynchronizers.WithoutEntityVersion; + +public class AuthorSynchronizer : EntitySynchronizer, 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/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.TestApp/Volo/Abp/TestApp/Domain/Person.cs b/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Domain/Person.cs index 0fc98a2a8b..0726126eba 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,13 +1,14 @@ using System; using System.Collections.ObjectModel; using System.ComponentModel.DataAnnotations.Schema; +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; } @@ -29,6 +30,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