Activities of "maliming"

Hi,

You're welcome! Glad we got it resolved. Just a quick summary for your reference:

The problem: With 4000+ permissions, /api/abp/application-configuration was taking 10+ seconds due to multiple performance bottlenecks stacking up.

What the 5 files do:

| File | What it fixes | Impact | |------|--------------|--------| | PermissionGrantBulkCacheItem.cs | Bulk cache data structure | — | | BulkPermissionStore.cs | 12,000+ individual Redis calls → 3 bulk lookups | 10s → ~1s | | PermissionGrantBulkCacheItemInvalidator.cs | Keeps bulk cache in sync when permissions change | — | | OptimizedPermissionChecker.cs | Pre-loads permission definitions + feature values, avoids thousands of DI scope creations | ~3.5s → 41ms | | OptimizedAppConfigurationAppService.cs | Replaces per-permission async lookups with a single batch load | ~800ms → 10ms |

Final result: 10s+ → 167ms server-side

These optimizations will be included in ABP 10.3.1. After upgrading, you can remove all 5 files.

Feel free to reach out if you run into any issues after deploying to your other environments.

Thanks

Hi,

Yes, please deploy the latest version of all 5 files to all three production environments. The diagnostic logs (timing info) are lightweight and won't affect performance, so you can keep them if you want to monitor things — but feel free to remove them if you prefer cleaner logs.

To summarize, these are the 5 files you should have in each admin service project:

  1. PermissionGrantBulkCacheItem.cs
  2. BulkPermissionStore.cs
  3. PermissionGrantBulkCacheItemInvalidator.cs
  4. OptimizedPermissionChecker.cs (the latest version with feature pre-loading)
  5. OptimizedAppConfigurationAppService.cs

Make sure the OptimizedPermissionChecker.cs is the latest version — the one that shows Pre-loaded XX unique feature values in the logs. That's the version that brought StepC from 3519ms down to 41ms.

Once you upgrade to ABP 10.3.1 or later, you can remove all 5 files.

Thanks

Hi,

The optimization is working perfectly:

StepC (StateChecker): 3519ms → 41ms
Total server-side:    3651ms → 167ms

The key line Pre-loaded 31 unique feature values confirms that instead of 4047 individual Redis calls, we only made 31 and checked everything else in memory.

"Without this, did anything happen in this API or not?"

No, there are no other changes to this API. The improvement is entirely from the patch files we provided. The API code itself in ABP hasn't changed — our patch files just replace the internal services with optimized versions.

About the timing difference (browser 848ms vs server logs 167ms):

Looking at your DevTools Timing tab:

  • Waiting (TTFB): 457ms = ~290ms network round-trip + 167ms server processing
  • Content Download: 137ms (transferring the 93.6 kB JSON response)
  • Total in Timing: 738ms

The 848ms total in the network tab includes additional time for connection setup, queueing, or browser-level overhead before the request was sent.

The server processes the request in 167ms — the rest is network latency and response transfer, which depends on the distance between your users and the server.

"Why does open-configuration take 1 sec?"

The first request after login (or after the application restarts) is slower because it needs to populate caches that are empty:

  • Loading permission definitions from the database
  • Loading feature values from Redis/database for the first time
  • Building internal caches

Once these caches are populated, subsequent requests are fast (167ms as you can see). This is expected behavior — the first request warms up the caches, and all following requests benefit from them.

Thanks

Hi,

We found the root cause. Your timing logs showed that StepC (StateCheckerManager) takes 3519ms — 4050 of your 4358 permissions have feature-based state checkers. Each state checker calls IFeatureChecker.IsEnabledAsync which does a Redis GET per call. That's 4050+ individual Redis calls just for feature checking.

The fix: pre-load all unique feature values once (~30-50 Redis calls), then check all 4050 permissions against in-memory data (0 Redis calls). This should reduce StepC from ~3500ms to under 100ms.

Please replace OptimizedPermissionChecker.cs with the version below. The other files stay the same.

OptimizedPermissionChecker.cs (replace existing):

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Volo.Abp;
using Volo.Abp.Authorization.Permissions;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Features;
using Volo.Abp.GlobalFeatures;
using Volo.Abp.MultiTenancy;
using Volo.Abp.Security.Claims;
using Volo.Abp.SimpleStateChecking;
using Volo.Abp.Users;

[Dependency(ReplaceServices = true)]
[ExposeServices(typeof(IPermissionChecker))]
public class OptimizedPermissionChecker : IPermissionChecker, ITransientDependency
{
    public ILogger<OptimizedPermissionChecker> Logger { get; set; }

    protected IPermissionDefinitionManager PermissionDefinitionManager { get; }
    protected ICurrentPrincipalAccessor PrincipalAccessor { get; }
    protected ICurrentTenant CurrentTenant { get; }
    protected IPermissionValueProviderManager PermissionValueProviderManager { get; }
    protected ISimpleStateCheckerManager<PermissionDefinition> StateCheckerManager { get; }
    protected IServiceProvider ServiceProvider { get; }
    protected AbpSimpleStateCheckerOptions<PermissionDefinition> StateCheckerOptions { get; }

