Browse Source

Merge pull request #14197 from gdlcf88/entity-sync

Introduce `IHasEntityVersion` and `EntitySynchronizer`
pull/15139/head
Halil İbrahim Kalkan 4 years ago
committed by GitHub
parent
commit
2c37e56751
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 55
      docs/en/Distributed-Event-Bus.md
  2. 12
      framework/src/Volo.Abp.Auditing.Contracts/Volo/Abp/Auditing/IHasEntityVersion.cs
  3. 10
      framework/src/Volo.Abp.Auditing/Volo/Abp/Auditing/AuditPropertySetter.cs
  4. 2
      framework/src/Volo.Abp.Auditing/Volo/Abp/Auditing/IAuditPropertySetter.cs
  5. 175
      framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/Events/Distributed/EntitySynchronizer.cs
  6. 6
      framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/AbpDbContext.cs
  7. 6
      framework/src/Volo.Abp.MemoryDb/Volo/Abp/Domain/Repositories/MemoryDb/MemoryDbRepository.cs
  8. 6
      framework/src/Volo.Abp.MongoDB/Volo/Abp/Domain/Repositories/MongoDB/MongoDbRepository.cs
  9. 3
      framework/test/Volo.Abp.Auditing.Tests/Volo/Abp/Auditing/AuditPropertySetterTestBase.cs
  10. 21
      framework/test/Volo.Abp.Auditing.Tests/Volo/Abp/Auditing/AuditPropertySetter_EntityVersion_Tests.cs
  11. 3
      framework/test/Volo.Abp.Ddd.Tests/Volo.Abp.Ddd.Tests.csproj
  12. 254
      framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Entities/Events/Distributed/EntitySynchronizers/EntitySynchronizer_Tests.cs
  13. 22
      framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Entities/Events/Distributed/EntitySynchronizers/WithEntityVersion/Book.cs
  14. 14
      framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Entities/Events/Distributed/EntitySynchronizers/WithEntityVersion/BookSynchronizer.cs
  15. 10
      framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Entities/Events/Distributed/EntitySynchronizers/WithEntityVersion/RemoteBookEto.cs
  16. 17
      framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Entities/Events/Distributed/EntitySynchronizers/WithoutEntityVersion/Author.cs
  17. 14
      framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Entities/Events/Distributed/EntitySynchronizers/WithoutEntityVersion/AuthorSynchronizer.cs
  18. 6
      framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Entities/Events/Distributed/EntitySynchronizers/WithoutEntityVersion/RemoteAuthorEto.cs
  19. 5
      framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Domain/Person.cs
  20. 16
      framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Testing/Auditing_Tests.cs

55
docs/en/Distributed-Event-Bus.md

