diff --git a/docs/en/AspNet-Boilerplate-Migration-Guide.md b/docs/en/AspNet-Boilerplate-Migration-Guide.md index 0a2b3b17b3..d47b266839 100644 --- a/docs/en/AspNet-Boilerplate-Migration-Guide.md +++ b/docs/en/AspNet-Boilerplate-Migration-Guide.md @@ -38,6 +38,10 @@ We also suggest you to compare the features of two products based on your needs. If you have an ASP.NET Zero based solution and want to migrate to the ABP Commercial, this guide will also help you. +### ASP.NET MVC 5.x Projects + +The ABP Framework doesn't support ASP.NET MVC 5.x, it only works with ASP.NET Core. So, if you migrate your ASP.NET MVC 5.x based projects, you will also deal with the .NET Core migration. + ## The Migration Progress We've designed the ABP Framework by **getting the best parts** of the ASP.NET Boilerplate framework, so it will be familiar to you if you've developed ASP.NET Boilerplate based applications. @@ -488,6 +492,85 @@ public class TaskAppService : ITaskAppService You inject the `ILogger` instead of the `ILogger`. +### Object to Object Mapping + +#### IObjectMapper Service + +ASP.NET Boilerplate defines an `IObjectMapper` service ([see](https://aspnetboilerplate.com/Pages/Documents/Object-To-Object-Mapping)) and has an integration to the [AutoMapper](https://automapper.org/) library. + +Example usage: Create a `User` object with the given `CreateUserInput` object: + +````csharp +public void CreateUser(CreateUserInput input) +{ + var user = ObjectMapper.Map(input); + ... +} +```` + +Example: Update an existing `User` properties with the given `UpdateUserInput` object: + +````csharp +public async Task UpdateUserAsync(Guid id, UpdateUserInput input) +{ + var user = await _userRepository.GetAsync(id); + ObjectMapper.Map(input, user); +} +```` + +ABP Framework has the same `IObjectMapper` service ([see](Object-To-Object-Mapping.md)) and the AutoMapper integration with a slightly different mapping methods. + +Example usage: Create a `User` object with the given `CreateUserInput` object: + +````csharp +public void CreateUser(CreateUserInput input) +{ + var user = ObjectMapper.Map(input); +} +```` + +This time you need to explicitly declare the source type and target type (while ASP.NET Boilerplate was requiring only the target type). + +Example: Update an existing `User` properties with the given `UpdateUserInput` object: + +````csharp +public async Task UpdateUserAsync(Guid id, UpdateUserInput input) +{ + var user = await _userRepository.GetAsync(id); + ObjectMapper.Map(input, user); +} +```` + +Again, ABP Framework expects to explicitly set the source and target types. + +#### AutoMapper Integration + +##### Auto Mapping Attributes + +ASP.NET Boilerplate has `AutoMapTo`, `AutoMapFrom` and `AutoMap` attributes to automatically create mappings for the declared types. Example: + +````csharp +[AutoMapTo(typeof(User))] +public class CreateUserInput +{ + public string Name { get; set; } + public string Surname { get; set; } + ... +} +```` + +ABP Framework has no such attributes, because AutoMapper as a [similar attribute](https://automapper.readthedocs.io/en/latest/Attribute-mapping.html) now. You need to switch to AutoMapper's attribute. + +##### Mapping Definitions + +ABP Framework follows AutoMapper principles closely. You can define classes derived from the `Profile` class to define your mappings. + +##### Configuration Validation + +Configuration validation is a best practice for the AutoMapper to maintain your mapping configuration in a safe way. + +See [the documentation](Object-To-Object-Mapping.md) for more information related to the object mapping. + ### Setting Management #### Defining the Settings @@ -514,11 +597,138 @@ ASP.NET Boilerplate has a static `Clock` service ([see](https://aspnetboilerplat ABP Framework has the `IClock` service ([see](Clock.md)) which has a similar goal, but now you need to inject it whenever you need it. +### Event Bus + +ASP.NET Boilerplate has an in-process event bus system. You typically inject the `IEventBus` (or use the static instance `EventBus.Default`) to trigger an event. It automatically triggers events for entity changes (like `EntityCreatingEventData` and `EntityUpdatedEventData`). You create a class by implementing the `IEventHandler` interface. + +ABP Framework separates the event bus into two services: `ILocalEventBus` and `IDistributedEventBus`. + +The local event bus is similar to the event bus of the ASP.NET Boilerplate while the distributed event bus is new feature introduced in the ABP Framework. + +So, to migrate your code; + +* Use the `ILocalEventBus` instead of the `IEventBus`. +* Implement the `ILocalEventHandler` instead of the `IEventHandler`. + +> Note that ABP Framework has also an `IEventBus` interface, but it does exists to be a common interface for the local and distributed event bus. It is not injected and directly used. + +### Feature Management + +Feature system is used in multi-tenant applications to define features of your application check if given feature is available for the current tenant. + +#### Defining Features + +In the ASP.NET Boilerplate ([see](https://aspnetboilerplate.com/Pages/Documents/Feature-Management)), you create a class inheriting from the `FeatureProvider`, override the `SetFeatures` method and add your class to the `Configuration.Features.Providers` list. + +In the ABP Framework ([see](Features.md)), you derive your class from the `FeatureDefinitionProvider` and override the `Define` method. No need to add your class to the configuration, it is automatically discovered by the framework. + +#### Checking Features + +You can continue to use the `RequiresFeature` attribute and `IFeatureChecker` service to check if a feature is enabled for the current tenant. + +#### Changing the Feature Values + +In the ABP Framework you use the `IFeatureManager` to change a feature value for a tenant. + +### Audit Logging + +The ASP.NET Boilerplate ([see](https://aspnetboilerplate.com/Pages/Documents/Audit-Logging)) and the ABP Framework ([see](Audit-Logging.md)) has similar audit logging systems. ABP Framework requires to add `UseAuditing()` middleware to the ASP.NET Core pipeline, which is already added in the startup templates. So, most of the times it will be work out of the box. + +### Localization + +ASP.NET Boilerplate supports XML and JSON files to define the localization key-values for the UI ([see](https://aspnetboilerplate.com/Pages/Documents/Localization)). ABP Framework only supports the JSON formatter localization files ([see](Localization.md)). So, you need to convert your XML file to JSON. + +The ASP.NET Boilerplate has its own the `ILocalizationManager` service to be injected and used for the localization in the server side. + +The ABP Framework uses [Microsoft localization extension](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/localization) library, so it is completely integrated to ASP.NET Core. You use the `IStringLocalizer` service to get a localized text. Example: + +````csharp +public class MyService +{ + private readonly IStringLocalizer _localizer; + + public MyService(IStringLocalizer localizer) + { + _localizer = localizer; + } + + public void Foo() + { + var str = _localizer["HelloWorld"]; //Get a localized text + } +} +```` + +So, you need to replace `ILocalizationManager` usage by the `IStringLocalizer`. + +It also provides API used in the client side: + +````js +var testResource = abp.localization.getResource('Test'); +var str = testResource('HelloWorld'); +```` + +It was like `abp.localization.localize(...)` in the ASP.NET Boilerplate. + +### Navigation vs Menu + +In ASP.NET you create a class deriving from the `NavigationProvider` to define your menu elements. Menu items has `requiredPermissionName` attributes to restrict access to a menu element. Menu items were static and your class is executed only one time. + +Int the ABP Framework you need to create a class implements the `IMenuContributor` interface. Your class is executed whenever the menu needs to be rendered. So, you can conditionally add menu items. + +As an example, this is the menu contributor of the tenant management module: + +````csharp +public class AbpTenantManagementWebMainMenuContributor : IMenuContributor +{ + public async Task ConfigureMenuAsync(MenuConfigurationContext context) + { + //Add items only to the main menu + if (context.Menu.Name != StandardMenus.Main) + { + return; + } + + //Get the standard administration menu item + var administrationMenu = context.Menu.GetAdministration(); + + //Resolve some needed services from the DI container + var authorizationService = context.ServiceProvider + .GetRequiredService(); + var l = context.ServiceProvider + .GetRequiredService>(); + + var tenantManagementMenuItem = new ApplicationMenuItem( + TenantManagementMenuNames.GroupName, + l["Menu:TenantManagement"], + icon: "fa fa-users"); + + administrationMenu.AddItem(tenantManagementMenuItem); + + //Conditionally add the "Tenants" menu item based on the permission + if (await authorizationService + .IsGrantedAsync(TenantManagementPermissions.Tenants.Default)) + { + tenantManagementMenuItem.AddItem( + new ApplicationMenuItem( + TenantManagementMenuNames.Tenants, + l["Tenants"], + url: "/TenantManagement/Tenants")); + } + } +} +```` + +So, you need to check permission using the `IAuthorizationService` if you want to show a menu item only when the user has the related permission. + +> Navigation/Menu system is only for ASP.NET Core MVC / Razor Pages applications. Angular applications has a different system implemented in the startup templates. + ## Missing Features -The following features are not present for the ABP Framework. Here, a list of major missing features (and the related issue for that feature waiting on the ABP Framework GitHub repository): +The following features are not present for the ABP Framework. Here, a list of some major missing features (and the related issue for that feature waiting on the ABP Framework GitHub repository): * [Multi-Lingual Entities](https://aspnetboilerplate.com/Pages/Documents/Multi-Lingual-Entities) ([#1754](https://github.com/abpframework/abp/issues/1754)) -* ...TODO +* [Real time notification system](https://aspnetboilerplate.com/Pages/Documents/Notification-System) ([#633](https://github.com/abpframework/abp/issues/633)) +* [NHibernate Integration](https://aspnetboilerplate.com/Pages/Documents/NHibernate-Integration) ([#339](https://github.com/abpframework/abp/issues/339)) - We don't intent to work on this, but any community contribution welcome. -Most of these features will eventually be implemented. However, you can implement them yourself if they are important for you. If you want, you can [contribute](Contribution/Index.md) to the framework by implementing these yourself. \ No newline at end of file +Some of these features will eventually be implemented. However, you can implement them yourself if they are important for you. If you want, you can [contribute](Contribution/Index.md) to the framework, it is appreciated. \ No newline at end of file diff --git a/docs/en/Features.md b/docs/en/Features.md new file mode 100644 index 0000000000..4b31ca16e9 --- /dev/null +++ b/docs/en/Features.md @@ -0,0 +1,3 @@ +# Features + +TODO \ No newline at end of file diff --git a/docs/en/Modules/Feature-Management.md b/docs/en/Modules/Feature-Management.md new file mode 100644 index 0000000000..5c41277dd8 --- /dev/null +++ b/docs/en/Modules/Feature-Management.md @@ -0,0 +1,3 @@ +# Feature Management Module + +TODO \ No newline at end of file diff --git a/docs/en/Modules/Index.md b/docs/en/Modules/Index.md index 812f22e8dd..2ffb509667 100644 --- a/docs/en/Modules/Index.md +++ b/docs/en/Modules/Index.md @@ -16,6 +16,7 @@ There are some **free and open source** application modules developed and mainta * **Background Jobs**: Persist background jobs when using the default background job manager. * **Blogging**: Used to create fancy blogs. ABP's [own blog](https://blog.abp.io/) already using this module. * [**Docs**](Docs.md): Used to create technical documentation pages. ABP's [own documentation](https://docs.abp.io) already using this module. +* **Feature Management**: Used to persist and manage the [features](../Features.md). * **Identity**: Manages roles, users and their permissions, based on the Microsoft Identity library. * **IdentityServer**: Integrates to IdentityServer4. * **Permission Management**: Used to persist permissions. diff --git a/docs/en/docs-nav.json b/docs/en/docs-nav.json index eb4ef13b8d..cbb4c80e72 100644 --- a/docs/en/docs-nav.json +++ b/docs/en/docs-nav.json @@ -49,6 +49,10 @@ } ] }, + { + "text": "ASP.NET Boilerplate Migration Guide", + "path": "AspNet-Boilerplate-Migration-Guide.md" + }, { "text": "CLI", "path": "CLI.md" diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/LoginCommand.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/LoginCommand.cs index fe9ed2d98f..32bc6671bf 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/LoginCommand.cs +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/LoginCommand.cs @@ -33,15 +33,19 @@ namespace Volo.Abp.Cli.Commands ); } - Console.Write("Password: "); - var password = ConsoleHelper.ReadSecret(); - if (password.IsNullOrWhiteSpace()) + var password = commandLineArgs.Options.GetOrNull(Options.Password.Short, Options.Password.Long); + if (password == null) { - throw new CliUsageException( - "Password is missing!" + - Environment.NewLine + Environment.NewLine + - GetUsageInfo() - ); + Console.Write("Password: "); + password = ConsoleHelper.ReadSecret(); + if (password.IsNullOrWhiteSpace()) + { + throw new CliUsageException( + "Password is missing!" + + Environment.NewLine + Environment.NewLine + + GetUsageInfo() + ); + } } await AuthService.LoginAsync( @@ -60,10 +64,12 @@ namespace Volo.Abp.Cli.Commands sb.AppendLine(""); sb.AppendLine("Usage:"); sb.AppendLine(" abp login "); + sb.AppendLine(" abp login -p "); sb.AppendLine(""); sb.AppendLine("Example:"); sb.AppendLine(""); sb.AppendLine(" abp login john"); + sb.AppendLine(" abp login john -p 1234"); sb.AppendLine(""); sb.AppendLine("See the documentation for more info: https://docs.abp.io/en/abp/latest/CLI"); @@ -82,6 +88,12 @@ namespace Volo.Abp.Cli.Commands public const string Short = "o"; public const string Long = "organization"; } + + public static class Password + { + public const string Short = "p"; + public const string Long = "password"; + } } } } \ No newline at end of file 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..9a2bc147af 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,32 @@ 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); + } + } } } 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.EventBus/Volo/Abp/EventBus/Local/ILocalEventHandler.cs b/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Local/ILocalEventHandler.cs index e3501294be..006097825c 100644 --- a/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Local/ILocalEventHandler.cs +++ b/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Local/ILocalEventHandler.cs @@ -2,6 +2,7 @@ using System.Threading.Tasks; namespace Volo.Abp.EventBus { + //TODO: Move to the right namespace in v3.0 public interface ILocalEventHandler : IEventHandler { /// 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(); + } + } + } + } +}