mirror of https://github.com/abpframework/abp.git
Browse Source
- Replace FormattedStringValueExtracter.Extract with LastIndexOf in PermissionGrantCacheItem and ResourcePermissionGrantCacheItem to eliminate repeated string tokenization and object allocations on every cache key parse (~12,000 calls per request with 4000+ permissions) - Add fast-path in SimpleStateCheckerManager.InternalIsEnabledAsync to skip DI scope creation when both StateCheckers and GlobalStateCheckers are empty, avoiding thousands of unnecessary scope allocations - Optimize PermissionChecker.IsGrantedAsync(string[]) and ResourcePermissionChecker.IsGrantedAsync(string[], resourceName, resourceKey) to load all permission definitions once via GetPermissionsAsync / GetResourcePermissionsAsync instead of N individual GetOrNullAsync calls, and use batch StateCheckerManager.IsEnabledAsync for state checking - Optimize AbpApplicationConfigurationAppService.GetAuthConfigAsync to pre-load all permission names into a HashSet for O(1) lookup instead of N async GetOrNullAsync calls inside the loop - Fix GetResourcePermissionsAsync to deduplicate by (ResourceName, Name) instead of Name only, matching the actual uniqueness constraint of resource permissions defined in PermissionDefinitionContext Production impact (customer with 4000+ permissions): 10s+ -> ~682mspull/25231/head
13 changed files with 309 additions and 27 deletions
@ -0,0 +1,51 @@ |
|||
using System; |
|||
using System.Threading.Tasks; |
|||
using Shouldly; |
|||
using Volo.Abp.Authorization.Permissions; |
|||
using Xunit; |
|||
|
|||
namespace Volo.Abp.Authorization; |
|||
|
|||
public class PermissionChecker_BulkWithStateChecker_Tests : AuthorizationTestBase |
|||
{ |
|||
private readonly IPermissionChecker _permissionChecker; |
|||
|
|||
public PermissionChecker_BulkWithStateChecker_Tests() |
|||
{ |
|||
_permissionChecker = GetRequiredService<IPermissionChecker>(); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task IsGrantedAsync_With_Empty_Names_Should_Return_Empty_Result() |
|||
{ |
|||
var result = await _permissionChecker.IsGrantedAsync(Array.Empty<string>()); |
|||
result.Result.ShouldBeEmpty(); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task IsGrantedAsync_StateChecker_Permission_Is_Undefined_When_StateChecker_Fails() |
|||
{ |
|||
// MyPermission1 has TestRequireEditionPermissionSimpleStateChecker.
|
|||
// Current user (Douglas) has no EditionId claim → StateChecker returns false.
|
|||
// The permission must stay Undefined (never reaches the value-provider pipeline).
|
|||
var result = await _permissionChecker.IsGrantedAsync(new[] { "MyPermission1", "MyPermission3" }); |
|||
|
|||
result.Result["MyPermission1"].ShouldBe(PermissionGrantResult.Undefined); |
|||
result.Result["MyPermission3"].ShouldBe(PermissionGrantResult.Granted); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task IsGrantedAsync_Mix_Of_Defined_And_Undefined_Permissions() |
|||
{ |
|||
var result = await _permissionChecker.IsGrantedAsync(new[] |
|||
{ |
|||
"MyPermission3", |
|||
"NonExistentPermission", |
|||
"MyPermission5" |
|||
}); |
|||
|
|||
result.Result["MyPermission3"].ShouldBe(PermissionGrantResult.Granted); |
|||
result.Result["NonExistentPermission"].ShouldBe(PermissionGrantResult.Prohibited); |
|||
result.Result["MyPermission5"].ShouldBe(PermissionGrantResult.Granted); |
|||
} |
|||
} |
|||
@ -0,0 +1,57 @@ |
|||
using System; |
|||
using System.Threading.Tasks; |
|||
using Shouldly; |
|||
using Volo.Abp.Authorization.Permissions; |
|||
using Volo.Abp.Authorization.Permissions.Resources; |
|||
using Volo.Abp.Authorization.TestServices.Resources; |
|||
using Xunit; |
|||
|
|||
namespace Volo.Abp.Authorization; |
|||
|
|||
public class ResourcePermissionChecker_BulkWithStateChecker_Tests : AuthorizationTestBase |
|||
{ |
|||
private readonly IResourcePermissionChecker _resourcePermissionChecker; |
|||
|
|||
public ResourcePermissionChecker_BulkWithStateChecker_Tests() |
|||
{ |
|||
_resourcePermissionChecker = GetRequiredService<IResourcePermissionChecker>(); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task IsGrantedAsync_With_Empty_Names_Should_Return_Empty_Result() |
|||
{ |
|||
var result = await _resourcePermissionChecker.IsGrantedAsync( |
|||
Array.Empty<string>(), TestEntityResource.ResourceName, TestEntityResource.ResourceKey5); |
|||
|
|||
result.Result.ShouldBeEmpty(); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task IsGrantedAsync_StateChecker_Permission_Is_Undefined_When_StateChecker_Fails() |
|||
{ |
|||
// MyResourcePermission1 has TestRequireEditionPermissionSimpleStateChecker.
|
|||
// Current user (Douglas) has no EditionId claim → StateChecker returns false.
|
|||
// The permission must stay Undefined (never reaches the value-provider pipeline).
|
|||
var result = await _resourcePermissionChecker.IsGrantedAsync( |
|||
new[] { "MyResourcePermission1", "MyResourcePermission3" }, |
|||
TestEntityResource.ResourceName, |
|||
TestEntityResource.ResourceKey3); |
|||
|
|||
result.Result["MyResourcePermission1"].ShouldBe(PermissionGrantResult.Undefined); |
|||
result.Result["MyResourcePermission3"].ShouldBe(PermissionGrantResult.Granted); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task IsGrantedAsync_Mix_Of_Defined_And_Undefined_Permissions() |
|||
{ |
|||
var result = await _resourcePermissionChecker.IsGrantedAsync( |
|||
new[] { "MyResourcePermission3", "NonExistentPermission", "MyResourcePermission5" }, |
|||
TestEntityResource.ResourceName, |
|||
TestEntityResource.ResourceKey5); |
|||
|
|||
result.Result["MyResourcePermission3"].ShouldBe(PermissionGrantResult.Granted); |
|||
result.Result["NonExistentPermission"].ShouldBe(PermissionGrantResult.Prohibited); |
|||
result.Result["MyResourcePermission5"].ShouldBe(PermissionGrantResult.Granted); |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,66 @@ |
|||
using System.Globalization; |
|||
using System; |
|||
using System.Threading.Tasks; |
|||
using Shouldly; |
|||
using Xunit; |
|||
|
|||
namespace Volo.Abp.SimpleStateChecking; |
|||
|
|||
public class SimpleStateChecker_NoCheckers_Test : SimpleStateCheckerTestBase |
|||
{ |
|||
// No GlobalStateCheckers registered — verifies the fast path when both
|
|||
// state.StateCheckers and Options.GlobalStateCheckers are empty.
|
|||
|
|||
[Fact] |
|||
public async Task Entity_With_No_State_Checkers_Should_Be_Enabled() |
|||
{ |
|||
var entity = new MyStateEntity(); |
|||
|
|||
(await SimpleStateCheckerManager.IsEnabledAsync(entity)).ShouldBeTrue(); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Entity_With_No_State_Checkers_Should_Not_Increment_Check_Counts() |
|||
{ |
|||
var entity = new MyStateEntity(); |
|||
|
|||
await SimpleStateCheckerManager.IsEnabledAsync(entity); |
|||
|
|||
entity.CheckCount.ShouldBe(0); |
|||
entity.GlobalCheckCount.ShouldBe(0); |
|||
entity.MultipleCheckCount.ShouldBe(0); |
|||
entity.MultipleGlobalCheckCount.ShouldBe(0); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Multiple_Entities_With_No_State_Checkers_Should_All_Be_Enabled() |
|||
{ |
|||
var entities = new[] |
|||
{ |
|||
new MyStateEntity { CreationTime = DateTime.Parse("2022-01-01", CultureInfo.InvariantCulture) }, |
|||
new MyStateEntity { CreationTime = DateTime.Parse("2019-01-01", CultureInfo.InvariantCulture) } |
|||
}; |
|||
|
|||
var result = await SimpleStateCheckerManager.IsEnabledAsync(entities); |
|||
|
|||
result.Values.ShouldAllBe(v => v); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Entity_With_Individual_Checker_Should_Still_Be_Checked() |
|||
{ |
|||
var entity = new MyStateEntity |
|||
{ |
|||
CreationTime = DateTime.Parse("2021-01-01", CultureInfo.InvariantCulture) |
|||
}; |
|||
entity.AddSimpleStateChecker(new MySimpleStateChecker()); |
|||
|
|||
(await SimpleStateCheckerManager.IsEnabledAsync(entity)).ShouldBeTrue(); |
|||
entity.CheckCount.ShouldBe(1); |
|||
|
|||
entity.CreationTime = DateTime.Parse("2001-01-01", CultureInfo.InvariantCulture); |
|||
|
|||
(await SimpleStateCheckerManager.IsEnabledAsync(entity)).ShouldBeFalse(); |
|||
entity.CheckCount.ShouldBe(2); |
|||
} |
|||
} |
|||
Loading…
Reference in new issue