@ -522,6 +522,61 @@ Configure<AbpDistributedEventBusOptions>(options =>
});
````
## Entity Synchronizer
Todo: introduction.
### Create a Synchronizer Class
Todo.
```csharp
public class BlogUserSynchronizer : EntitySynchronizer<BlogUser, Guid, UserEto>, ITransientDependency
{
public BlogUserSynchronizer(IObjectMapper objectMapper, IRepository<BlogUser, Guid> 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<BlogUser, Guid, UserEto>, 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<Guid>, 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)

12
framework/src/Volo.Abp.Auditing.Contracts/Volo/Abp/Auditing/IHasEntityVersion.cs

@ -0,0 +1,12 @@
namespace Volo.Abp.Auditing;
/// <summary>
/// An entity version property that auto-increments when the entity changes.
/// </summary>
public interface IHasEntityVersion
{
/// <summary>
/// An entity version property that auto-increments when the entity changes.
/// </summary>
int EntityVersion { get; }
}

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

2
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);
}

175
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<TEntity, TKey, TExternalEntityEto> :
EntitySynchronizer<TEntity, TExternalEntityEto>
where TEntity : class, IEntity<TKey>
where TExternalEntityEto : EntityEto
{
private readonly IRepository<TEntity, TKey> _repository;
protected EntitySynchronizer(IObjectMapper objectMapper, IRepository<TEntity, TKey> repository) :
base(objectMapper, repository)
{
_repository = repository;
}
protected override Task<TEntity> 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<TEntity, TExternalEntityEto> :
IDistributedEventHandler<EntityCreatedEto<TExternalEntityEto>>,
IDistributedEventHandler<EntityUpdatedEto<TExternalEntityEto>>,
IDistributedEventHandler<EntityDeletedEto<TExternalEntityEto>>,
IUnitOfWorkEnabled
where TEntity : class, IEntity
where TExternalEntityEto : EntityEto
{
protected IObjectMapper ObjectMapper { get; }
private readonly IRepository<TEntity> _repository;
protected virtual bool IgnoreEntityCreatedEvent { get; set; }
protected virtual bool IgnoreEntityUpdatedEvent { get; set; }
protected virtual bool IgnoreEntityDeletedEvent { get; set; }
public EntitySynchronizer(
IObjectMapper objectMapper,
IRepository<TEntity> repository)
{
ObjectMapper = objectMapper;
_repository = repository;
}
public virtual async Task HandleEventAsync(EntityCreatedEto<TExternalEntityEto> eventData)
{
if (IgnoreEntityCreatedEvent)
{
return;
}
await TryCreateOrUpdateEntityAsync(eventData.Entity);
}
public virtual async Task HandleEventAsync(EntityUpdatedEto<TExternalEntityEto> eventData)
{
if (IgnoreEntityUpdatedEvent)
{
return;
}
await TryCreateOrUpdateEntityAsync(eventData.Entity);
}
public virtual async Task HandleEventAsync(EntityDeletedEto<TExternalEntityEto> eventData)
{
if (IgnoreEntityDeletedEvent)
{
return;
}
await TryDeleteEntityAsync(eventData.Entity);
}
protected virtual async Task<bool> 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<TEntity> MapToEntityAsync(TExternalEntityEto eto)
{
return Task.FromResult(ObjectMapper.Map<TExternalEntityEto, TEntity>(eto));
}
protected virtual Task MapToEntityAsync(TExternalEntityEto eto, TEntity localEntity)
{
ObjectMapper.Map(eto, localEntity);
return Task.CompletedTask;
}
protected virtual async Task<bool> TryDeleteEntityAsync(TExternalEntityEto eto)
{
var localEntity = await FindLocalEntityAsync(eto);
if (localEntity == null)
{
return false;
}
await _repository.DeleteAsync(localEntity, true);
return true;
}
[ItemCanBeNull]
protected abstract Task<TEntity> FindLocalEntityAsync(TExternalEntityEto eto);
protected virtual Task<bool> 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);
}
}

6
framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/AbpDbContext.cs

@ -459,6 +459,7 @@ public abstract class AbpDbContext<TDbContext> : 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<ISoftDelete>().IsDeleted)
@ -574,6 +575,11 @@ public abstract class AbpDbContext<TDbContext> : DbContext, IAbpEfCoreDbContext,
AuditPropertySetter?.SetDeletionProperties(entry.Entity);
}
protected virtual void IncrementEntityVersionProperty(EntityEntry entry)
{
AuditPropertySetter?.IncrementEntityVersionProperty(entry.Entity);
}
protected virtual void ConfigureBaseProperties<TEntity>(ModelBuilder modelBuilder, IMutableEntityType mutableEntityType)
where TEntity : class
{

6
framework/src/Volo.Abp.MemoryDb/Volo/Abp/Domain/Repositories/MemoryDb/MemoryDbRepository.cs

@ -157,6 +157,11 @@ public class MemoryDbRepository<TMemoryDbContext, TEntity> : RepositoryBase<TEnt
AuditPropertySetter.SetDeletionProperties(entity);
}
protected virtual void IncrementEntityVersionProperty(TEntity entity)
{
AuditPropertySetter.IncrementEntityVersionProperty(entity);
}
protected virtual void TriggerEntityCreateEvents(TEntity entity)
{
EntityChangeEventHelper.PublishEntityCreatedEvent(entity);
@ -227,6 +232,7 @@ public class MemoryDbRepository<TMemoryDbContext, TEntity> : RepositoryBase<TEnt
bool autoSave = false,
CancellationToken cancellationToken = default)
{
IncrementEntityVersionProperty(entity);
SetModificationAuditProperties(entity);
if (entity is ISoftDelete softDeleteEntity && softDeleteEntity.IsDeleted)

6
framework/src/Volo.Abp.MongoDB/Volo/Abp/Domain/Repositories/MongoDB/MongoDbRepository.cs

@ -177,6 +177,7 @@ public class MongoDbRepository<TMongoDbContext, TEntity>
{
cancellationToken = GetCancellationToken(cancellationToken);
IncrementEntityVersionProperty(entity);
SetModificationAuditProperties(entity);
if (entity is ISoftDelete softDeleteEntity && softDeleteEntity.IsDeleted)
@ -668,6 +669,11 @@ public class MongoDbRepository<TMongoDbContext, TEntity>
AuditPropertySetter.SetDeletionProperties(entity);
}
protected virtual void IncrementEntityVersionProperty(TEntity entity)
{
AuditPropertySetter.IncrementEntityVersionProperty(entity);
}
protected virtual void TriggerDomainEvents(object entity)
{
var generatesDomainEventsEntity = entity as IGeneratesDomainEvents;

3
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; }
}
}

21
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);
}
}

3
framework/test/Volo.Abp.Ddd.Tests/Volo.Abp.Ddd.Tests.csproj

@ -8,7 +8,10 @@
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Volo.Abp.Autofac\Volo.Abp.Autofac.csproj" />
<ProjectReference Include="..\..\src\Volo.Abp.AutoMapper\Volo.Abp.AutoMapper.csproj" />
<ProjectReference Include="..\..\src\Volo.Abp.Ddd.Domain\Volo.Abp.Ddd.Domain.csproj" />
<ProjectReference Include="..\..\src\Volo.Abp.MemoryDb\Volo.Abp.MemoryDb.csproj" />
<ProjectReference Include="..\AbpTestBase\AbpTestBase.csproj" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="$(MicrosoftNETTestSdkPackageVersion)" />
</ItemGroup>

254
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<EntitySynchronizer_Tests.TestModule>
{
[Fact]
public async Task Should_Handle_Entity_Created_Event()
{
var authorId = Guid.NewGuid();
var uowManager = GetRequiredService<IUnitOfWorkManager>();
using var uow = uowManager.Begin();
var authorSynchronizer = GetRequiredService<AuthorSynchronizer>();
var repository = GetRequiredService<IRepository<Author, Guid>>();
(await repository.FindAsync(authorId)).ShouldBeNull();
var remoteAuthorEto = new RemoteAuthorEto { KeysAsString = authorId.ToString(), Name = "New" };
await authorSynchronizer.HandleEventAsync(new EntityCreatedEto<RemoteAuthorEto>(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<IUnitOfWorkManager>();
using var uow = uowManager.Begin();
var authorSynchronizer = GetRequiredService<AuthorSynchronizer>();
var repository = GetRequiredService<IRepository<Author, Guid>>();
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>(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<IUnitOfWorkManager>();
using var uow = uowManager.Begin();
var authorSynchronizer = GetRequiredService<AuthorSynchronizer>();
var repository = GetRequiredService<IRepository<Author, Guid>>();
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>(remoteAuthorEto));
(await repository.FindAsync(authorId)).ShouldBeNull();
await Should.NotThrowAsync(() =>
authorSynchronizer.HandleEventAsync(new EntityDeletedEto<RemoteAuthorEto>(remoteAuthorEto)));
}
[Fact]
public async Task Should_Handle_Versioned_Entity_Created_Event()
{
var bookId = Guid.NewGuid();
var uowManager = GetRequiredService<IUnitOfWorkManager>();
using var uow = uowManager.Begin();
var bookSynchronizer = GetRequiredService<BookSynchronizer>();
var repository = GetRequiredService<IRepository<Book, Guid>>();
(await repository.FindAsync(bookId)).ShouldBeNull();
var remoteBookEto = new RemoteBookEto { KeysAsString = bookId.ToString(), EntityVersion = 0, Sold = 1 };
await bookSynchronizer.HandleEventAsync(new EntityCreatedEto<RemoteBookEto>(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<IUnitOfWorkManager>();
using var uow = uowManager.Begin();
var bookSynchronizer = GetRequiredService<BookSynchronizer>();
var repository = GetRequiredService<IRepository<Book, Guid>>();
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>(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>(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>(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<IUnitOfWorkManager>();
using var uow = uowManager.Begin();
var bookSynchronizer = GetRequiredService<BookSynchronizer>();
var repository = GetRequiredService<IRepository<Book, Guid>>();
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>(remoteBookEto));
(await repository.FindAsync(bookId)).ShouldBeNull();
await Should.NotThrowAsync(() =>
bookSynchronizer.HandleEventAsync(new EntityDeletedEto<RemoteBookEto>(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<AbpDbConnectionOptions>(options =>
{
options.ConnectionStrings.Default = connStr;
});
context.Services.AddMemoryDbContext<MyMemoryDbContext>(options =>
{
options.AddDefaultRepositories(includeAllEntities: true);
});
context.Services.AddAutoMapperObjectMapper<TestModule>();
Configure<AbpAutoMapperOptions>(options =>
{
options.AddMaps<TestModule>(validate: true);
});
}
}
public class MyMemoryDbContext : MemoryDbContext
{
public override IReadOnlyList<Type> GetEntityTypes()
{
return new List<Type> { typeof(Book), typeof(Author) };
}
}
public class MyAutoMapperProfile : Profile
{
public MyAutoMapperProfile()
{
CreateMap<RemoteBookEto, Book>(MemberList.None)
.ForMember(x => x.Id, options => options.MapFrom(x => Guid.Parse(x.KeysAsString)));
CreateMap<RemoteAuthorEto, Author>(MemberList.None)
.ForMember(x => x.Id, options => options.MapFrom(x => Guid.Parse(x.KeysAsString)));
}
}
}

22
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<Guid>, 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;
}
}

14
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<Book, Guid, RemoteBookEto>, ITransientDependency
{
public BookSynchronizer(IObjectMapper objectMapper, IRepository<Book, Guid> repository)
: base(objectMapper, repository)
{
}
}

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

17
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<Guid>
{
public virtual string Name { get; set; }
protected Author()
{
}
public Author(Guid id, string name) : base(id)
{
Name = name;
}
}

14
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<Author, Guid, RemoteAuthorEto>, ITransientDependency
{
public AuthorSynchronizer(IObjectMapper objectMapper, IRepository<Author, Guid> repository)
: base(objectMapper, repository)
{
}
}

6
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; }
}

5
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<Guid>, IMultiTenant
public class Person : FullAuditedAggregateRoot<Guid>, IMultiTenant, IHasEntityVersion
{
public virtual Guid? TenantId { get; set; }
@ -29,6 +30,8 @@ public class Person : FullAuditedAggregateRoot<Guid>, IMultiTenant
public virtual DateTime LastActiveTime { get; set; }
public int EntityVersion { get; set; }
private Person()
{
}

16
framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Testing/Auditing_Tests.cs

@ -111,4 +111,20 @@ public abstract class Auditing_Tests<TStartupModule> : TestAppTestBase<TStartupM
douglas.DeleterId.ShouldBe(CurrentUserId);
}
}
[Fact]
public async Task Should_Increment_EntityVersion_Property()
{
var douglas = await PersonRepository.GetAsync(TestDataBuilder.UserDouglasId);
douglas.EntityVersion.ShouldBe(0);
douglas.Age++;
await PersonRepository.UpdateAsync(douglas);
douglas = await PersonRepository.FindAsync(TestDataBuilder.UserDouglasId);
douglas.ShouldNotBeNull();
douglas.EntityVersion.ShouldBe(1);
}
}

Loading…
Cancel
Save