    public OptimizedPermissionChecker(
        ICurrentPrincipalAccessor principalAccessor,
        IPermissionDefinitionManager permissionDefinitionManager,
        ICurrentTenant currentTenant,
        IPermissionValueProviderManager permissionValueProviderManager,
        ISimpleStateCheckerManager<PermissionDefinition> stateCheckerManager,
        IServiceProvider serviceProvider,
        IOptions<AbpSimpleStateCheckerOptions<PermissionDefinition>> stateCheckerOptions)
    {
        PrincipalAccessor = principalAccessor;
        PermissionDefinitionManager = permissionDefinitionManager;
        CurrentTenant = currentTenant;
        PermissionValueProviderManager = permissionValueProviderManager;
        StateCheckerManager = stateCheckerManager;
        ServiceProvider = serviceProvider;
        StateCheckerOptions = stateCheckerOptions.Value;
        Logger = NullLogger<OptimizedPermissionChecker>.Instance;
    }

    public virtual async Task<bool> IsGrantedAsync(string name)
    {
        return await IsGrantedAsync(PrincipalAccessor.Principal, name);
    }

    public virtual async Task<bool> IsGrantedAsync(ClaimsPrincipal? claimsPrincipal, string name)
    {
        Check.NotNull(name, nameof(name));

        var permission = await PermissionDefinitionManager.GetOrNullAsync(name);
        if (permission == null)
        {
            return false;
        }

        if (!permission.IsEnabled)
        {
            return false;
        }

        if (!await StateCheckerManager.IsEnabledAsync(permission))
        {
            return false;
        }

        var multiTenancySide = CurrentTenant.GetMultiTenancySide();

        if (!permission.MultiTenancySide.HasFlag(multiTenancySide))
        {
            return false;
        }

        var isGranted = false;
        var context = new PermissionValueCheckContext(permission, claimsPrincipal);
        foreach (var provider in PermissionValueProviderManager.ValueProviders)
        {
            if (context.Permission.Providers.Any() &&
                !context.Permission.Providers.Contains(provider.Name))
            {
                continue;
            }

            var result = await provider.CheckAsync(context);

            if (result == PermissionGrantResult.Granted)
            {
                isGranted = true;
            }
            else if (result == PermissionGrantResult.Prohibited)
            {
                return false;
            }
        }

        return isGranted;
    }

    public virtual async Task<MultiplePermissionGrantResult> IsGrantedAsync(string[] names)
    {
        return await IsGrantedAsync(PrincipalAccessor.Principal, names);
    }

    public virtual async Task<MultiplePermissionGrantResult> IsGrantedAsync(
        ClaimsPrincipal? claimsPrincipal, string[] names)
    {
        Check.NotNull(names, nameof(names));

        Logger.LogInformation("OptimizedPermissionChecker.IsGrantedAsync called with {Count} permissions", names.Length);

        var totalSw = Stopwatch.StartNew();
        var sw = Stopwatch.StartNew();

        var result = new MultiplePermissionGrantResult();
        if (!names.Any())
        {
            return result;
        }

        var multiTenancySide = CurrentTenant.GetMultiTenancySide();

        var allPermissions = (await PermissionDefinitionManager.GetPermissionsAsync())
            .ToDictionary(p => p.Name);
        Logger.LogInformation(
            "OptimizedPermissionChecker [StepA] GetPermissionsAsync: {Elapsed}ms, count: {Count}",
            sw.ElapsedMilliseconds, allPermissions.Count);

        sw.Restart();
        var pendingStateCheck = new List<PermissionDefinition>();
        var permissionDefinitions = new List<PermissionDefinition>();

        foreach (var name in names)
        {
            if (!allPermissions.TryGetValue(name, out var permission))
            {
                result.Result.Add(name, PermissionGrantResult.Prohibited);
                continue;
            }

            result.Result.Add(name, PermissionGrantResult.Undefined);

            if (!permission.IsEnabled || !permission.MultiTenancySide.HasFlag(multiTenancySide))
            {
                continue;
            }

            if (permission.StateCheckers.Any())
            {
                pendingStateCheck.Add(permission);
            }
            else
            {
                permissionDefinitions.Add(permission);
            }
        }
        Logger.LogInformation(
            "OptimizedPermissionChecker [StepB] Filter: {Elapsed}ms, needStateCheck: {StateCount}, noStateCheck: {NoStateCount}",
            sw.ElapsedMilliseconds, pendingStateCheck.Count, permissionDefinitions.Count);

        sw.Restart();
        if (pendingStateCheck.Any())
        {
            await CheckStatesInSingleScopeAsync(pendingStateCheck, permissionDefinitions);
        }
        Logger.LogInformation(
            "OptimizedPermissionChecker [StepC] StateCheck: {Elapsed}ms, passed: {Count}",
            sw.ElapsedMilliseconds, permissionDefinitions.Count);

        sw.Restart();
        foreach (var provider in PermissionValueProviderManager.ValueProviders)
        {
            var permissions = permissionDefinitions
                .Where(x => !x.Providers.Any() || x.Providers.Contains(provider.Name))
                .ToList();

            if (permissions.IsNullOrEmpty())
            {
                continue;
            }

            var context = new PermissionValuesCheckContext(permissions, claimsPrincipal);
            var multipleResult = await provider.CheckAsync(context);

            foreach (var grantResult in multipleResult.Result.Where(x => result.Result.ContainsKey(x.Key)))
            {
                switch (grantResult.Value)
                {
                    case PermissionGrantResult.Granted:
                    {
                        if (result.Result[grantResult.Key] != PermissionGrantResult.Prohibited)
                        {
                            result.Result[grantResult.Key] = PermissionGrantResult.Granted;
                        }
                        break;
                    }
                    case PermissionGrantResult.Prohibited:
                        result.Result[grantResult.Key] = PermissionGrantResult.Prohibited;
                        permissionDefinitions.RemoveAll(x => x.Name == grantResult.Key);
                        break;
                }
            }

            if (result.AllProhibited)
            {
                break;
            }
        }
        Logger.LogInformation(
            "OptimizedPermissionChecker [StepD] Providers: {Elapsed}ms",
            sw.ElapsedMilliseconds);

        Logger.LogInformation(
            "OptimizedPermissionChecker total: {Elapsed}ms",
            totalSw.ElapsedMilliseconds);

        return result;
    }

