Browse Source

Merge pull request #25938 from abpframework/auto-merge/rel-10-7/4755

Merge branch dev with rel-10.7
pull/25939/head
Volosoft Agent 1 week ago
committed by GitHub
parent
commit
ca0c892d80
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 18
      docs/en/release-info/migration-guides/abp-10-7.md
  2. 74
      framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/Events/EntityChangeUnitOfWorkExtensions.cs
  3. 12
      framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/AbpDbContext.cs
  4. 5
      framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/ChangeTrackers/AbpEfCoreNavigationHelper.cs
  5. 8
      framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/DomainEvents/DomainEvents_Tests.cs
  6. 394
      framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/DomainEvents/UpdateAggregateRootWhenNavigationChanges_Tests.cs
  7. 33
      framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/TestMigrationsDbContext.cs
  8. 33
      framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/TestApp/EntityFrameworkCore/TestAppDbContext.cs
  9. 83
      framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Domain/AppEntityWithNavigations.cs

18
docs/en/release-info/migration-guides/abp-10-7.md

@ -116,6 +116,24 @@ If you change transforming pipeline contributors or an encryption passphrase, re
> See the [BLOB Encryption](../../framework/infrastructure/blob-storing/encryption.md) and [BLOB Content Pipeline](../../framework/infrastructure/blob-storing/pipeline.md) documents and [#25836](https://github.com/abpframework/abp/pull/25836) for details.
### Aggregate Root Update on Foreign Key Only Relations
**Who is affected**
- Applications with a relation that has no navigation property on the principal side, such as `HasOne<T>().WithMany()`, where the principal entity is tracked while one of its dependents is added, modified or deleted.
**What changed**
- ABP treated any dependent change as a navigation change of the principal, so the principal was updated (`ConcurrencyStamp`, audit properties) and its entity updated event was published, even if the principal had no navigation for that relation. Concurrent changes on such dependents conflicted on the principal row and failed with `AbpDbConcurrencyException`.
- The principal is now updated only when it really has a navigation property for the changed relation. Relations with navigation properties keep their current behavior.
- `IUnitOfWork.DisableUpdateAggregateRootWhenNavigationChanges()` is added to disable updating the aggregate root for a unit of work, without disabling its entity updated event. It overrides the `AbpEntityChangeOptions.UpdateAggregateRootWhenNavigationChanges` option.
**What to do**
Add a navigation property to the principal entity if you rely on the previous behavior, or update the principal in your own code.
> See [#25937](https://github.com/abpframework/abp/pull/25937) for details.
### Dependency Updates
**Who is affected**

74
framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/Events/EntityChangeUnitOfWorkExtensions.cs

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

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

@ -284,7 +284,7 @@ public abstract class AbpDbContext<TDbContext> : DbContext, IAbpEfCoreDbContext,
continue;
}
if (EntityChangeOptions.Value.UpdateAggregateRootWhenNavigationChanges &&
if (IsUpdateAggregateRootWhenNavigationChangesEnabled() &&
EntityChangeOptions.Value.IgnoredUpdateAggregateRootSelectors.All(selector => !selector.Predicate(entityEntry.Entity.GetType())) &&
entityEntry.State == EntityState.Unchanged)
{
@ -467,7 +467,7 @@ public abstract class AbpDbContext<TDbContext> : DbContext, IAbpEfCoreDbContext,
EntityChangeOptions.Value.IgnoredNavigationEntitySelectors.All(selector => !selector.Predicate(entry.Entity.GetType())) &&
AbpEfCoreNavigationHelper.IsNavigationEntryModified(entry))
{
if (EntityChangeOptions.Value.UpdateAggregateRootWhenNavigationChanges &&
if (IsUpdateAggregateRootWhenNavigationChangesEnabled() &&
EntityChangeOptions.Value.IgnoredUpdateAggregateRootSelectors.All(selector => !selector.Predicate(entry.Entity.GetType())))
{
ApplyAbpConceptsForModifiedEntity(entry, true);
@ -524,6 +524,12 @@ public abstract class AbpDbContext<TDbContext> : DbContext, IAbpEfCoreDbContext,
(x.CurrentValue == null || x.OriginalValue?.ToString() == x.CurrentValue?.ToString()));
}
protected virtual bool IsUpdateAggregateRootWhenNavigationChangesEnabled()
{
return UnitOfWorkManager.Current?.GetUpdateAggregateRootWhenNavigationChangesOrNull() ??
EntityChangeOptions.Value.UpdateAggregateRootWhenNavigationChanges;
}
protected virtual void HandlePropertiesBeforeSave()
{
var entries = ChangeTracker.Entries().ToList();
@ -538,7 +544,7 @@ public abstract class AbpDbContext<TDbContext> : DbContext, IAbpEfCoreDbContext,
}
if (EntityChangeOptions.Value.PublishEntityUpdatedEventWhenNavigationChanges &&
EntityChangeOptions.Value.UpdateAggregateRootWhenNavigationChanges)
IsUpdateAggregateRootWhenNavigationChangesEnabled())
{
foreach (var entry in AbpEfCoreNavigationHelper.GetChangedEntityEntries()
.Where(x => x.State == EntityState.Unchanged)

5
framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/ChangeTrackers/AbpEfCoreNavigationHelper.cs

@ -83,7 +83,8 @@ public class AbpEfCoreNavigationHelper : ITransientDependency
abpEntityEntry.UpdateNavigation(entityEntry, navigationEntry);
}
if (!abpEntityEntry.IsModified && (!checkEntityEntryState || IsEntityEntryChanged(entityEntry)))
// A principal without a navigation to the dependent has no navigation change to report.
if (navigationEntry != null && !abpEntityEntry.IsModified && (!checkEntityEntryState || IsEntityEntryChanged(entityEntry)))
{
abpEntityEntry.IsModified = true;
DetectChanges(abpEntityEntry.EntityEntry, false);
@ -128,7 +129,7 @@ public class AbpEfCoreNavigationHelper : ITransientDependency
abpEntityEntry.UpdateNavigation(entityEntry, navigationEntry);
}
if (!abpEntityEntry.IsModified && (!checkEntityEntryState || IsEntityEntryChanged(entityEntry)))
if (navigationEntry != null && !abpEntityEntry.IsModified && (!checkEntityEntryState || IsEntityEntryChanged(entityEntry)))
{
abpEntityEntry.IsModified = true;
DetectChanges(abpEntityEntry.EntityEntry, false);

8
framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/DomainEvents/DomainEvents_Tests.cs

@ -569,11 +569,13 @@ public class AbpEfCoreDomainEvents_IgnoredUpdateAggregateRootSelectors_Test : A
{
protected override void AfterAddApplication(IServiceCollection services)
{
base.AfterAddApplication(services);
services.Configure<AbpEntityChangeOptions>(options =>
{
options.IgnoredUpdateAggregateRootSelectors.Add("AppEntityWithValueObjectAddress", x => x == typeof(AppEntityWithNavigations));
// The base class disables it for all entities, the selector has to be the only reason of the expected behavior.
options.UpdateAggregateRootWhenNavigationChanges = true;
options.IgnoredUpdateAggregateRootSelectors.Add("AppEntityWithNavigations", x => x == typeof(AppEntityWithNavigations));
});
base.AfterAddApplication(services);
}
}

394
framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/DomainEvents/UpdateAggregateRootWhenNavigationChanges_Tests.cs

@ -0,0 +1,394 @@
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<AppEntityWithForeignKeyOnlyOwner, Guid> _ownerRepository;
private readonly IRepository<AppEntityWithForeignKeyOnlyEntityChild, Guid> _entityChildRepository;
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>>();
_ownerRepository = GetRequiredService<IRepository<AppEntityWithForeignKeyOnlyOwner, Guid>>();
_entityChildRepository = GetRequiredService<IRepository<AppEntityWithForeignKeyOnlyEntityChild, 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_Not_Update_Principal_Entity_Without_Navigation_Property_On_Update_And_Delete()
{
var principalId = Guid.NewGuid();
var childId = Guid.NewGuid();
await WithUnitOfWorkAsync(async () =>
{
await _entityWithForeignKeyOnlyRepository.InsertAsync(
new AppEntityWithForeignKeyOnly(principalId, "Principal"));
await _childRepository.InsertAsync(
new AppEntityWithForeignKeyOnlyChild(childId, principalId, "Child"));
});
var concurrencyStamp = (await _entityWithForeignKeyOnlyRepository.GetAsync(principalId)).ConcurrencyStamp;
await WithUnitOfWorkAsync(async () =>
{
await _entityWithForeignKeyOnlyRepository.GetAsync(principalId);
var child = await _childRepository.GetAsync(childId);
child.Name = "Child-Updated";
await _childRepository.UpdateAsync(child);
});
(await _entityWithForeignKeyOnlyRepository.GetAsync(principalId)).ConcurrencyStamp.ShouldBe(concurrencyStamp);
await WithUnitOfWorkAsync(async () =>
{
await _entityWithForeignKeyOnlyRepository.GetAsync(principalId);
await _childRepository.DeleteAsync(childId);
});
(await _entityWithForeignKeyOnlyRepository.GetAsync(principalId)).ConcurrencyStamp.ShouldBe(concurrencyStamp);
}
[Fact]
public async Task Should_Update_The_Owner_But_Not_The_Referenced_Aggregate_Root_Of_A_Child_Entity()
{
var ownerId = Guid.NewGuid();
var referencedId = Guid.NewGuid();
await WithUnitOfWorkAsync(async () =>
{
await _ownerRepository.InsertAsync(new AppEntityWithForeignKeyOnlyOwner(ownerId, "Owner"));
await _entityWithForeignKeyOnlyRepository.InsertAsync(
new AppEntityWithForeignKeyOnly(referencedId, "Referenced"));
});
var ownerStamp = (await _ownerRepository.GetAsync(ownerId)).ConcurrencyStamp;
var referencedStamp = (await _entityWithForeignKeyOnlyRepository.GetAsync(referencedId)).ConcurrencyStamp;
await WithUnitOfWorkAsync(async () =>
{
await _ownerRepository.GetAsync(ownerId);
await _entityWithForeignKeyOnlyRepository.GetAsync(referencedId);
await _entityChildRepository.InsertAsync(
new AppEntityWithForeignKeyOnlyEntityChild(Guid.NewGuid(), ownerId, referencedId, "Child"));
});
(await _ownerRepository.GetAsync(ownerId)).ConcurrencyStamp.ShouldNotBe(ownerStamp);
(await _entityWithForeignKeyOnlyRepository.GetAsync(referencedId)).ConcurrencyStamp.ShouldBe(referencedStamp);
}
[Fact]
public async Task Should_Update_Aggregate_Root_When_Owned_Entity_Changes()
{
var entityId = Guid.NewGuid();
await WithUnitOfWorkAsync(async () =>
{
await _entityWithNavigationsRepository.InsertAsync(
new AppEntityWithNavigations(entityId, "Entity"));
});
var concurrencyStamp = (await _entityWithNavigationsRepository.GetAsync(entityId)).ConcurrencyStamp;
await WithUnitOfWorkAsync(async () =>
{
var entity = await _entityWithNavigationsRepository.GetAsync(entityId);
entity.AppEntityWithValueObjectAddress = new AppEntityWithValueObjectAddress("Turkey");
await _entityWithNavigationsRepository.UpdateAsync(entity);
});
(await _entityWithNavigationsRepository.GetAsync(entityId)).ConcurrencyStamp.ShouldNotBe(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);
}
}

33
framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/TestMigrationsDbContext.cs

@ -38,6 +38,14 @@ public class TestMigrationsDbContext : AbpDbContext<TestMigrationsDbContext>
public DbSet<AppEntityWithNavigationsForeign> AppEntityWithNavigationsForeign { get; set; }
public DbSet<AppEntityWithForeignKeyOnly> AppEntityWithForeignKeyOnly { get; set; }
public DbSet<AppEntityWithForeignKeyOnlyChild> AppEntityWithForeignKeyOnlyChild { get; set; }
public DbSet<AppEntityWithForeignKeyOnlyOwner> AppEntityWithForeignKeyOnlyOwner { get; set; }
public DbSet<AppEntityWithForeignKeyOnlyEntityChild> AppEntityWithForeignKeyOnlyEntityChild { get; set; }
public DbSet<Blog> Blogs { get; set; }
public DbSet<BlogPost> BlogPosts { get; set; }
@ -162,6 +170,31 @@ public class TestMigrationsDbContext : AbpDbContext<TestMigrationsDbContext>
b.ConfigureByConvention();
});
modelBuilder.Entity<AppEntityWithForeignKeyOnly>(b =>
{
b.ConfigureByConvention();
});
modelBuilder.Entity<AppEntityWithForeignKeyOnlyChild>(b =>
{
b.ConfigureByConvention();
// No navigation property on both sides, only a foreign key.
b.HasOne<AppEntityWithForeignKeyOnly>().WithMany().HasForeignKey(x => x.AppEntityWithForeignKeyOnlyId);
});
modelBuilder.Entity<AppEntityWithForeignKeyOnlyOwner>(b =>
{
b.ConfigureByConvention();
b.HasMany(x => x.Children).WithOne().HasForeignKey(x => x.OwnerId);
});
modelBuilder.Entity<AppEntityWithForeignKeyOnlyEntityChild>(b =>
{
b.ConfigureByConvention();
// The owner has a navigation, the referenced aggregate root has not.
b.HasOne<AppEntityWithForeignKeyOnly>().WithMany().HasForeignKey(x => x.AppEntityWithForeignKeyOnlyId);
});
modelBuilder.Entity<Blog>(b =>
{
b.ConfigureByConvention();

33
framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/TestApp/EntityFrameworkCore/TestAppDbContext.cs

@ -46,6 +46,14 @@ public class TestAppDbContext : AbpDbContext<TestAppDbContext>, IThirdDbContext,
public DbSet<AppEntityWithNavigationsForeign> AppEntityWithNavigationsForeign { get; set; }
public DbSet<AppEntityWithForeignKeyOnly> AppEntityWithForeignKeyOnly { get; set; }
public DbSet<AppEntityWithForeignKeyOnlyChild> AppEntityWithForeignKeyOnlyChild { get; set; }
public DbSet<AppEntityWithForeignKeyOnlyOwner> AppEntityWithForeignKeyOnlyOwner { get; set; }
public DbSet<AppEntityWithForeignKeyOnlyEntityChild> AppEntityWithForeignKeyOnlyEntityChild { get; set; }
public DbSet<Blog> Blogs { get; set; }
public DbSet<BlogPost> BlogPosts { get; set; }
@ -161,6 +169,31 @@ public class TestAppDbContext : AbpDbContext<TestAppDbContext>, IThirdDbContext,
b.HasMany(x => x.OneToMany).WithOne().HasForeignKey(x => x.AppEntityWithNavigationForeignId);
});
modelBuilder.Entity<AppEntityWithForeignKeyOnly>(b =>
{
b.ConfigureByConvention();
});
modelBuilder.Entity<AppEntityWithForeignKeyOnlyChild>(b =>
{
b.ConfigureByConvention();
// No navigation property on both sides, only a foreign key.
b.HasOne<AppEntityWithForeignKeyOnly>().WithMany().HasForeignKey(x => x.AppEntityWithForeignKeyOnlyId);
});
modelBuilder.Entity<AppEntityWithForeignKeyOnlyOwner>(b =>
{
b.ConfigureByConvention();
b.HasMany(x => x.Children).WithOne().HasForeignKey(x => x.OwnerId);
});
modelBuilder.Entity<AppEntityWithForeignKeyOnlyEntityChild>(b =>
{
b.ConfigureByConvention();
// The owner has a navigation, the referenced aggregate root has not.
b.HasOne<AppEntityWithForeignKeyOnly>().WithMany().HasForeignKey(x => x.AppEntityWithForeignKeyOnlyId);
});
modelBuilder.Entity<AppEntityWithNavigationChildOneToOne>(b =>
{
b.ConfigureByConvention();

83
framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Domain/AppEntityWithNavigations.cs

@ -121,3 +121,86 @@ public class AppEntityWithNavigationsForeign : AggregateRoot<Guid>
public virtual List<AppEntityWithNavigations> OneToMany { get; set; }
}
/// <summary>
/// Has no navigation property to <see cref="AppEntityWithForeignKeyOnlyChild"/>,
/// the relation is only a foreign key on the child.
/// </summary>
public class AppEntityWithForeignKeyOnly : AggregateRoot<Guid>
{
protected AppEntityWithForeignKeyOnly()
{
}
public AppEntityWithForeignKeyOnly(Guid id, string name)
: base(id)
{
Name = name;
}
public string Name { get; set; }
}
public class AppEntityWithForeignKeyOnlyChild : AggregateRoot<Guid>
{
protected AppEntityWithForeignKeyOnlyChild()
{
}
public AppEntityWithForeignKeyOnlyChild(Guid id, Guid appEntityWithForeignKeyOnlyId, string name)
: base(id)
{
AppEntityWithForeignKeyOnlyId = appEntityWithForeignKeyOnlyId;
Name = name;
}
public Guid AppEntityWithForeignKeyOnlyId { get; set; }
public string Name { get; set; }
}
public class AppEntityWithForeignKeyOnlyOwner : AggregateRoot<Guid>
{
protected AppEntityWithForeignKeyOnlyOwner()
{
}
public AppEntityWithForeignKeyOnlyOwner(Guid id, string name)
: base(id)
{
Name = name;
}
public string Name { get; set; }
public virtual List<AppEntityWithForeignKeyOnlyEntityChild> Children { get; set; }
}
/// <summary>
/// Belongs to the <see cref="AppEntityWithForeignKeyOnlyOwner"/> aggregate,
/// but references the <see cref="AppEntityWithForeignKeyOnly"/> aggregate root with a foreign key only.
/// </summary>
public class AppEntityWithForeignKeyOnlyEntityChild : Entity<Guid>
{
protected AppEntityWithForeignKeyOnlyEntityChild()
{
}
public AppEntityWithForeignKeyOnlyEntityChild(Guid id, Guid ownerId, Guid appEntityWithForeignKeyOnlyId, string name)
: base(id)
{
OwnerId = ownerId;
AppEntityWithForeignKeyOnlyId = appEntityWithForeignKeyOnlyId;
Name = name;
}
public Guid OwnerId { get; set; }
public Guid AppEntityWithForeignKeyOnlyId { get; set; }
public string Name { get; set; }
}

Loading…
Cancel
Save