diff --git a/docs/en/Audit-Logging.md b/docs/en/Audit-Logging.md index 610efc0451..799b3b76c3 100644 --- a/docs/en/Audit-Logging.md +++ b/docs/en/Audit-Logging.md @@ -39,6 +39,7 @@ Here, a list of the options you can configure: * `IsEnabled` (default: `true`): A root switch to enable or disable the auditing system. Other options is not used if this value is `false`. * `HideErrors` (default: `true`): Audit log system hides and write regular [logs](Logging.md) if any error occurs while saving the audit log objects. If saving the audit logs is critical for your system, set this to `false` to throw exception in case of hiding the errors. * `IsEnabledForAnonymousUsers` (default: `true`): If you want to write audit logs only for the authenticated users, set this to `false`. If you save audit logs for anonymous users, you will see `null` for `UserId` values for these users. +* `AlwaysLogOnException` (default: `true`): If you set to true, it always saves the audit log on an exception/error case without checking other options (except `IsEnabled`, which completely disables the audit logging). * `IsEnabledForGetRequests` (default: `false`): HTTP GET requests should not make any change in the database normally and audit log system doesn't save audit log objects for GET request. Set this to `true` to enable it also for the GET requests. * `ApplicationName`: If multiple applications saving audit logs into a single database, set this property to your application name, so you can distinguish the logs of different applications. * `IgnoredTypes`: A list of `Type`s to be ignored for audit logging. If this is an entity type, changes for this type of entities will not be saved. This list is also used while serializing the action parameters. @@ -369,4 +370,4 @@ You can call other services, they may call others, they may change entities and The Audit Logging Module basically implements the `IAuditingStore` to save the audit log objects to a database. It supports multiple database providers. This module is added to the startup templates by default. -See [the Audit Logging Module document](Modules/Audit-Logging.md) for more about it. \ No newline at end of file +See [the Audit Logging Module document](Modules/Audit-Logging.md) for more about it. diff --git a/docs/en/Caching.md b/docs/en/Caching.md index 8aa9f00657..bb11b2b45a 100644 --- a/docs/en/Caching.md +++ b/docs/en/Caching.md @@ -2,6 +2,32 @@ ABP framework extends ASP.NET Core's distributed caching system. +## Volo.Abp.Caching Package + +> This package is already installed by default with the startup template. So, most of the time, you don't need to install it manually. + +Volo.Abp.Caching is the core package of the caching system. Install it to your project using the package manager console (PMC): + +``` +Install-Package Volo.Abp.Caching +``` + +Then you can add **AbpCachingModule** dependency to your module: + +```c# +using Volo.Abp.Modularity; +using Volo.Abp.Caching; + +namespace MyCompany.MyProject +{ + [DependsOn(typeof(AbpCachingModule))] + public class MyModule : AbpModule + { + //... + } +} +``` + ## `IDistributedCache` Interface ASP.NET Core defines the `IDistributedCache` interface to get/set cache values. But it has some difficulties: 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 cce1e9c18f..a33c5f56ec 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 @@ -112,26 +112,5 @@ 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 b908827a98..3ade403017 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 @@ -18,6 +18,7 @@ namespace Volo.Abp.Domain.Repositories 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 9a2bc147af..ef44d6830d 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,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Linq; using System.Linq.Expressions; using System.Threading; using System.Threading.Tasks; @@ -44,31 +43,55 @@ namespace Volo.Abp.Domain.Repositories } } - public static async Task HardDeleteAsync(this IRepository repository, TEntity entity) - where TEntity : class, IEntity, ISoftDelete + public static async Task HardDeleteAsync( + this IBasicRepository repository, + TEntity entity, + bool autoSave = false, + CancellationToken cancellationToken = default + ) + where TEntity : class, IEntity, ISoftDelete { - var repo = ProxyHelper.UnProxy(repository) as IRepository; - if (repo != null) + if (!(ProxyHelper.UnProxy(repository) is IUnitOfWorkManagerAccessor unitOfWorkManagerAccessor)) { - 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; + throw new AbpException($"The given repository (of type {repository.GetType().AssemblyQualifiedName}) should implement the {typeof(IUnitOfWorkManagerAccessor).AssemblyQualifiedName} interface in order to invoke the {nameof(HardDeleteAsync)} method!"); + } - var hardDeleteKey = EntityHelper.GetHardDeleteKey(entity, baseRepository.CurrentTenant?.Id?.ToString()); - hardDeleteEntities.Add(hardDeleteKey); + var uowManager = unitOfWorkManagerAccessor.UnitOfWorkManager; + if (uowManager == null) + { + throw new AbpException($"{nameof(unitOfWorkManagerAccessor.UnitOfWorkManager)} property of the given {nameof(repository)} object is null!"); + } - await repo.DeleteAsync(entity); + if (uowManager.Current == null) + { + using (var uow = uowManager.Begin()) + { + await HardDeleteWithUnitOfWorkAsync(repository, entity, autoSave, cancellationToken, uowManager.Current); + await uow.CompleteAsync(cancellationToken); + } } - } - public static async Task HardDeleteAsync(this IRepository repository, Expression> predicate) - where TEntity : class, IEntity, ISoftDelete - { - foreach (var entity in repository.Where(predicate).ToList()) + else { - await repository.HardDeleteAsync(entity); + await HardDeleteWithUnitOfWorkAsync(repository, entity, autoSave, cancellationToken, uowManager.Current); } } + + private static async Task HardDeleteWithUnitOfWorkAsync( + IBasicRepository repository, + TEntity entity, + bool autoSave, + CancellationToken cancellationToken, IUnitOfWork currentUow + ) + where TEntity : class, IEntity, ISoftDelete + { + var hardDeleteEntities = (HashSet) currentUow.Items.GetOrAdd( + UnitOfWorkItemNames.HardDeletedEntities, + () => new HashSet() + ); + + hardDeleteEntities.Add(entity); + + await repository.DeleteAsync(entity, autoSave, cancellationToken); + } } } 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 deleted file mode 100644 index a95aca42c0..0000000000 --- a/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Repositories/UnitOfWorkExtensionDataTypes.cs +++ /dev/null @@ -1,11 +0,0 @@ -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.Ddd.Domain/Volo/Abp/Domain/Repositories/UnitOfWorkItemNames.cs b/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Repositories/UnitOfWorkItemNames.cs new file mode 100644 index 0000000000..4ec067ceec --- /dev/null +++ b/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Repositories/UnitOfWorkItemNames.cs @@ -0,0 +1,7 @@ +namespace Volo.Abp.Domain.Repositories +{ + public static class UnitOfWorkItemNames + { + public const string HardDeletedEntities = "AbpHardDeletedEntities"; + } +} diff --git a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/Domain/Repositories/EntityFrameworkCore/EfCoreRepository.cs b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/Domain/Repositories/EntityFrameworkCore/EfCoreRepository.cs index 6c3eb689d8..106e00f77a 100644 --- a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/Domain/Repositories/EntityFrameworkCore/EfCoreRepository.cs +++ b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/Domain/Repositories/EntityFrameworkCore/EfCoreRepository.cs @@ -80,7 +80,7 @@ namespace Volo.Abp.Domain.Repositories.EntityFrameworkCore { return includeDetails ? await WithDetails().ToListAsync(GetCancellationToken(cancellationToken)) - : await DbSet.ToListAsync(GetCancellationToken(cancellationToken)); + : await DbSet.ToListAsync(GetCancellationToken(cancellationToken)); } public override async Task GetCountAsync(CancellationToken cancellationToken = default) @@ -208,7 +208,7 @@ namespace Volo.Abp.Domain.Repositories.EntityFrameworkCore { return includeDetails ? await WithDetails().FirstOrDefaultAsync(e => e.Id.Equals(id), GetCancellationToken(cancellationToken)) - : await DbSet.FindAsync(new object[] { id }, GetCancellationToken(cancellationToken)); + : await DbSet.FindAsync(new object[] {id}, GetCancellationToken(cancellationToken)); } public virtual async Task DeleteAsync(TKey id, bool autoSave = false, CancellationToken cancellationToken = default) 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 4e2f4c0d51..ba9cee70ee 100644 --- a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/AbpDbContext.cs +++ b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/AbpDbContext.cs @@ -50,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; } @@ -199,37 +200,24 @@ namespace Volo.Abp.EntityFrameworkCore protected virtual void ApplyAbpConceptsForDeletedEntity(EntityEntry entry, EntityChangeReport changeReport) { - if (IsHardDeleteEntity(entry)) + if (TryCancelDeletionForSoftDelete(entry)) { - changeReport.ChangedEntities.Add(new EntityChangeEntry(entry.Entity, EntityChangeType.Deleted)); - return; + UpdateConcurrencyStamp(entry); + SetDeletionAuditProperties(entry); } - CancelDeletionForSoftDelete(entry); - UpdateConcurrencyStamp(entry); - SetDeletionAuditProperties(entry); + changeReport.ChangedEntities.Add(new EntityChangeEntry(entry.Entity, EntityChangeType.Deleted)); } - protected virtual bool IsHardDeleteEntity(EntityEntry entry) + protected virtual bool IsHardDeleted(EntityEntry entry) { - if (UnitOfWorkManager?.Current?.Items == null) - { - return false; - } - - if (!UnitOfWorkManager.Current.Items.ContainsKey(UnitOfWorkExtensionDataTypes.HardDelete)) + var hardDeletedEntities = UnitOfWorkManager?.Current?.Items.GetOrDefault(UnitOfWorkItemNames.HardDeletedEntities) as HashSet; + if (hardDeletedEntities == null) { 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); + return hardDeletedEntities.Contains(entry.Entity); } protected virtual void AddDomainEvents(EntityChangeReport changeReport, object entityAsObj) @@ -283,16 +271,22 @@ namespace Volo.Abp.EntityFrameworkCore entity.ConcurrencyStamp = Guid.NewGuid().ToString("N"); } - protected virtual void CancelDeletionForSoftDelete(EntityEntry entry) + protected virtual bool TryCancelDeletionForSoftDelete(EntityEntry entry) { if (!(entry.Entity is ISoftDelete)) { - return; + return false; + } + + if (IsHardDeleted(entry)) + { + return false; } entry.Reload(); entry.State = EntityState.Modified; entry.Entity.As().IsDeleted = true; + return true; } protected virtual void CheckAndSetId(EntityEntry entry) 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 b827292702..3333f1d505 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 && !IsHardDeleteEntity(entity)) + if (entity is ISoftDelete softDeleteEntity && !IsHardDeleted(entity)) { softDeleteEntity.IsDeleted = true; var result = await Collection.ReplaceOneAsync( @@ -175,32 +175,21 @@ namespace Volo.Abp.Domain.Repositories.MongoDB Collection.AsQueryable() ); } - protected virtual bool IsHardDeleteEntity(TEntity entry) + protected virtual bool IsHardDeleted(TEntity entity) { - if (UnitOfWorkManager?.Current?.Items == null) + var hardDeletedEntities = UnitOfWorkManager?.Current?.Items.GetOrDefault(UnitOfWorkItemNames.HardDeletedEntities) as HashSet; + if (hardDeletedEntities == 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); + return hardDeletedEntities.Contains(entity); } protected virtual FilterDefinition CreateEntityFilter(TEntity entity, bool withConcurrencyStamp = false, string concurrencyStamp = null) { throw new NotImplementedException( - $"{nameof(CreateEntityFilter)} is not implemented for MongoDB by default. It should be overrided and implemented by the deriving class!" + $"{nameof(CreateEntityFilter)} is not implemented for MongoDB by default. It should be overriden and implemented by the deriving class!" ); } 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 97fd81372b..ee5baa58bc 100644 --- a/framework/src/Volo.Abp.Uow/Volo/Abp/Uow/IUnitOfWork.cs +++ b/framework/src/Volo.Abp.Uow/Volo/Abp/Uow/IUnitOfWork.cs @@ -9,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 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 c494bfdfe6..05f49df180 100644 --- a/framework/src/Volo.Abp.Uow/Volo/Abp/Uow/UnitOfWork.cs +++ b/framework/src/Volo.Abp.Uow/Volo/Abp/Uow/UnitOfWork.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Threading; using System.Threading.Tasks; +using JetBrains.Annotations; using Microsoft.Extensions.Options; using Volo.Abp.DependencyInjection; @@ -31,6 +32,7 @@ namespace Volo.Abp.Uow public IServiceProvider ServiceProvider { get; } + [NotNull] public Dictionary Items { get; } private readonly Dictionary _databaseApis; @@ -48,6 +50,7 @@ namespace Volo.Abp.Uow _databaseApis = new Dictionary(); _transactionApis = new Dictionary(); + Items = new Dictionary(); } @@ -320,10 +323,5 @@ 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.TestApp/Volo/Abp/TestApp/Testing/HardDelete_Tests.cs b/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Testing/HardDelete_Tests.cs index 9d0430e968..1eac79fd7c 100644 --- 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 @@ -1,7 +1,5 @@ using Shouldly; using System; -using System.Collections.Generic; -using System.Text; using System.Threading.Tasks; using Volo.Abp.Data; using Volo.Abp.Domain.Repositories; @@ -15,58 +13,57 @@ namespace Volo.Abp.TestApp.Testing public abstract class HardDelete_Tests : TestAppTestBase where TStartupModule : IAbpModule { - protected readonly IRepository _personRepository; + protected readonly IRepository PersonRepository; protected readonly IDataFilter DataFilter; - protected readonly IUnitOfWorkManager _unitOfWorkManager; - public HardDelete_Tests() + protected readonly IUnitOfWorkManager UnitOfWorkManager; + + protected HardDelete_Tests() { - _personRepository = GetRequiredService>(); + PersonRepository = GetRequiredService>(); DataFilter = GetRequiredService(); - _unitOfWorkManager = GetRequiredService(); + UnitOfWorkManager = GetRequiredService(); } + [Fact] - public async Task Should_HardDelete_Entity_With_Collection() + public async Task Should_HardDelete_Entities() { - 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 douglas = await PersonRepository.GetAsync(TestDataBuilder.UserDouglasId); + await PersonRepository.HardDeleteAsync(douglas); - var deletedDougles = await _personRepository.FindAsync(TestDataBuilder.UserDouglasId); - deletedDougles.ShouldBeNull(); - } + douglas = await PersonRepository.FindAsync(TestDataBuilder.UserDouglasId); + douglas.ShouldBeNull(); } + [Fact] public async Task Should_HardDelete_Soft_Deleted_Entities() { - var douglas = await _personRepository.GetAsync(TestDataBuilder.UserDouglasId); - await _personRepository.DeleteAsync(douglas); + var douglas = await PersonRepository.GetAsync(TestDataBuilder.UserDouglasId); + await PersonRepository.DeleteAsync(douglas); - douglas = await _personRepository.FindAsync(TestDataBuilder.UserDouglasId); + douglas = await PersonRepository.FindAsync(TestDataBuilder.UserDouglasId); douglas.ShouldBeNull(); using (DataFilter.Disable()) { - douglas = await _personRepository.FindAsync(TestDataBuilder.UserDouglasId); + douglas = await PersonRepository.FindAsync(TestDataBuilder.UserDouglasId); douglas.ShouldNotBeNull(); douglas.IsDeleted.ShouldBeTrue(); douglas.DeletionTime.ShouldNotBeNull(); } - using (var uow = _unitOfWorkManager.Begin()) + + 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(); + douglas = await PersonRepository.GetAsync(TestDataBuilder.UserDouglasId); } + + await PersonRepository.HardDeleteAsync(douglas); + await uow.CompleteAsync(); } + + douglas = await PersonRepository.FindAsync(TestDataBuilder.UserDouglasId); + douglas.ShouldBeNull(); } } }