    protected virtual async Task CheckStatesInSingleScopeAsync(
        List<PermissionDefinition> pendingStateCheck,
        List<PermissionDefinition> permissionDefinitions)
    {
        using (var scope = ServiceProvider.CreateScope())
        {
            var cachedServiceProvider = scope.ServiceProvider.GetRequiredService<ICachedServiceProvider>();

            // 1. Handle batch state checkers
            var batchStateCheckers = pendingStateCheck
                .SelectMany(x => x.StateCheckers)
                .Where(x => x is ISimpleBatchStateChecker<PermissionDefinition>)
                .Cast<ISimpleBatchStateChecker<PermissionDefinition>>()
                .GroupBy(x => x)
                .Select(x => x.Key);

            var batchDisabledSet = new HashSet<string>();

            foreach (var stateChecker in batchStateCheckers)
            {
                var relevantStates = pendingStateCheck
                    .Where(x => x.StateCheckers.Contains(stateChecker))
                    .ToArray();
                var context = new SimpleBatchStateCheckerContext<PermissionDefinition>(
                    cachedServiceProvider, relevantStates);

                foreach (var item in await stateChecker.IsEnabledAsync(context))
                {
                    if (!item.Value)
                    {
                        batchDisabledSet.Add(item.Key.Name);
                    }
                }
            }

            // 2. Handle global batch state checkers
            foreach (ISimpleBatchStateChecker<PermissionDefinition> globalChecker in StateCheckerOptions
                .GlobalStateCheckers
                .Where(x => typeof(ISimpleBatchStateChecker<PermissionDefinition>).IsAssignableFrom(x))
                .Select(x => scope.ServiceProvider.GetRequiredService(x)))
            {
                var relevantStates = pendingStateCheck
                    .Where(x => !batchDisabledSet.Contains(x.Name))
                    .ToArray();
                if (!relevantStates.Any())
                {
                    break;
                }

                var context = new SimpleBatchStateCheckerContext<PermissionDefinition>(
                    cachedServiceProvider, relevantStates);

                foreach (var item in await globalChecker.IsEnabledAsync(context))
                {
                    if (!item.Value)
                    {
                        batchDisabledSet.Add(item.Key.Name);
                    }
                }
            }

            // 3. Pre-load feature values (key optimization!)
            //    Instead of 4000+ individual Redis GETs, we load ~30-50 unique features once.
            var featureChecker = scope.ServiceProvider.GetRequiredService<IFeatureChecker>();
            var isAuthenticated = scope.ServiceProvider.GetRequiredService<ICurrentUser>().IsAuthenticated;

            var uniqueFeatureNames = pendingStateCheck
                .SelectMany(p => p.StateCheckers)
                .OfType<RequireFeaturesSimpleStateChecker<PermissionDefinition>>()
                .SelectMany(c => c.FeatureNames)
                .Distinct()
                .ToList();

            var featureEnabledCache = new Dictionary<string, bool>();
            foreach (var featureName in uniqueFeatureNames)
            {
                featureEnabledCache[featureName] = await featureChecker.IsEnabledAsync(featureName);
            }

            Logger.LogInformation(
                "OptimizedPermissionChecker: Pre-loaded {Count} unique feature values",
                uniqueFeatureNames.Count);

            // 4. Evaluate state checkers against in-memory data (0 Redis calls)
            var globalNonBatchCheckerTypes = StateCheckerOptions.GlobalStateCheckers
                .Where(x => !typeof(ISimpleBatchStateChecker<PermissionDefinition>).IsAssignableFrom(x))
                .ToList();

            var resolvedGlobalCheckers = globalNonBatchCheckerTypes
                .Select(x => (ISimpleStateChecker<PermissionDefinition>)scope.ServiceProvider.GetRequiredService(x))
                .ToList();

            foreach (var permission in pendingStateCheck)
            {
                if (batchDisabledSet.Contains(permission.Name))
                {
                    continue;
                }

                var isEnabled = true;

                foreach (var checker in permission.StateCheckers
                    .Where(x => x is not ISimpleBatchStateChecker<PermissionDefinition>))
                {
                    if (checker is RequireFeaturesSimpleStateChecker<PermissionDefinition> fc)
                    {
                        isEnabled = fc.RequiresAll
                            ? fc.FeatureNames.All(f => featureEnabledCache.GetValueOrDefault(f))
                            : fc.FeatureNames.Any(f => featureEnabledCache.GetValueOrDefault(f));
                    }
                    else if (checker is RequireGlobalFeaturesSimpleStateChecker<PermissionDefinition> gfc)
                    {
                        isEnabled = gfc.RequiresAll
                            ? gfc.GlobalFeatureNames.All(x => GlobalFeatureManager.Instance.IsEnabled(x))
                            : gfc.GlobalFeatureNames.Any(x => GlobalFeatureManager.Instance.IsEnabled(x));
                    }
                    else if (checker is RequireAuthenticatedSimpleStateChecker<PermissionDefinition>)
                    {
                        isEnabled = isAuthenticated;
                    }
                    else
                    {
                        var context = new SimpleStateCheckerContext<PermissionDefinition>(
                            cachedServiceProvider, permission);
                        isEnabled = await checker.IsEnabledAsync(context);
                    }

                    if (!isEnabled)
                    {
                        break;
                    }
                }

                if (isEnabled && resolvedGlobalCheckers.Any())
                {
                    var context = new SimpleStateCheckerContext<PermissionDefinition>(
                        cachedServiceProvider, permission);
                    foreach (var globalChecker in resolvedGlobalCheckers)
                    {
                        if (!await globalChecker.IsEnabledAsync(context))
                        {
                            isEnabled = false;
                            break;
                        }
                    }
                }

                if (isEnabled)
                {
                    permissionDefinitions.Add(permission);
                }
            }
        }
    }
}

