diff --git a/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/EntityHelper.cs b/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/EntityHelper.cs index 92ebdd4568..cce1e9c18f 100644 --- a/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/EntityHelper.cs +++ b/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/EntityHelper.cs @@ -3,6 +3,8 @@ using System.Collections.Generic; using System.Linq.Expressions; using System.Reflection; using JetBrains.Annotations; +using Volo.Abp.MultiTenancy; +using Volo.Abp.Reflection; namespace Volo.Abp.Domain.Entities { @@ -11,6 +13,7 @@ namespace Volo.Abp.Domain.Entities /// public static class EntityHelper { + public static bool IsEntity([NotNull] Type type) { return typeof(IEntity).IsAssignableFrom(type); @@ -82,10 +85,10 @@ namespace Volo.Abp.Domain.Entities var lambdaBody = Expression.Equal(leftExpression, rightExpression); return Expression.Lambda>(lambdaBody, lambdaParam); } - + public static void TrySetId( - IEntity entity, - Func idFactory, + IEntity entity, + Func idFactory, bool checkForDisableGuidGenerationAttribute = false) { //TODO: Can be optimized (by caching per entity type)? @@ -109,5 +112,26 @@ namespace Volo.Abp.Domain.Entities idProperty.SetValue(entity, idFactory()); } + + public static object GetEntityId(object entity) + { + if (!IsEntity(entity.GetType())) + { + throw new AbpException(entity.GetType() + " is not an Entity !"); + } + + return ReflectionHelper.GetValueByPath(entity, entity.GetType(), "Id"); + } + public static string GetHardDeleteKey(object entity, string tenantId) + { + //if (entity is IMultiTenant) // IsMultiTenantEntity + if (typeof(IMultiTenant).IsAssignableFrom(entity.GetType())) + { + var tenantIdString = !string.IsNullOrEmpty(tenantId) ? tenantId : "null"; + return entity.GetType().FullName + ";TenantId=" + tenantIdString + ";Id=" + GetEntityId(entity); + } + + return entity.GetType().FullName + ";Id=" + GetEntityId(entity); + } } } diff --git a/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Repositories/RepositoryBase.cs b/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Repositories/RepositoryBase.cs index 29814f4de6..b908827a98 100644 --- a/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Repositories/RepositoryBase.cs +++ b/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Repositories/RepositoryBase.cs @@ -8,15 +8,17 @@ using System.Threading.Tasks; using Volo.Abp.Data; using Volo.Abp.Domain.Entities; using Volo.Abp.MultiTenancy; +using Volo.Abp.Uow; namespace Volo.Abp.Domain.Repositories { - public abstract class RepositoryBase : BasicRepositoryBase, IRepository + public abstract class RepositoryBase : BasicRepositoryBase, IRepository, IUnitOfWorkManagerAccessor where TEntity : class, IEntity { public IDataFilter DataFilter { get; set; } public ICurrentTenant CurrentTenant { get; set; } + public IUnitOfWorkManager UnitOfWorkManager { get; set; } public virtual Type ElementType => GetQueryable().ElementType; diff --git a/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Repositories/RepositoryExtensions.cs b/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Repositories/RepositoryExtensions.cs index 40ebca015e..92ebf2e74a 100644 --- a/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Repositories/RepositoryExtensions.cs +++ b/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Repositories/RepositoryExtensions.cs @@ -1,10 +1,12 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Linq.Expressions; using System.Threading; using System.Threading.Tasks; using Volo.Abp.Domain.Entities; using Volo.Abp.DynamicProxy; +using Volo.Abp.Uow; namespace Volo.Abp.Domain.Repositories { @@ -41,5 +43,59 @@ namespace Volo.Abp.Domain.Repositories await repo.EnsurePropertyLoadedAsync(entity, propertyExpression, cancellationToken); } } + + public static async Task HardDeleteAsync(this IRepository repository, TEntity entity) + where TEntity : class, IEntity, ISoftDelete + { + var repo = ProxyHelper.UnProxy(repository) as IRepository; + if (repo != null) + { + var uow = ((IUnitOfWorkManagerAccessor)repo).UnitOfWorkManager; + var baseRepository = ((RepositoryBase)repo); + + var items = ((IUnitOfWorkManagerAccessor)repo).UnitOfWorkManager.Current.Items; + var hardDeleteEntities = items.GetOrAdd(UnitOfWorkExtensionDataTypes.HardDelete, () => new HashSet()) as HashSet; + + var hardDeleteKey = EntityHelper.GetHardDeleteKey(entity, baseRepository.CurrentTenant?.Id?.ToString()); + hardDeleteEntities.Add(hardDeleteKey); + + await repo.DeleteAsync(entity); + } + } + public static async Task HardDeleteAsync(this IRepository repository, Expression> predicate) + where TEntity : class, IEntity, ISoftDelete + { + foreach (var entity in repository.Where(predicate).ToList()) + { + await repository.HardDeleteAsync(entity); + } + } + public static void HardDelete(this IRepository repository, TEntity entity) + where TEntity : class, IEntity, ISoftDelete + { + var repo = ProxyHelper.UnProxy(repository) as IRepository; + if (repo != null) + { + var uow = ((IUnitOfWorkManagerAccessor)repo).UnitOfWorkManager; + var baseRepository = ((RepositoryBase)repo); + + var items = ((IUnitOfWorkManagerAccessor)repo).UnitOfWorkManager.Current.Items; + var hardDeleteEntities = items.GetOrAdd(UnitOfWorkExtensionDataTypes.HardDelete, () => new HashSet()) as HashSet; + + var hardDeleteKey = EntityHelper.GetHardDeleteKey(entity, baseRepository.CurrentTenant?.Id?.ToString()); + hardDeleteEntities.Add(hardDeleteKey); + + Task.FromResult(repo.DeleteAsync(entity)); + } + } + public static void HardDelete(this IRepository repository, Expression> predicate) + where TEntity : class, IEntity, ISoftDelete + { + foreach (var entity in repository.Where(predicate).ToList()) + { + repository.HardDelete(entity); + } + } + } } diff --git a/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Repositories/UnitOfWorkExtensionDataTypes.cs b/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Repositories/UnitOfWorkExtensionDataTypes.cs new file mode 100644 index 0000000000..a95aca42c0 --- /dev/null +++ b/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Repositories/UnitOfWorkExtensionDataTypes.cs @@ -0,0 +1,11 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Volo.Abp.Domain.Repositories +{ + public class UnitOfWorkExtensionDataTypes + { + public static string HardDelete { get; } = "HardDelete"; + } +} diff --git a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/AbpDbContext.cs b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/AbpDbContext.cs index 16c5c38389..4e2f4c0d51 100644 --- a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/AbpDbContext.cs +++ b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/AbpDbContext.cs @@ -16,6 +16,7 @@ using Volo.Abp.Data; using Volo.Abp.DependencyInjection; using Volo.Abp.Domain.Entities; using Volo.Abp.Domain.Entities.Events; +using Volo.Abp.Domain.Repositories; using Volo.Abp.EntityFrameworkCore.EntityHistory; using Volo.Abp.EntityFrameworkCore.Modeling; using Volo.Abp.EntityFrameworkCore.ValueConverters; @@ -23,6 +24,7 @@ using Volo.Abp.Guids; using Volo.Abp.MultiTenancy; using Volo.Abp.Reflection; using Volo.Abp.Timing; +using Volo.Abp.Uow; namespace Volo.Abp.EntityFrameworkCore { @@ -48,6 +50,7 @@ namespace Volo.Abp.EntityFrameworkCore public IEntityHistoryHelper EntityHistoryHelper { get; set; } public IAuditingManager AuditingManager { get; set; } + public IUnitOfWorkManager UnitOfWorkManager { get; set; } public IClock Clock { get; set; } @@ -102,7 +105,7 @@ namespace Volo.Abp.EntityFrameworkCore .Invoke(this, new object[] { modelBuilder, entityType }); } } - + public override async Task SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = default) { try @@ -196,12 +199,39 @@ namespace Volo.Abp.EntityFrameworkCore protected virtual void ApplyAbpConceptsForDeletedEntity(EntityEntry entry, EntityChangeReport changeReport) { + if (IsHardDeleteEntity(entry)) + { + changeReport.ChangedEntities.Add(new EntityChangeEntry(entry.Entity, EntityChangeType.Deleted)); + return; + } CancelDeletionForSoftDelete(entry); UpdateConcurrencyStamp(entry); SetDeletionAuditProperties(entry); changeReport.ChangedEntities.Add(new EntityChangeEntry(entry.Entity, EntityChangeType.Deleted)); } + protected virtual bool IsHardDeleteEntity(EntityEntry entry) + { + if (UnitOfWorkManager?.Current?.Items == null) + { + return false; + } + + if (!UnitOfWorkManager.Current.Items.ContainsKey(UnitOfWorkExtensionDataTypes.HardDelete)) + { + return false; + } + + var hardDeleteItems = UnitOfWorkManager.Current.Items[UnitOfWorkExtensionDataTypes.HardDelete]; + if (!(hardDeleteItems is HashSet objects)) + { + return false; + } + string hardDeleteKey = EntityHelper.GetHardDeleteKey(entry.Entity, CurrentTenantId?.ToString()); + + return objects.Contains(hardDeleteKey); + } + protected virtual void AddDomainEvents(EntityChangeReport changeReport, object entityAsObj) { var generatesDomainEventsEntity = entityAsObj as IGeneratesDomainEvents; @@ -382,7 +412,7 @@ namespace Volo.Abp.EntityFrameworkCore return; } - var idPropertyBuilder = modelBuilder.Entity().Property(x => ((IEntity) x).Id); + var idPropertyBuilder = modelBuilder.Entity().Property(x => ((IEntity)x).Id); if (idPropertyBuilder.Metadata.PropertyInfo.IsDefined(typeof(DatabaseGeneratedAttribute), true)) { return; diff --git a/framework/src/Volo.Abp.MongoDB/Volo/Abp/Domain/Repositories/MongoDB/MongoDbRepository.cs b/framework/src/Volo.Abp.MongoDB/Volo/Abp/Domain/Repositories/MongoDB/MongoDbRepository.cs index 4e8ccaa82e..b827292702 100644 --- a/framework/src/Volo.Abp.MongoDB/Volo/Abp/Domain/Repositories/MongoDB/MongoDbRepository.cs +++ b/framework/src/Volo.Abp.MongoDB/Volo/Abp/Domain/Repositories/MongoDB/MongoDbRepository.cs @@ -111,7 +111,7 @@ namespace Volo.Abp.Domain.Repositories.MongoDB await ApplyAbpConceptsForDeletedEntityAsync(entity); var oldConcurrencyStamp = SetNewConcurrencyStamp(entity); - if (entity is ISoftDelete softDeleteEntity) + if (entity is ISoftDelete softDeleteEntity && !IsHardDeleteEntity(entity)) { softDeleteEntity.IsDeleted = true; var result = await Collection.ReplaceOneAsync( @@ -175,6 +175,27 @@ namespace Volo.Abp.Domain.Repositories.MongoDB Collection.AsQueryable() ); } + protected virtual bool IsHardDeleteEntity(TEntity entry) + { + if (UnitOfWorkManager?.Current?.Items == null) + { + return false; + } + + if (!UnitOfWorkManager.Current.Items.ContainsKey(UnitOfWorkExtensionDataTypes.HardDelete)) + { + return false; + } + + var hardDeleteItems = UnitOfWorkManager.Current.Items[UnitOfWorkExtensionDataTypes.HardDelete]; + if (!(hardDeleteItems is HashSet objects)) + { + return false; + } + string hardDeleteKey = EntityHelper.GetHardDeleteKey(entry, CurrentTenant?.Id.ToString()); + + return objects.Contains(hardDeleteKey); + } protected virtual FilterDefinition CreateEntityFilter(TEntity entity, bool withConcurrencyStamp = false, string concurrencyStamp = null) { diff --git a/framework/src/Volo.Abp.Uow/Volo/Abp/Uow/ChildUnitOfWork.cs b/framework/src/Volo.Abp.Uow/Volo/Abp/Uow/ChildUnitOfWork.cs index d1d90b6f6f..882c8d6a70 100644 --- a/framework/src/Volo.Abp.Uow/Volo/Abp/Uow/ChildUnitOfWork.cs +++ b/framework/src/Volo.Abp.Uow/Volo/Abp/Uow/ChildUnitOfWork.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using JetBrains.Annotations; @@ -26,6 +27,8 @@ namespace Volo.Abp.Uow public IServiceProvider ServiceProvider => _parent.ServiceProvider; + public Dictionary Items => _parent.Items; + private readonly IUnitOfWork _parent; public ChildUnitOfWork([NotNull] IUnitOfWork parent) diff --git a/framework/src/Volo.Abp.Uow/Volo/Abp/Uow/IUnitOfWork.cs b/framework/src/Volo.Abp.Uow/Volo/Abp/Uow/IUnitOfWork.cs index 32ef781133..97fd81372b 100644 --- a/framework/src/Volo.Abp.Uow/Volo/Abp/Uow/IUnitOfWork.cs +++ b/framework/src/Volo.Abp.Uow/Volo/Abp/Uow/IUnitOfWork.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using JetBrains.Annotations; @@ -8,6 +9,7 @@ namespace Volo.Abp.Uow public interface IUnitOfWork : IDatabaseApiContainer, ITransactionApiContainer, IDisposable { Guid Id { get; } + Dictionary Items { get; } //TODO: Switch to OnFailed (sync) and OnDisposed (sync) methods to be compatible with OnCompleted event EventHandler Failed; diff --git a/framework/src/Volo.Abp.Uow/Volo/Abp/Uow/IUnitOfWorkManagerAccessor.cs b/framework/src/Volo.Abp.Uow/Volo/Abp/Uow/IUnitOfWorkManagerAccessor.cs new file mode 100644 index 0000000000..a12b77855e --- /dev/null +++ b/framework/src/Volo.Abp.Uow/Volo/Abp/Uow/IUnitOfWorkManagerAccessor.cs @@ -0,0 +1,7 @@ +namespace Volo.Abp.Uow +{ + public interface IUnitOfWorkManagerAccessor + { + IUnitOfWorkManager UnitOfWorkManager { get; } + } +} diff --git a/framework/src/Volo.Abp.Uow/Volo/Abp/Uow/UnitOfWork.cs b/framework/src/Volo.Abp.Uow/Volo/Abp/Uow/UnitOfWork.cs index e7e86774b5..c494bfdfe6 100644 --- a/framework/src/Volo.Abp.Uow/Volo/Abp/Uow/UnitOfWork.cs +++ b/framework/src/Volo.Abp.Uow/Volo/Abp/Uow/UnitOfWork.cs @@ -31,6 +31,8 @@ namespace Volo.Abp.Uow public IServiceProvider ServiceProvider { get; } + public Dictionary Items { get; } + private readonly Dictionary _databaseApis; private readonly Dictionary _transactionApis; private readonly AbpUnitOfWorkDefaultOptions _defaultOptions; @@ -46,6 +48,7 @@ namespace Volo.Abp.Uow _databaseApis = new Dictionary(); _transactionApis = new Dictionary(); + Items = new Dictionary(); } public virtual void Initialize(AbpUnitOfWorkOptions options) @@ -317,5 +320,10 @@ namespace Volo.Abp.Uow { return $"[UnitOfWork {Id}]"; } + + public Dictionary GetHardDeleteItems() + { + return Items; + } } } \ No newline at end of file diff --git a/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/DataFiltering/HardDelete_Tests.cs b/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/DataFiltering/HardDelete_Tests.cs new file mode 100644 index 0000000000..df3fd98b62 --- /dev/null +++ b/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/DataFiltering/HardDelete_Tests.cs @@ -0,0 +1,8 @@ +using Volo.Abp.TestApp.Testing; + +namespace Volo.Abp.EntityFrameworkCore.DataFiltering +{ + public class HardDelete_Tests : HardDelete_Tests + { + } +} diff --git a/framework/test/Volo.Abp.MongoDB.Tests/Volo/Abp/MongoDB/DataFiltering/HardDelete_Tests.cs b/framework/test/Volo.Abp.MongoDB.Tests/Volo/Abp/MongoDB/DataFiltering/HardDelete_Tests.cs new file mode 100644 index 0000000000..66c6e58b5c --- /dev/null +++ b/framework/test/Volo.Abp.MongoDB.Tests/Volo/Abp/MongoDB/DataFiltering/HardDelete_Tests.cs @@ -0,0 +1,11 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Volo.Abp.TestApp.Testing; + +namespace Volo.Abp.MongoDB.DataFiltering +{ + public class HardDelete_Tests : HardDelete_Tests + { + } +} diff --git a/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Domain/Phone.cs b/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Domain/Phone.cs index 20292e3de8..c5522cf53a 100644 --- a/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Domain/Phone.cs +++ b/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Domain/Phone.cs @@ -7,7 +7,7 @@ using Volo.Abp.Domain.Entities; namespace Volo.Abp.TestApp.Domain { [Table("AppPhones")] - public class Phone : Entity + public class Phone : Entity { public virtual Guid PersonId { get; set; } @@ -17,11 +17,12 @@ namespace Volo.Abp.TestApp.Domain private Phone() { - + } public Phone(Guid personId, string number, PhoneType type = PhoneType.Mobile) { + Id = Guid.NewGuid(); PersonId = personId; Number = number; Type = type; @@ -29,7 +30,7 @@ namespace Volo.Abp.TestApp.Domain public override object[] GetKeys() { - return new object[] {PersonId, Number}; + return new object[] { PersonId, Number }; } } @@ -45,7 +46,7 @@ namespace Volo.Abp.TestApp.Domain protected Order() { - + } public Order(Guid id, string referenceNo) @@ -104,7 +105,7 @@ namespace Volo.Abp.TestApp.Domain public override object[] GetKeys() { - return new object[] {OrderId, ProductId}; + return new object[] { OrderId, ProductId }; } } } \ No newline at end of file diff --git a/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Testing/HardDelete_Tests.cs b/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Testing/HardDelete_Tests.cs new file mode 100644 index 0000000000..9d0430e968 --- /dev/null +++ b/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Testing/HardDelete_Tests.cs @@ -0,0 +1,72 @@ +using Shouldly; +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; +using Volo.Abp.Data; +using Volo.Abp.Domain.Repositories; +using Volo.Abp.Modularity; +using Volo.Abp.TestApp.Domain; +using Volo.Abp.Uow; +using Xunit; + +namespace Volo.Abp.TestApp.Testing +{ + public abstract class HardDelete_Tests : TestAppTestBase + where TStartupModule : IAbpModule + { + protected readonly IRepository _personRepository; + protected readonly IDataFilter DataFilter; + protected readonly IUnitOfWorkManager _unitOfWorkManager; + public HardDelete_Tests() + { + _personRepository = GetRequiredService>(); + DataFilter = GetRequiredService(); + _unitOfWorkManager = GetRequiredService(); + } + [Fact] + public async Task Should_HardDelete_Entity_With_Collection() + { + using (var uow = _unitOfWorkManager.Begin()) + { + using (DataFilter.Disable()) + { + var douglas = await _personRepository.FindAsync(TestDataBuilder.UserDouglasId); + await _personRepository.HardDeleteAsync(x => x.Id == TestDataBuilder.UserDouglasId); + await uow.CompleteAsync(); + } + + var deletedDougles = await _personRepository.FindAsync(TestDataBuilder.UserDouglasId); + deletedDougles.ShouldBeNull(); + } + } + [Fact] + public async Task Should_HardDelete_Soft_Deleted_Entities() + { + var douglas = await _personRepository.GetAsync(TestDataBuilder.UserDouglasId); + await _personRepository.DeleteAsync(douglas); + + douglas = await _personRepository.FindAsync(TestDataBuilder.UserDouglasId); + douglas.ShouldBeNull(); + + using (DataFilter.Disable()) + { + douglas = await _personRepository.FindAsync(TestDataBuilder.UserDouglasId); + douglas.ShouldNotBeNull(); + douglas.IsDeleted.ShouldBeTrue(); + douglas.DeletionTime.ShouldNotBeNull(); + } + using (var uow = _unitOfWorkManager.Begin()) + { + using (DataFilter.Disable()) + { + douglas = await _personRepository.GetAsync(TestDataBuilder.UserDouglasId); + await _personRepository.HardDeleteAsync(douglas); + await uow.CompleteAsync(); + var deletedDougles = await _personRepository.FindAsync(TestDataBuilder.UserDouglasId); + deletedDougles.ShouldBeNull(); + } + } + } + } +}