Browse Source

Merge remote-tracking branch 'abpframework/dev' into Translate

pull/2834/head
liangshiw 7 years ago
parent
commit
ccc5b45b33
  1. 216
      docs/en/AspNet-Boilerplate-Migration-Guide.md
  2. 3
      docs/en/Features.md
  3. 3
      docs/en/Modules/Feature-Management.md
  4. 1
      docs/en/Modules/Index.md
  5. 4
      docs/en/docs-nav.json
  6. 28
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/LoginCommand.cs
  7. 30
      framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/EntityHelper.cs
  8. 4
      framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Repositories/RepositoryBase.cs
  9. 29
      framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Repositories/RepositoryExtensions.cs
  10. 11
      framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Repositories/UnitOfWorkExtensionDataTypes.cs
  11. 34
      framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/AbpDbContext.cs
  12. 1
      framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Local/ILocalEventHandler.cs
  13. 23
      framework/src/Volo.Abp.MongoDB/Volo/Abp/Domain/Repositories/MongoDB/MongoDbRepository.cs
  14. 3
      framework/src/Volo.Abp.Uow/Volo/Abp/Uow/ChildUnitOfWork.cs
  15. 2
      framework/src/Volo.Abp.Uow/Volo/Abp/Uow/IUnitOfWork.cs
  16. 7
      framework/src/Volo.Abp.Uow/Volo/Abp/Uow/IUnitOfWorkManagerAccessor.cs
  17. 8
      framework/src/Volo.Abp.Uow/Volo/Abp/Uow/UnitOfWork.cs
  18. 8
      framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/DataFiltering/HardDelete_Tests.cs
  19. 11
      framework/test/Volo.Abp.MongoDB.Tests/Volo/Abp/MongoDB/DataFiltering/HardDelete_Tests.cs
  20. 11
      framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Domain/Phone.cs
  21. 72
      framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Testing/HardDelete_Tests.cs