After deploying, please make two requests and share the logs. The key log to look for:

OptimizedPermissionChecker: Pre-loaded XX unique feature values
OptimizedPermissionChecker [StepC] StateCheck: XXms

We expect StepC to drop from ~3500ms to well under 100ms.

Thanks

Answer

Thanks. 👍

Hi,

Great, the timing logs pinpoint the issue perfectly:

Step1: 6ms    ✅ fast
Step2: 3ms    ✅ fast  
Step3: 0ms    ✅ fast
Step4: 0ms    ✅ fast
Step5: 3462ms ← all the time is here

Step 5 (PermissionChecker.IsGrantedAsync) is taking 3462ms. This strongly suggests the OptimizedPermissionChecker is not being picked up by DI in this environment. Without it, the original PermissionChecker calls GetOrNullAsync + StateCheckerManager.IsEnabledAsync individually for each of the 4358 permissions — that's ~8700 async operations with semaphore locks and DI scope creation, which explains the 3.4 seconds.

Please replace both files with these updated versions. They include diagnostic logging that will confirm whether OptimizedPermissionChecker is active and show exactly where time is spent.

File 1: OptimizedPermissionChecker.cs (replace existing)

using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Volo.Abp;
using Volo.Abp.Authorization.Permissions;
using Volo.Abp.DependencyInjection;
using Volo.Abp.MultiTenancy;
using Volo.Abp.Security.Claims;
using Volo.Abp.SimpleStateChecking;

[Dependency(ReplaceServices = true)]
[ExposeServices(typeof(IPermissionChecker))]
public class OptimizedPermissionChecker : IPermissionChecker, ITransientDependency
{
    public ILogger<OptimizedPermissionChecker> Logger { get; set; }

    protected IPermissionDefinitionManager PermissionDefinitionManager { get; }
    protected ICurrentPrincipalAccessor PrincipalAccessor { get; }
    protected ICurrentTenant CurrentTenant { get; }
    protected IPermissionValueProviderManager PermissionValueProviderManager { get; }
    protected ISimpleStateCheckerManager<PermissionDefinition> StateCheckerManager { get; }

    public OptimizedPermissionChecker(
        ICurrentPrincipalAccessor principalAccessor,
        IPermissionDefinitionManager permissionDefinitionManager,
        ICurrentTenant currentTenant,
        IPermissionValueProviderManager permissionValueProviderManager,
        ISimpleStateCheckerManager<PermissionDefinition> stateCheckerManager)
    {
        PrincipalAccessor = principalAccessor;
        PermissionDefinitionManager = permissionDefinitionManager;
        CurrentTenant = currentTenant;
        PermissionValueProviderManager = permissionValueProviderManager;
        StateCheckerManager = stateCheckerManager;
        Logger = NullLogger<OptimizedPermissionChecker>.Instance;
    }

    public virtual async Task<bool> IsGrantedAsync(string name)
    {
        return await IsGrantedAsync(PrincipalAccessor.Principal, name);
    }

    public virtual async Task<bool> IsGrantedAsync(ClaimsPrincipal? claimsPrincipal, string name)
    {
        Check.NotNull(name, nameof(name));

        var permission = await PermissionDefinitionManager.GetOrNullAsync(name);
        if (permission == null)
        {
            return false;
        }

        if (!permission.IsEnabled)
        {
            return false;
        }

        if (!await StateCheckerManager.IsEnabledAsync(permission))
        {
            return false;
        }

        var multiTenancySide = CurrentTenant.GetMultiTenancySide();

        if (!permission.MultiTenancySide.HasFlag(multiTenancySide))
        {
            return false;
        }

        var isGranted = false;
        var context = new PermissionValueCheckContext(permission, claimsPrincipal);
        foreach (var provider in PermissionValueProviderManager.ValueProviders)
        {
            if (context.Permission.Providers.Any() &&
                !context.Permission.Providers.Contains(provider.Name))
            {
                continue;
            }

            var result = await provider.CheckAsync(context);

            if (result == PermissionGrantResult.Granted)
            {
                isGranted = true;
            }
            else if (result == PermissionGrantResult.Prohibited)
            {
                return false;
            }
        }

        return isGranted;
    }

    public virtual async Task<MultiplePermissionGrantResult> IsGrantedAsync(string[] names)
    {
        return await IsGrantedAsync(PrincipalAccessor.Principal, names);
    }

