mirror of https://github.com/abpframework/abp.git
committed by
GitHub
20 changed files with 654 additions and 3 deletions
@ -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; } |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
@ -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))); |
|||
} |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
@ -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) |
|||
{ |
|||
} |
|||
} |
|||
@ -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; } |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
@ -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) |
|||
{ |
|||
} |
|||
} |
|||
@ -0,0 +1,6 @@ |
|||
namespace Volo.Abp.Domain.Entities.Events.Distributed.EntitySynchronizers.WithoutEntityVersion; |
|||
|
|||
public class RemoteAuthorEto : EntityEto |
|||
{ |
|||
public string Name { get; set; } |
|||
} |
|||
Loading…
Reference in new issue