216
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<T>` 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<User>(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<CreateUserInput, User>(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<UpdateUserInput, User>(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<T>` 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<T>` service to get a localized text. Example:
````csharp
public class MyService
{
private readonly IStringLocalizer<TestResource> _localizer;
public MyService(IStringLocalizer<TestResource> 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<IAuthorizationService>();
var l = context.ServiceProvider
.GetRequiredService<IStringLocalizer<AbpTenantManagementResource>>();
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.
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.

3
docs/en/Features.md

@ -0,0 +1,3 @@
# Features
TODO

3
docs/en/Modules/Feature-Management.md

@ -0,0 +1,3 @@
# Feature Management Module
TODO

1
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.

4
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"

28
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 <username>");
sb.AppendLine(" abp login <username> -p <password>");
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";
}
}
}
}

30
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
/// </summary>
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<Func<TEntity, bool>>(lambdaBody, lambdaParam);
}
public static void TrySetId<TKey>(
IEntity<TKey> entity,
Func<TKey> idFactory,
IEntity<TKey> entity,
Func<TKey> 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);
}
}
}

4
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<TEntity> : BasicRepositoryBase<TEntity>, IRepository<TEntity>
public abstract class RepositoryBase<TEntity> : BasicRepositoryBase<TEntity>, IRepository<TEntity>, 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;

29
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<TEntity, TPrimaryKey>(this IRepository<TEntity, TPrimaryKey> repository, TEntity entity)
where TEntity : class, IEntity<TPrimaryKey>, ISoftDelete
{
var repo = ProxyHelper.UnProxy(repository) as IRepository<TEntity, TPrimaryKey>;
if (repo != null)
{
var uow = ((IUnitOfWorkManagerAccessor)repo).UnitOfWorkManager;
var baseRepository = ((RepositoryBase<TEntity>)repo);
var items = ((IUnitOfWorkManagerAccessor)repo).UnitOfWorkManager.Current.Items;
var hardDeleteEntities = items.GetOrAdd(UnitOfWorkExtensionDataTypes.HardDelete, () => new HashSet<string>()) as HashSet<string>;
var hardDeleteKey = EntityHelper.GetHardDeleteKey(entity, baseRepository.CurrentTenant?.Id?.ToString());
hardDeleteEntities.Add(hardDeleteKey);
await repo.DeleteAsync(entity);
}
}
public static async Task HardDeleteAsync<TEntity, TPrimaryKey>(this IRepository<TEntity, TPrimaryKey> repository, Expression<Func<TEntity, bool>> predicate)
where TEntity : class, IEntity<TPrimaryKey>, ISoftDelete
{
foreach (var entity in repository.Where(predicate).ToList())
{
await repository.HardDeleteAsync(entity);
}
}
}
}

11
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";
}
}

34
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<int> 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<string> 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<TEntity>().Property(x => ((IEntity<Guid>) x).Id);
var idPropertyBuilder = modelBuilder.Entity<TEntity>().Property(x => ((IEntity<Guid>)x).Id);
if (idPropertyBuilder.Metadata.PropertyInfo.IsDefined(typeof(DatabaseGeneratedAttribute), true))
{
return;

1
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<in TEvent> : IEventHandler
{
/// <summary>

23
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<string> objects))
{
return false;
}
string hardDeleteKey = EntityHelper.GetHardDeleteKey(entry, CurrentTenant?.Id.ToString());
return objects.Contains(hardDeleteKey);
}
protected virtual FilterDefinition<TEntity> CreateEntityFilter(TEntity entity, bool withConcurrencyStamp = false, string concurrencyStamp = null)
{

3
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<string, object> Items => _parent.Items;
private readonly IUnitOfWork _parent;
public ChildUnitOfWork([NotNull] IUnitOfWork parent)

2
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<string, object> Items { get; }
//TODO: Switch to OnFailed (sync) and OnDisposed (sync) methods to be compatible with OnCompleted
event EventHandler<UnitOfWorkFailedEventArgs> Failed;

7
framework/src/Volo.Abp.Uow/Volo/Abp/Uow/IUnitOfWorkManagerAccessor.cs

@ -0,0 +1,7 @@
namespace Volo.Abp.Uow
{
public interface IUnitOfWorkManagerAccessor
{
IUnitOfWorkManager UnitOfWorkManager { get; }
}
}

8
framework/src/Volo.Abp.Uow/Volo/Abp/Uow/UnitOfWork.cs

@ -31,6 +31,8 @@ namespace Volo.Abp.Uow
public IServiceProvider ServiceProvider { get; }
public Dictionary<string, object> Items { get; }
private readonly Dictionary<string, IDatabaseApi> _databaseApis;
private readonly Dictionary<string, ITransactionApi> _transactionApis;
private readonly AbpUnitOfWorkDefaultOptions _defaultOptions;
@ -46,6 +48,7 @@ namespace Volo.Abp.Uow
_databaseApis = new Dictionary<string, IDatabaseApi>();
_transactionApis = new Dictionary<string, ITransactionApi>();
Items = new Dictionary<string, object>();
}
public virtual void Initialize(AbpUnitOfWorkOptions options)
@ -317,5 +320,10 @@ namespace Volo.Abp.Uow
{
return $"[UnitOfWork {Id}]";
}
public Dictionary<string, object> GetHardDeleteItems()
{
return Items;
}
}
}

8
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<AbpEntityFrameworkCoreTestModule>
{
}
}

11
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<AbpMongoDbTestModule>
{
}
}

11
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<Guid>
{
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 };
}
}
}

72
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<TStartupModule> : TestAppTestBase<TStartupModule>
where TStartupModule : IAbpModule
{
protected readonly IRepository<Person, Guid> _personRepository;
protected readonly IDataFilter DataFilter;
protected readonly IUnitOfWorkManager _unitOfWorkManager;
public HardDelete_Tests()
{
_personRepository = GetRequiredService<IRepository<Person, Guid>>();
DataFilter = GetRequiredService<IDataFilter>();
_unitOfWorkManager = GetRequiredService<IUnitOfWorkManager>();
}
[Fact]
public async Task Should_HardDelete_Entity_With_Collection()
{
using (var uow = _unitOfWorkManager.Begin())
{
using (DataFilter.Disable<ISoftDelete>())
{
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<ISoftDelete>())
{
douglas = await _personRepository.FindAsync(TestDataBuilder.UserDouglasId);
douglas.ShouldNotBeNull();
douglas.IsDeleted.ShouldBeTrue();
douglas.DeletionTime.ShouldNotBeNull();
}
using (var uow = _unitOfWorkManager.Begin())
{
using (DataFilter.Disable<ISoftDelete>())
{
douglas = await _personRepository.GetAsync(TestDataBuilder.UserDouglasId);
await _personRepository.HardDeleteAsync(douglas);
await uow.CompleteAsync();
var deletedDougles = await _personRepository.FindAsync(TestDataBuilder.UserDouglasId);
deletedDougles.ShouldBeNull();
}
}
}
}
}
Loading…
Cancel
Save