    public virtual async Task<MultiplePermissionGrantResult> IsGrantedAsync(
        ClaimsPrincipal? claimsPrincipal, string[] names)
    {
        Check.NotNull(names, nameof(names));

        Logger.LogInformation("OptimizedPermissionChecker.IsGrantedAsync called with {Count} permissions", names.Length);

        var totalSw = Stopwatch.StartNew();
        var sw = Stopwatch.StartNew();

        var result = new MultiplePermissionGrantResult();
        if (!names.Any())
        {
            return result;
        }

        var multiTenancySide = CurrentTenant.GetMultiTenancySide();

        // Step A: Pre-load all permission definitions at once
        var allPermissions = (await PermissionDefinitionManager.GetPermissionsAsync())
            .ToDictionary(p => p.Name);
        Logger.LogInformation(
            "OptimizedPermissionChecker [StepA] GetPermissionsAsync: {Elapsed}ms, count: {Count}",
            sw.ElapsedMilliseconds, allPermissions.Count);

        sw.Restart();
        var pendingStateCheck = new List<PermissionDefinition>();
        var permissionDefinitions = new List<PermissionDefinition>();

        foreach (var name in names)
        {
            if (!allPermissions.TryGetValue(name, out var permission))
            {
                result.Result.Add(name, PermissionGrantResult.Prohibited);
                continue;
            }

            result.Result.Add(name, PermissionGrantResult.Undefined);

            if (!permission.IsEnabled || !permission.MultiTenancySide.HasFlag(multiTenancySide))
            {
                continue;
            }

            if (permission.StateCheckers.Any())
            {
                pendingStateCheck.Add(permission);
            }
            else
            {
                permissionDefinitions.Add(permission);
            }
        }
        Logger.LogInformation(
            "OptimizedPermissionChecker [StepB] Filter: {Elapsed}ms, needStateCheck: {StateCount}, noStateCheck: {NoStateCount}",
            sw.ElapsedMilliseconds, pendingStateCheck.Count, permissionDefinitions.Count);

        // Step C: Batch state check
        sw.Restart();
        if (pendingStateCheck.Any())
        {
            var stateCheckResult = await StateCheckerManager.IsEnabledAsync(
                pendingStateCheck.ToArray());
            foreach (var item in stateCheckResult)
            {
                if (item.Value)
                {
                    permissionDefinitions.Add(item.Key);
                }
            }
        }
        Logger.LogInformation(
            "OptimizedPermissionChecker [StepC] StateCheckerManager: {Elapsed}ms",
            sw.ElapsedMilliseconds);

        // Step D: Provider checking
        sw.Restart();
        foreach (var provider in PermissionValueProviderManager.ValueProviders)
        {
            var permissions = permissionDefinitions
                .Where(x => !x.Providers.Any() || x.Providers.Contains(provider.Name))
                .ToList();

            if (permissions.IsNullOrEmpty())
            {
                continue;
            }

            var context = new PermissionValuesCheckContext(permissions, claimsPrincipal);
            var multipleResult = await provider.CheckAsync(context);

            foreach (var grantResult in multipleResult.Result.Where(x => result.Result.ContainsKey(x.Key)))
            {
                switch (grantResult.Value)
                {
                    case PermissionGrantResult.Granted:
                    {
                        if (result.Result[grantResult.Key] != PermissionGrantResult.Prohibited)
                        {
                            result.Result[grantResult.Key] = PermissionGrantResult.Granted;
                        }
                        break;
                    }
                    case PermissionGrantResult.Prohibited:
                        result.Result[grantResult.Key] = PermissionGrantResult.Prohibited;
                        permissionDefinitions.RemoveAll(x => x.Name == grantResult.Key);
                        break;
                }
            }

            if (result.AllProhibited)
            {
                break;
            }
        }
        Logger.LogInformation(
            "OptimizedPermissionChecker [StepD] Providers: {Elapsed}ms",
            sw.ElapsedMilliseconds);

        Logger.LogInformation(
            "OptimizedPermissionChecker total: {Elapsed}ms",
            totalSw.ElapsedMilliseconds);

        return result;
    }
}

File 2: OptimizedAppConfigurationAppService.cs (replace existing — same as before but now logs the PermissionChecker type)

The key new log line in this file is:

OptimizedAppConfig [Step5] PermissionChecker type: {Type}

If it shows OptimizedPermissionChecker, the checker is active. If it shows Volo.Abp.Authorization.Permissions.PermissionChecker, it means the replacement didn't work.

Please deploy both files, make two requests (first login + page refresh), and share the full logs. We expect to see:

OptimizedAppConfig [Step5] PermissionChecker type: OptimizedPermissionChecker
OptimizedPermissionChecker.IsGrantedAsync called with 4358 permissions
OptimizedPermissionChecker [StepA] GetPermissionsAsync: Xms, count: XXX
OptimizedPermissionChecker [StepB] Filter: Xms, ...
OptimizedPermissionChecker [StepC] StateCheckerManager: Xms
OptimizedPermissionChecker [StepD] Providers: Xms

If you DON'T see the OptimizedPermissionChecker logs, then the file is not being registered properly — make sure it's included in the admin service project and the project compiles without errors.

Thanks

Hi,

The timing shows GetAuthConfigAsync alone takes 3474ms — and looking at the timestamps, there's a 3-second gap BEFORE the BulkPermissionStore cache hits. This means the bottleneck is somewhere inside GetAuthConfigAsync before it even reaches the permission check.

I've updated OptimizedAppConfigurationAppService.cs with detailed step-by-step timing. Please replace the file and make two requests (first login + one page refresh), then share the logs. We'll see exactly which step is slow:

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations;
using Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations.ObjectExtending;
using Volo.Abp.Authorization;
using Volo.Abp.Authorization.Permissions;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Features;
using Volo.Abp.Localization;
using Volo.Abp.MultiTenancy;
using Volo.Abp.Settings;
using Volo.Abp.Timing;
using Volo.Abp.Users;

[Dependency(ReplaceServices = true)]
[ExposeServices(typeof(IAbpApplicationConfigurationAppService))]
public class OptimizedAppConfigurationAppService : AbpApplicationConfigurationAppService
{
    public OptimizedAppConfigurationAppService(
        IOptions<AbpLocalizationOptions> localizationOptions,
        IOptions<AbpMultiTenancyOptions> multiTenancyOptions,
        IServiceProvider serviceProvider,
        IAbpAuthorizationPolicyProvider abpAuthorizationPolicyProvider,
        IPermissionDefinitionManager permissionDefinitionManager,
        DefaultAuthorizationPolicyProvider defaultAuthorizationPolicyProvider,
        IPermissionChecker permissionChecker,
        IAuthorizationService authorizationService,
        ICurrentUser currentUser,
        ISettingProvider settingProvider,
        ISettingDefinitionManager settingDefinitionManager,
        IFeatureDefinitionManager featureDefinitionManager,
        ILanguageProvider languageProvider,
        ITimezoneProvider timezoneProvider,
        IOptions<AbpClockOptions> abpClockOptions,
        ICachedObjectExtensionsDtoService cachedObjectExtensionsDtoService,
        IOptions<AbpApplicationConfigurationOptions> options)
        : base(localizationOptions, multiTenancyOptions, serviceProvider,
            abpAuthorizationPolicyProvider, permissionDefinitionManager,
            defaultAuthorizationPolicyProvider, permissionChecker,
            authorizationService, currentUser, settingProvider,
            settingDefinitionManager, featureDefinitionManager,
            languageProvider, timezoneProvider, abpClockOptions,
            cachedObjectExtensionsDtoService, options)
    {
    }

    protected override async Task<ApplicationAuthConfigurationDto> GetAuthConfigAsync()
    {
        var totalSw = Stopwatch.StartNew();
        var sw = Stopwatch.StartNew();

        var authConfig = new ApplicationAuthConfigurationDto();

        var abpAuthorizationPolicyProvider = LazyServiceProvider
            .LazyGetRequiredService<IAbpAuthorizationPolicyProvider>();
        var permissionDefinitionManager = LazyServiceProvider
            .LazyGetRequiredService<IPermissionDefinitionManager>();
        var defaultAuthorizationPolicyProvider = LazyServiceProvider
            .LazyGetRequiredService<DefaultAuthorizationPolicyProvider>();
        var permissionChecker = LazyServiceProvider
            .LazyGetRequiredService<IPermissionChecker>();
        var authorizationService = LazyServiceProvider
            .LazyGetRequiredService<IAuthorizationService>();

        // Step 1: Get all policy names
        var policyNames = await abpAuthorizationPolicyProvider.GetPoliciesNamesAsync();
        Logger.LogInformation(
            "OptimizedAppConfig [Step1] GetPoliciesNamesAsync: {Elapsed}ms, count: {Count}",
            sw.ElapsedMilliseconds, policyNames.Count);

        // Step 2: Load all permission definitions into HashSet
        sw.Restart();
        var permissionNameSet = new HashSet<string>(
            (await permissionDefinitionManager.GetPermissionsAsync()).Select(p => p.Name),
            StringComparer.Ordinal);
        Logger.LogInformation(
            "OptimizedAppConfig [Step2] GetPermissionsAsync+HashSet: {Elapsed}ms, count: {Count}",
            sw.ElapsedMilliseconds, permissionNameSet.Count);

        // Step 3: Classify policy names (ABP permissions vs other policies)
        sw.Restart();
        var abpPolicyNames = new List<string>();
        var otherPolicyNames = new List<string>();

        foreach (var policyName in policyNames)
        {
            if (await defaultAuthorizationPolicyProvider.GetPolicyAsync(policyName) == null &&
                permissionNameSet.Contains(policyName))
            {
                abpPolicyNames.Add(policyName);
            }
            else
            {
                otherPolicyNames.Add(policyName);
            }
        }
        Logger.LogInformation(
            "OptimizedAppConfig [Step3] ClassifyPolicies loop: {Elapsed}ms, abp: {AbpCount}, other: {OtherCount}",
            sw.ElapsedMilliseconds, abpPolicyNames.Count, otherPolicyNames.Count);

        // Step 4: Check other (non-ABP) policies
        sw.Restart();
        foreach (var policyName in otherPolicyNames)
        {
            if (await authorizationService.IsGrantedAsync(policyName))
            {
                authConfig.GrantedPolicies[policyName] = true;
            }
        }
        Logger.LogInformation(
            "OptimizedAppConfig [Step4] OtherPolicies check: {Elapsed}ms",
            sw.ElapsedMilliseconds);

        // Step 5: Batch permission check (goes through OptimizedPermissionChecker -> BulkPermissionStore)
        sw.Restart();
        var result = await permissionChecker.IsGrantedAsync(abpPolicyNames.ToArray());
        foreach (var item in result.Result)
        {
            if (item.Value == PermissionGrantResult.Granted)
            {
                authConfig.GrantedPolicies[item.Key] = true;
            }
        }
        Logger.LogInformation(
            "OptimizedAppConfig [Step5] PermissionChecker.IsGrantedAsync: {Elapsed}ms, granted: {Count}",
            sw.ElapsedMilliseconds, authConfig.GrantedPolicies.Count);

        Logger.LogInformation(
            "OptimizedAppConfig GetAuthConfigAsync total: {Elapsed}ms",
            totalSw.ElapsedMilliseconds);

        return authConfig;
    }

    protected override async Task<ApplicationFeatureConfigurationDto> GetFeaturesConfigAsync()
    {
        var sw = Stopwatch.StartNew();
        var result = await base.GetFeaturesConfigAsync();
        Logger.LogInformation("OptimizedAppConfig: GetFeaturesConfigAsync took {Elapsed}ms", sw.ElapsedMilliseconds);
        return result;
    }

    protected override async Task<ApplicationLocalizationConfigurationDto> GetLocalizationConfigAsync(
        ApplicationConfigurationRequestOptions options)
    {
        var sw = Stopwatch.StartNew();
        var result = await base.GetLocalizationConfigAsync(options);
        Logger.LogInformation("OptimizedAppConfig: GetLocalizationConfigAsync took {Elapsed}ms", sw.ElapsedMilliseconds);
        return result;
    }

    protected override async Task<TimingDto> GetTimingConfigAsync()
    {
        var sw = Stopwatch.StartNew();
        var result = await base.GetTimingConfigAsync();
        Logger.LogInformation("OptimizedAppConfig: GetTimingConfigAsync took {Elapsed}ms", sw.ElapsedMilliseconds);
        return result;
    }
}

The logs will show something like:

OptimizedAppConfig [Step1] GetPoliciesNamesAsync: XXms, count: XXX
OptimizedAppConfig [Step2] GetPermissionsAsync+HashSet: XXms, count: XXX
OptimizedAppConfig [Step3] ClassifyPolicies loop: XXms, abp: XXX, other: XXX
OptimizedAppConfig [Step4] OtherPolicies check: XXms
OptimizedAppConfig [Step5] PermissionChecker.IsGrantedAsync: XXms, granted: XXX
OptimizedAppConfig GetAuthConfigAsync total: XXms

This will tell us exactly which step is consuming the 3474ms. Please share logs for both the first request (cold) and second request (warm).

Thanks

Answer

Hi,

You're right, this change should have been documented in the release notes. Currently our release notes focus on new features and may miss breaking changes like this.

The reason you hit this issue is that you're using a custom React frontend. Our official templates and UI projects (MVC, Blazor, Angular, React) are updated to match these DTO changes automatically with each release, so users on those don't notice the change.

I've noted this as feedback — DTO contract changes that break existing API calls should be listed in the release notes or migration guide, especially for users with custom frontends.

Thanks

Hi,

Thanks for testing. The 3391ms is likely a one-time cold start after restarting the application — the permission definition cache needs to reload from the database on the first request.

Could you please refresh the page once more and share the server log for that second request? We need to compare it with the previous 1954ms to see if the optimization is working.

Also, I've updated the OptimizedAppConfigurationAppService.cs with timing logs for each step. Please replace the file with this new version — it will tell us exactly where the time is being spent:

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations;
using Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations.ObjectExtending;
using Volo.Abp.Authorization;
using Volo.Abp.Authorization.Permissions;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Features;
using Volo.Abp.Localization;
using Volo.Abp.MultiTenancy;
using Volo.Abp.Settings;
using Volo.Abp.Timing;
using Volo.Abp.Users;

[Dependency(ReplaceServices = true)]
[ExposeServices(typeof(IAbpApplicationConfigurationAppService))]
public class OptimizedAppConfigurationAppService : AbpApplicationConfigurationAppService
{
    public OptimizedAppConfigurationAppService(
        IOptions<AbpLocalizationOptions> localizationOptions,
        IOptions<AbpMultiTenancyOptions> multiTenancyOptions,
        IServiceProvider serviceProvider,
        IAbpAuthorizationPolicyProvider abpAuthorizationPolicyProvider,
        IPermissionDefinitionManager permissionDefinitionManager,
        DefaultAuthorizationPolicyProvider defaultAuthorizationPolicyProvider,
        IPermissionChecker permissionChecker,
        IAuthorizationService authorizationService,
        ICurrentUser currentUser,
        ISettingProvider settingProvider,
        ISettingDefinitionManager settingDefinitionManager,
        IFeatureDefinitionManager featureDefinitionManager,
        ILanguageProvider languageProvider,
        ITimezoneProvider timezoneProvider,
        IOptions<AbpClockOptions> abpClockOptions,
        ICachedObjectExtensionsDtoService cachedObjectExtensionsDtoService,
        IOptions<AbpApplicationConfigurationOptions> options)
        : base(localizationOptions, multiTenancyOptions, serviceProvider,
            abpAuthorizationPolicyProvider, permissionDefinitionManager,
            defaultAuthorizationPolicyProvider, permissionChecker,
            authorizationService, currentUser, settingProvider,
            settingDefinitionManager, featureDefinitionManager,
            languageProvider, timezoneProvider, abpClockOptions,
            cachedObjectExtensionsDtoService, options)
    {
    }

