mirror of https://github.com/abpframework/abp.git
9 changed files with 479 additions and 8 deletions
@ -0,0 +1,74 @@ |
|||||
|
using System; |
||||
|
using JetBrains.Annotations; |
||||
|
using Volo.Abp.Uow; |
||||
|
|
||||
|
namespace Volo.Abp.Domain.Entities.Events; |
||||
|
|
||||
|
public static class EntityChangeUnitOfWorkExtensions |
||||
|
{ |
||||
|
private const string UpdateAggregateRootWhenNavigationChangesItemKey = "Abp.UpdateAggregateRootWhenNavigationChanges"; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Disables updating the aggregate root when one of its navigation properties changes,
|
||||
|
/// for the given unit of work. It overrides the <see cref="AbpEntityChangeOptions.UpdateAggregateRootWhenNavigationChanges"/> option.
|
||||
|
/// It doesn't disable publishing the entity updated event, which is still controlled by the
|
||||
|
/// <see cref="AbpEntityChangeOptions.PublishEntityUpdatedEventWhenNavigationChanges"/> option
|
||||
|
/// and the <see cref="AbpEntityChangeOptions.IgnoredNavigationEntitySelectors"/>.
|
||||
|
/// </summary>
|
||||
|
/// <param name="unitOfWork">A unit of work object</param>
|
||||
|
/// <returns>
|
||||
|
/// A disposable object. Dispose it to restore the setting back to its previous state.
|
||||
|
/// </returns>
|
||||
|
public static IDisposable DisableUpdateAggregateRootWhenNavigationChanges([NotNull] this IUnitOfWork unitOfWork) |
||||
|
{ |
||||
|
return SetUpdateAggregateRootWhenNavigationChanges(unitOfWork, false); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Enables updating the aggregate root when one of its navigation properties changes,
|
||||
|
/// for the given unit of work. It overrides the <see cref="AbpEntityChangeOptions.UpdateAggregateRootWhenNavigationChanges"/> option.
|
||||
|
/// </summary>
|
||||
|
/// <param name="unitOfWork">A unit of work object</param>
|
||||
|
/// <returns>
|
||||
|
/// A disposable object. Dispose it to restore the setting back to its previous state.
|
||||
|
/// </returns>
|
||||
|
public static IDisposable EnableUpdateAggregateRootWhenNavigationChanges([NotNull] this IUnitOfWork unitOfWork) |
||||
|
{ |
||||
|
return SetUpdateAggregateRootWhenNavigationChanges(unitOfWork, true); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Returns the value set for the given unit of work, or null when it was not set.
|
||||
|
/// The <see cref="AbpEntityChangeOptions.UpdateAggregateRootWhenNavigationChanges"/> option is used when it is null.
|
||||
|
/// </summary>
|
||||
|
public static bool? GetUpdateAggregateRootWhenNavigationChangesOrNull([NotNull] this IUnitOfWork unitOfWork) |
||||
|
{ |
||||
|
Check.NotNull(unitOfWork, nameof(unitOfWork)); |
||||
|
|
||||
|
return unitOfWork.Items.TryGetValue(UpdateAggregateRootWhenNavigationChangesItemKey, out var value) |
||||
|
? value as bool? |
||||
|
: null; |
||||
|
} |
||||
|
|
||||
|
private static IDisposable SetUpdateAggregateRootWhenNavigationChanges(IUnitOfWork unitOfWork, bool value) |
||||
|
{ |
||||
|
Check.NotNull(unitOfWork, nameof(unitOfWork)); |
||||
|
|
||||
|
// Items of a child unit of work is the same instance with its parent,
|
||||
|
// so the previous value is restored on dispose to not leak into the outer unit of work.
|
||||
|
var previousValue = unitOfWork.GetUpdateAggregateRootWhenNavigationChangesOrNull(); |
||||
|
unitOfWork.Items[UpdateAggregateRootWhenNavigationChangesItemKey] = value; |
||||
|
|
||||
|
return new DisposeAction(() => |
||||
|
{ |
||||
|
if (previousValue == null) |
||||
|
{ |
||||
|
unitOfWork.Items.Remove(UpdateAggregateRootWhenNavigationChangesItemKey); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
unitOfWork.Items[UpdateAggregateRootWhenNavigationChangesItemKey] = previousValue.Value; |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,301 @@ |
|||||
|
using System; |
||||
|
using Microsoft.Extensions.DependencyInjection; |
||||
|
using System.Threading.Tasks; |
||||
|
using Shouldly; |
||||
|
using Volo.Abp.Domain.Entities.Events; |
||||
|
using Volo.Abp.Domain.Repositories; |
||||
|
using Volo.Abp.EventBus.Local; |
||||
|
using Volo.Abp.TestApp.Domain; |
||||
|
using Volo.Abp.Uow; |
||||
|
using Xunit; |
||||
|
|
||||
|
namespace Volo.Abp.EntityFrameworkCore.DomainEvents; |
||||
|
|
||||
|
public class UpdateAggregateRootWhenNavigationChanges_Tests : EntityFrameworkCoreTestBase |
||||
|
{ |
||||
|
private readonly IRepository<AppEntityWithForeignKeyOnly, Guid> _entityWithForeignKeyOnlyRepository; |
||||
|
private readonly IRepository<AppEntityWithForeignKeyOnlyChild, Guid> _childRepository; |
||||
|
private readonly IRepository<AppEntityWithNavigations, Guid> _entityWithNavigationsRepository; |
||||
|
private readonly IRepository<AppEntityWithNavigationsForeign, Guid> _entityWithNavigationsForeignRepository; |
||||
|
private readonly IUnitOfWorkManager _unitOfWorkManager; |
||||
|
private readonly ILocalEventBus _localEventBus; |
||||
|
|
||||
|
public UpdateAggregateRootWhenNavigationChanges_Tests() |
||||
|
{ |
||||
|
_entityWithForeignKeyOnlyRepository = GetRequiredService<IRepository<AppEntityWithForeignKeyOnly, Guid>>(); |
||||
|
_childRepository = GetRequiredService<IRepository<AppEntityWithForeignKeyOnlyChild, Guid>>(); |
||||
|
_entityWithNavigationsRepository = GetRequiredService<IRepository<AppEntityWithNavigations, Guid>>(); |
||||
|
_entityWithNavigationsForeignRepository = GetRequiredService<IRepository<AppEntityWithNavigationsForeign, Guid>>(); |
||||
|
_unitOfWorkManager = GetRequiredService<IUnitOfWorkManager>(); |
||||
|
_localEventBus = GetRequiredService<ILocalEventBus>(); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_Not_Update_Principal_Entity_Without_Navigation_Property() |
||||
|
{ |
||||
|
var principalId = Guid.NewGuid(); |
||||
|
|
||||
|
await WithUnitOfWorkAsync(async () => |
||||
|
{ |
||||
|
await _entityWithForeignKeyOnlyRepository.InsertAsync( |
||||
|
new AppEntityWithForeignKeyOnly(principalId, "Principal")); |
||||
|
}); |
||||
|
|
||||
|
var concurrencyStamp = (await _entityWithForeignKeyOnlyRepository.GetAsync(principalId)).ConcurrencyStamp; |
||||
|
|
||||
|
var principalUpdatedEventTriggered = false; |
||||
|
_localEventBus.Subscribe<EntityUpdatedEventData<AppEntityWithForeignKeyOnly>>(_ => |
||||
|
{ |
||||
|
principalUpdatedEventTriggered = true; |
||||
|
return Task.CompletedTask; |
||||
|
}); |
||||
|
|
||||
|
await WithUnitOfWorkAsync(async () => |
||||
|
{ |
||||
|
// The principal has to be tracked to be a candidate for the aggregate root update.
|
||||
|
await _entityWithForeignKeyOnlyRepository.GetAsync(principalId); |
||||
|
|
||||
|
await _childRepository.InsertAsync( |
||||
|
new AppEntityWithForeignKeyOnlyChild(Guid.NewGuid(), principalId, "Child")); |
||||
|
}); |
||||
|
|
||||
|
principalUpdatedEventTriggered.ShouldBeFalse(); |
||||
|
(await _entityWithForeignKeyOnlyRepository.GetAsync(principalId)).ConcurrencyStamp.ShouldBe(concurrencyStamp); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_Update_Aggregate_Root_When_Navigation_Changes_By_Default() |
||||
|
{ |
||||
|
var entityId = Guid.NewGuid(); |
||||
|
var foreignId = Guid.NewGuid(); |
||||
|
|
||||
|
await WithUnitOfWorkAsync(async () => |
||||
|
{ |
||||
|
await _entityWithNavigationsForeignRepository.InsertAsync( |
||||
|
new AppEntityWithNavigationsForeign(foreignId, "Foreign")); |
||||
|
await _entityWithNavigationsRepository.InsertAsync( |
||||
|
new AppEntityWithNavigations(entityId, "Entity")); |
||||
|
}); |
||||
|
|
||||
|
var concurrencyStamp = (await _entityWithNavigationsForeignRepository.GetAsync(foreignId)).ConcurrencyStamp; |
||||
|
|
||||
|
var foreignUpdatedEventTriggered = false; |
||||
|
_localEventBus.Subscribe<EntityUpdatedEventData<AppEntityWithNavigationsForeign>>(_ => |
||||
|
{ |
||||
|
foreignUpdatedEventTriggered = true; |
||||
|
return Task.CompletedTask; |
||||
|
}); |
||||
|
|
||||
|
await WithUnitOfWorkAsync(async () => |
||||
|
{ |
||||
|
await _entityWithNavigationsForeignRepository.GetAsync(foreignId); |
||||
|
|
||||
|
var entity = await _entityWithNavigationsRepository.GetAsync(entityId); |
||||
|
entity.AppEntityWithNavigationForeignId = foreignId; |
||||
|
await _entityWithNavigationsRepository.UpdateAsync(entity); |
||||
|
}); |
||||
|
|
||||
|
foreignUpdatedEventTriggered.ShouldBeTrue(); |
||||
|
(await _entityWithNavigationsForeignRepository.GetAsync(foreignId)).ConcurrencyStamp.ShouldNotBe(concurrencyStamp); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_Not_Update_Aggregate_Root_But_Still_Publish_Event_When_Disabled_For_The_Unit_Of_Work() |
||||
|
{ |
||||
|
var entityId = Guid.NewGuid(); |
||||
|
var foreignId = Guid.NewGuid(); |
||||
|
|
||||
|
await WithUnitOfWorkAsync(async () => |
||||
|
{ |
||||
|
await _entityWithNavigationsForeignRepository.InsertAsync( |
||||
|
new AppEntityWithNavigationsForeign(foreignId, "Foreign")); |
||||
|
await _entityWithNavigationsRepository.InsertAsync( |
||||
|
new AppEntityWithNavigations(entityId, "Entity")); |
||||
|
}); |
||||
|
|
||||
|
var concurrencyStamp = (await _entityWithNavigationsForeignRepository.GetAsync(foreignId)).ConcurrencyStamp; |
||||
|
|
||||
|
var foreignUpdatedEventTriggered = false; |
||||
|
_localEventBus.Subscribe<EntityUpdatedEventData<AppEntityWithNavigationsForeign>>(_ => |
||||
|
{ |
||||
|
foreignUpdatedEventTriggered = true; |
||||
|
return Task.CompletedTask; |
||||
|
}); |
||||
|
|
||||
|
using (var uow = _unitOfWorkManager.Begin(requiresNew: true)) |
||||
|
{ |
||||
|
using (uow.DisableUpdateAggregateRootWhenNavigationChanges()) |
||||
|
{ |
||||
|
await _entityWithNavigationsForeignRepository.GetAsync(foreignId); |
||||
|
|
||||
|
var entity = await _entityWithNavigationsRepository.GetAsync(entityId); |
||||
|
entity.AppEntityWithNavigationForeignId = foreignId; |
||||
|
await _entityWithNavigationsRepository.UpdateAsync(entity); |
||||
|
|
||||
|
await uow.CompleteAsync(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
foreignUpdatedEventTriggered.ShouldBeTrue(); |
||||
|
(await _entityWithNavigationsForeignRepository.GetAsync(foreignId)).ConcurrencyStamp.ShouldBe(concurrencyStamp); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_Update_Aggregate_Root_Of_Its_Own_Changes_While_Disabled() |
||||
|
{ |
||||
|
var entityId = Guid.NewGuid(); |
||||
|
var foreignId = Guid.NewGuid(); |
||||
|
|
||||
|
await WithUnitOfWorkAsync(async () => |
||||
|
{ |
||||
|
await _entityWithNavigationsForeignRepository.InsertAsync( |
||||
|
new AppEntityWithNavigationsForeign(foreignId, "Foreign")); |
||||
|
await _entityWithNavigationsRepository.InsertAsync( |
||||
|
new AppEntityWithNavigations(entityId, "Entity")); |
||||
|
}); |
||||
|
|
||||
|
var concurrencyStamp = (await _entityWithNavigationsForeignRepository.GetAsync(foreignId)).ConcurrencyStamp; |
||||
|
|
||||
|
using (var uow = _unitOfWorkManager.Begin(requiresNew: true)) |
||||
|
{ |
||||
|
using (uow.DisableUpdateAggregateRootWhenNavigationChanges()) |
||||
|
{ |
||||
|
// The foreign entity is changed by itself, not by a navigation change.
|
||||
|
var foreign = await _entityWithNavigationsForeignRepository.GetAsync(foreignId); |
||||
|
foreign.Name = "Foreign-Updated"; |
||||
|
await _entityWithNavigationsForeignRepository.UpdateAsync(foreign); |
||||
|
|
||||
|
await uow.CompleteAsync(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
var updatedForeign = await _entityWithNavigationsForeignRepository.GetAsync(foreignId); |
||||
|
updatedForeign.Name.ShouldBe("Foreign-Updated"); |
||||
|
updatedForeign.ConcurrencyStamp.ShouldNotBe(concurrencyStamp); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_Use_The_Current_Value_On_Each_Save_Changes_Of_The_Same_Unit_Of_Work() |
||||
|
{ |
||||
|
var firstEntityId = Guid.NewGuid(); |
||||
|
var secondEntityId = Guid.NewGuid(); |
||||
|
var foreignId = Guid.NewGuid(); |
||||
|
|
||||
|
await WithUnitOfWorkAsync(async () => |
||||
|
{ |
||||
|
await _entityWithNavigationsForeignRepository.InsertAsync( |
||||
|
new AppEntityWithNavigationsForeign(foreignId, "Foreign")); |
||||
|
await _entityWithNavigationsRepository.InsertAsync( |
||||
|
new AppEntityWithNavigations(firstEntityId, "Entity1")); |
||||
|
await _entityWithNavigationsRepository.InsertAsync( |
||||
|
new AppEntityWithNavigations(secondEntityId, "Entity2")); |
||||
|
}); |
||||
|
|
||||
|
var concurrencyStamp = (await _entityWithNavigationsForeignRepository.GetAsync(foreignId)).ConcurrencyStamp; |
||||
|
|
||||
|
using (var uow = _unitOfWorkManager.Begin(requiresNew: true)) |
||||
|
{ |
||||
|
using (uow.DisableUpdateAggregateRootWhenNavigationChanges()) |
||||
|
{ |
||||
|
await _entityWithNavigationsForeignRepository.GetAsync(foreignId); |
||||
|
|
||||
|
var firstEntity = await _entityWithNavigationsRepository.GetAsync(firstEntityId); |
||||
|
firstEntity.AppEntityWithNavigationForeignId = foreignId; |
||||
|
await _entityWithNavigationsRepository.UpdateAsync(firstEntity, autoSave: true); |
||||
|
} |
||||
|
|
||||
|
(await _entityWithNavigationsForeignRepository.GetAsync(foreignId)).ConcurrencyStamp.ShouldBe(concurrencyStamp); |
||||
|
|
||||
|
// The setting is restored, the second save has to update the aggregate root.
|
||||
|
var secondEntity = await _entityWithNavigationsRepository.GetAsync(secondEntityId); |
||||
|
secondEntity.AppEntityWithNavigationForeignId = foreignId; |
||||
|
await _entityWithNavigationsRepository.UpdateAsync(secondEntity, autoSave: true); |
||||
|
|
||||
|
await uow.CompleteAsync(); |
||||
|
} |
||||
|
|
||||
|
(await _entityWithNavigationsForeignRepository.GetAsync(foreignId)).ConcurrencyStamp.ShouldNotBe(concurrencyStamp); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_Restore_The_Previous_Value_On_Dispose() |
||||
|
{ |
||||
|
using (var uow = _unitOfWorkManager.Begin(requiresNew: true)) |
||||
|
{ |
||||
|
uow.GetUpdateAggregateRootWhenNavigationChangesOrNull().ShouldBeNull(); |
||||
|
|
||||
|
using (uow.DisableUpdateAggregateRootWhenNavigationChanges()) |
||||
|
{ |
||||
|
uow.GetUpdateAggregateRootWhenNavigationChangesOrNull().ShouldBe(false); |
||||
|
|
||||
|
using (uow.EnableUpdateAggregateRootWhenNavigationChanges()) |
||||
|
{ |
||||
|
uow.GetUpdateAggregateRootWhenNavigationChangesOrNull().ShouldBe(true); |
||||
|
} |
||||
|
|
||||
|
uow.GetUpdateAggregateRootWhenNavigationChangesOrNull().ShouldBe(false); |
||||
|
} |
||||
|
|
||||
|
uow.GetUpdateAggregateRootWhenNavigationChangesOrNull().ShouldBeNull(); |
||||
|
|
||||
|
await uow.CompleteAsync(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public class UpdateAggregateRootWhenNavigationChanges_Globally_Disabled_Tests : EntityFrameworkCoreTestBase |
||||
|
{ |
||||
|
private readonly IRepository<AppEntityWithNavigations, Guid> _entityWithNavigationsRepository; |
||||
|
private readonly IRepository<AppEntityWithNavigationsForeign, Guid> _entityWithNavigationsForeignRepository; |
||||
|
private readonly IUnitOfWorkManager _unitOfWorkManager; |
||||
|
|
||||
|
public UpdateAggregateRootWhenNavigationChanges_Globally_Disabled_Tests() |
||||
|
{ |
||||
|
_entityWithNavigationsRepository = GetRequiredService<IRepository<AppEntityWithNavigations, Guid>>(); |
||||
|
_entityWithNavigationsForeignRepository = GetRequiredService<IRepository<AppEntityWithNavigationsForeign, Guid>>(); |
||||
|
_unitOfWorkManager = GetRequiredService<IUnitOfWorkManager>(); |
||||
|
} |
||||
|
|
||||
|
protected override void AfterAddApplication(IServiceCollection services) |
||||
|
{ |
||||
|
services.Configure<AbpEntityChangeOptions>(options => |
||||
|
{ |
||||
|
options.UpdateAggregateRootWhenNavigationChanges = false; |
||||
|
}); |
||||
|
|
||||
|
base.AfterAddApplication(services); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_Update_Aggregate_Root_When_Enabled_For_The_Unit_Of_Work() |
||||
|
{ |
||||
|
var entityId = Guid.NewGuid(); |
||||
|
var foreignId = Guid.NewGuid(); |
||||
|
|
||||
|
await WithUnitOfWorkAsync(async () => |
||||
|
{ |
||||
|
await _entityWithNavigationsForeignRepository.InsertAsync( |
||||
|
new AppEntityWithNavigationsForeign(foreignId, "Foreign")); |
||||
|
await _entityWithNavigationsRepository.InsertAsync( |
||||
|
new AppEntityWithNavigations(entityId, "Entity")); |
||||
|
}); |
||||
|
|
||||
|
var concurrencyStamp = (await _entityWithNavigationsForeignRepository.GetAsync(foreignId)).ConcurrencyStamp; |
||||
|
|
||||
|
using (var uow = _unitOfWorkManager.Begin(requiresNew: true)) |
||||
|
{ |
||||
|
using (uow.EnableUpdateAggregateRootWhenNavigationChanges()) |
||||
|
{ |
||||
|
await _entityWithNavigationsForeignRepository.GetAsync(foreignId); |
||||
|
|
||||
|
var entity = await _entityWithNavigationsRepository.GetAsync(entityId); |
||||
|
entity.AppEntityWithNavigationForeignId = foreignId; |
||||
|
await _entityWithNavigationsRepository.UpdateAsync(entity); |
||||
|
|
||||
|
await uow.CompleteAsync(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
(await _entityWithNavigationsForeignRepository.GetAsync(foreignId)).ConcurrencyStamp.ShouldNotBe(concurrencyStamp); |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue