Activities of "maliming"

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

Hi,

Let me explain what's happening with the permission counts.

Why Host (8389) has more than Tenant (4317)

This is expected in ABP multi-tenancy. Each permission definition has a MultiTenancySide flag — it can be Host, Tenant, or Both. When you click "Grant all permissions":

  • On the Host side: it grants all permissions marked as Host or Both
  • On the Tenant side: it grants all permissions marked as Tenant or Both

Host typically has more permissions because some permissions are host-only (e.g., tenant management, edition management). So the difference between 8389 and 4317 is normal.

Why the database has more than the UI shows (~1810)

The permission management UI only shows currently defined permissions — permissions that your running microservices actively register. But the AbpPermissionGrants table stores all grants ever made, including grants for permissions that no longer exist (orphaned grants).

This can happen when:

  • A module was installed, "Grant all" was clicked, then the module was later removed or replaced
  • Permission names were changed in a module update (old grants for old names remain)
  • A microservice was previously running and registered permissions, but is no longer active

Important: Don't delete these records

In a microservices architecture, the AbpPermissionGrants table might be shared across services. What looks like "orphaned" from the admin service UI might actually be permissions registered by other microservices that aren't visible in the current admin UI. Deleting them could break permission checks in those services.

ABP safely ignores grants for undefined permissions at runtime, so they don't cause any functional issues.

How to investigate

You can run these queries to understand the data better. These are read-only queries, just for investigation:

-- 1. How many grants per provider in this database?
SELECT ProviderName, ProviderKey, COUNT(*) as GrantCount
FROM AbpPermissionGrants
GROUP BY ProviderName, ProviderKey
ORDER BY GrantCount DESC;

-- 2. Find permission names that are granted but might not be currently defined
-- Export this list and compare with what you see in the UI
SELECT DISTINCT Name
FROM AbpPermissionGrants
WHERE ProviderName = 'R' AND ProviderKey = 'admin'
ORDER BY Name;

-- 3. Check if there are duplicate grants (same permission granted multiple times)
SELECT Name, COUNT(*) as Cnt
FROM AbpPermissionGrants
WHERE ProviderName = 'R' AND ProviderKey = 'admin'
GROUP BY Name
HAVING COUNT(*) > 1;

Query #2 will give you the full list of granted permission names. You can compare this with the permissions shown in the UI to identify which ones are "extra". Most likely they belong to modules or services that were previously installed.

About the 5th patch file

Did you get a chance to deploy the OptimizedAppConfigurationAppService.cs file I shared earlier? That should help with the remaining performance issue. Please share the updated logs after deploying it.

Thanks

Hi,

Thanks for the detailed logs and screenshots! Let me address everything.

About the warm cache performance (1954ms / 829ms)

Even though BulkPermissionStore is working well (all cache hits), there's still a bottleneck in GetAuthConfigAsync — it calls GetOrNullAsync for every single permission in a loop. With 4410 permissions that's 4410 async calls, with 8389 it's 8389 async calls. This is why the time is proportional to permission count.

We have a 5th patch file that fixes this. Please add OptimizedAppConfigurationAppService.cs to your project alongside the other 4 files:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
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 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;
            }
        }

        return authConfig;
    }
}

Note: The constructor parameters must match your ABP version (9.2.0). If you get a compile error about constructor parameters, please let me know and I'll adjust.

About the permission count (UI shows ~1800 but cache shows 4410/8389)

The permission count in the cache comes from the AbpPermissionGrants database table — it shows how many permissions are granted, not how many are defined. In your screenshot I can see "Grant all permissions" is checked, so all defined permissions get granted.

The UI dialog shows permissions from currently registered modules. But the database may contain grants from previously installed modules that were later removed. These orphaned grants are harmless but they increase the cache size. You can verify by running this SQL on both databases:

SELECT COUNT(*) FROM AbpPermissionGrants WHERE ProviderName = 'R' AND ProviderKey = 'admin';

About a meeting

I understand the desire for a call, but the bottleneck here is something we can only diagnose through logs and data — a meeting wouldn't give us more visibility than what the debug logs already show. Each round of logs has helped us identify and fix a specific layer of the problem. Please try deploying this 5th file and share the updated logs — I expect a significant improvement.

Thanks

Hi,

The "Set Password" from the Tenant management page works through a distributed event internally — the UI shows success as soon as the event is published, but the actual password change happens asynchronously in a background handler. So the success toast doesn't necessarily mean the password was actually changed.

Could you check the logs of your HttpApi.Host and AuthServer after attempting the password change? Look for any of these messages:

  • User password changed: admin — means it actually worked
  • User password change failed: admin — password validation error
  • An error occurred while handling UserPasswordChangeRequestedEto — exception during processing
  • If none of these appear, the event was never processed at all

You can share the relevant logs(Logs.txt) to liming.ma@volosoft.com if that's easier.

Thanks

Hi

Sorry about this issue — it's a bug on our side. We've refunded the support credits for this question.

The RedirectUris field in the edit modal does support multiple URIs (one per line in the textarea). However, there's a line-ending bug when the application runs on Windows: the browser sends \n but the server-side code splits by \r\n, which causes the URIs to get corrupted on save.

We've already fixed this and it will be included in an upcoming release.

Quick fix to unblock yourself now:

Add this single class to your Blazor Server project to override the built-in mappers:

using System;
using System.Collections.Generic;
using System.Linq;
using Volo.Abp.DependencyInjection;
using Volo.Abp.OpenIddict.Applications.Dtos;
using Volo.Abp.OpenIddict.Pro.Blazor;
using Volo.Abp.OpenIddict.Pro.Blazor.Pages;

[Dependency(ReplaceServices = true)]
public class FixedApplicationCreateMapper : ApplicationModalViewToCreateApplicationInputMapper
{
    public override void AfterMap(ApplicationModalView source, CreateApplicationInput destination)
    {
        base.AfterMap(source, destination);
        destination.RedirectUris = SplitByNewLine(source.RedirectUris);
        destination.PostLogoutRedirectUris = SplitByNewLine(source.PostLogoutRedirectUris);
    }

    public static HashSet<string> SplitByNewLine(string? value)
    {
        if (string.IsNullOrWhiteSpace(value))
        {
            return new HashSet<string>();
        }
        return value.Trim()
            .Split(new[] { "\r\n", "\n" }, StringSplitOptions.RemoveEmptyEntries)
            .Select(x => x.Trim())
            .Where(x => !string.IsNullOrEmpty(x))
            .Distinct()
            .ToHashSet();
    }
}

[Dependency(ReplaceServices = true)]
public class FixedApplicationUpdateMapper : ApplicationModalViewToUpdateApplicationInputMapper
{
    public override void AfterMap(ApplicationModalView source, UpdateApplicationInput destination)
    {
        base.AfterMap(source, destination);
        destination.RedirectUris = FixedApplicationCreateMapper.SplitByNewLine(source.RedirectUris);
        destination.PostLogoutRedirectUris = FixedApplicationCreateMapper.SplitByNewLine(source.PostLogoutRedirectUris);
    }
}

Just drop this file in your Blazor Server project — ABP will automatically pick it up and replace the original mappers. The UI will then correctly handle multiple redirect URIs. You can remove this file once you upgrade to the version with the official fix.

Thanks

Hi,

Your analysis is correct. We confirmed this by comparing the Blazorise source code between v2.0.0 and v2.0.4.

The IconName enum in Blazorise has no explicit integer values. In v2.0.4, two new members (Minus and Plus) were inserted into the middle of the enum (commit e3f5460ab - Gantt component #6454):

 MicrophoneSlash,
+Minus,
 MinusCircle,
 ...
 Plug,
+Plus,
 PlusCircle,

This shifts all subsequent enum integer values by +1 or +2. Since ABP 10.2.0 was compiled against Blazorise 2.0.0, the integer constants baked into the IL now map to wrong IconName members when running with 2.0.4. That's why Times becomes Ticket, QuestionCircle becomes Print, etc.

For now, please pin all your Blazorise packages to 2.0.0 to match what ABP 10.2.0 was compiled against:

<PackageReference Include="Blazorise.Bootstrap5" Version="2.0.0" />
<PackageReference Include="Blazorise.Icons.FontAwesome" Version="2.0.0" />
<PackageReference Include="Blazorise.Components" Version="2.0.0" />

We'll look into updating the Blazorise dependency version in a future release.

Thanks

Your current question ticket has been refunded.

Thanks.

Hi,

This issue has been fixed in PR https://github.com/volosoft/volo/pull/22075. In version 10.2.0, the AdvancedEntityFiltersToggleComponent had both expanded = model(false) and an explicit expandedChange = output<boolean>(), which caused a conflict — Angular's output resolution picked the explicit (never-emitted) output over the model signal's implicit one, breaking the two-way binding.

Please upgrade to version 10.2.1 to resolve this issue, and you can remove the patch-package patch after upgrading.

Thanks

Showing 641 to 650 of 12928 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.