    protected override async Task<ApplicationAuthConfigurationDto> GetAuthConfigAsync()
    {
        var sw = Stopwatch.StartNew();

        var authConfig = new ApplicationAuthConfigurationDto();

        var abpAuthorizationPolicyProvider = LazyServiceProvider
            .LazyGetRequiredService<IAbpAuthorizationPolicyProvider>();
        var permissionDefinitionManager = LazyServiceProvider
            .LazyGetRequiredService<IPermissionDefinitionManager>();
        var defaultAuthorizationPolicyProvider = LazyServiceProvider
            .LazyGetRequiredService<DefaultAuthorizationPolicyProvider>();
        var permissionChecker = LazyServiceProvider
            .LazyGetRequiredService<IPermissionChecker>();
        var authorizationService = LazyServiceProvider
            .LazyGetRequiredService<IAuthorizationService>();

        var policyNames = await abpAuthorizationPolicyProvider.GetPoliciesNamesAsync();
        var abpPolicyNames = new List<string>();
        var otherPolicyNames = new List<string>();

        var permissionNameSet = new HashSet<string>(
            (await permissionDefinitionManager.GetPermissionsAsync()).Select(p => p.Name),
            StringComparer.Ordinal);

        foreach (var policyName in policyNames)
        {
            if (await defaultAuthorizationPolicyProvider.GetPolicyAsync(policyName) == null &&
                permissionNameSet.Contains(policyName))
            {
                abpPolicyNames.Add(policyName);
            }
            else
            {
                otherPolicyNames.Add(policyName);
            }
        }

        foreach (var policyName in otherPolicyNames)
        {
            if (await authorizationService.IsGrantedAsync(policyName))
            {
                authConfig.GrantedPolicies[policyName] = true;
            }
        }

        var result = await permissionChecker.IsGrantedAsync(abpPolicyNames.ToArray());
        foreach (var item in result.Result)
        {
            if (item.Value == PermissionGrantResult.Granted)
            {
                authConfig.GrantedPolicies[item.Key] = true;
            }
        }

        Logger.LogInformation("OptimizedAppConfig: GetAuthConfigAsync took {Elapsed}ms", sw.ElapsedMilliseconds);
        return authConfig;
    }

    protected override async Task<ApplicationFeatureConfigurationDto> GetFeaturesConfigAsync()
    {
        var sw = Stopwatch.StartNew();
        var result = await base.GetFeaturesConfigAsync();
        Logger.LogInformation("OptimizedAppConfig: GetFeaturesConfigAsync took {Elapsed}ms", sw.ElapsedMilliseconds);
        return result;
    }

    protected override async Task<ApplicationLocalizationConfigurationDto> GetLocalizationConfigAsync(
        ApplicationConfigurationRequestOptions options)
    {
        var sw = Stopwatch.StartNew();
        var result = await base.GetLocalizationConfigAsync(options);
        Logger.LogInformation("OptimizedAppConfig: GetLocalizationConfigAsync took {Elapsed}ms", sw.ElapsedMilliseconds);
        return result;
    }

    protected override async Task<TimingDto> GetTimingConfigAsync()
    {
        var sw = Stopwatch.StartNew();
        var result = await base.GetTimingConfigAsync();
        Logger.LogInformation("OptimizedAppConfig: GetTimingConfigAsync took {Elapsed}ms", sw.ElapsedMilliseconds);
        return result;
    }
}

After deploying, please make two requests (first login + one page refresh) and share the full server logs. We'll see something like:

OptimizedAppConfig: GetAuthConfigAsync took XXms
OptimizedAppConfig: GetFeaturesConfigAsync took XXms
OptimizedAppConfig: GetLocalizationConfigAsync took XXms
OptimizedAppConfig: GetTimingConfigAsync took XXms

This will tell us exactly which part is slow.

About the permission count in the database

  • Host (8389) vs Tenant (4317): This is expected. ABP permissions have a MultiTenancySide setting — some permissions are host-only (like tenant management). When you click "Grant all permissions" on the host side, it grants more permissions than on the tenant side. So the difference is normal.

  • Database (4317) vs UI (~1810): The AbpPermissionGrants table stores all grants ever made. The UI only shows permissions that are currently defined by your running microservices. The difference could be from:

    • Modules or microservices that were previously installed and had "Grant all" clicked, but were later removed
    • Microservices that registered permissions but are no longer connected to the admin service
    • Permission names that changed in module updates (old grants remain)

These extra grants in the database are harmless — ABP ignores them at runtime. They don't affect functionality or security. I would not recommend deleting them, especially in a microservices setup where other services might still reference those permissions.

To check if there are grants for permissions that no longer have a definition record:

SELECT pg.Name
FROM AbpPermissionGrants pg
WHERE pg.ProviderName = 'R' AND pg.ProviderKey = 'admin'
AND pg.Name NOT IN (SELECT Name FROM AbpPermissions)
ORDER BY pg.Name;

Note: ABP also keeps old permission definitions in the AbpPermissions table even after a module is removed, so this query might not find all the "extra" ones. The difference between the UI count and the database count is mostly due to how ABP accumulates data over time across all your microservices.

Thanks

Answer

Hi,

The AppName property was added to UpdateProfileDto in v10.1.0 as part of a feature that requires email confirmation when a user changes their email address on the profile page.

When Abp.Identity.SignIn.RequireEmailVerificationToRegister setting is enabled, updating the email triggers a confirmation email with a verification link. The AppName tells the system which application URL to use when generating that link (e.g., MVC, Blazor, Angular), since different frontend apps may have different base URLs configured via IAppUrlProvider.

To fix this, just pass an AppName value when calling the profile update endpoint. You can use "MVC" or any name that matches your app URL configuration. For example:

{
  "userName": "john",
  "email": "john@example.com",
  "appName": "MVC",
  ...
}

If you have a custom app URL configured in AbpAccountOptions, use that name instead. Otherwise "MVC" works as the default.

Thanks

Showing 631 to 640 of 12917 entries
Boost Your Development
ABP Live Training
Packages
See Trainings
Mastering ABP Framework Book
The Official Guide
Mastering
ABP Framework
Learn More
Mastering ABP Framework Book
Made with ❤️ on ABP v10.8.0-preview. Updated on November 02, 2026, 06:56
1
ABP Assistant
🔐 You need to be logged in to use the chatbot. Please log in first.