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":
Host or BothTenant or BothHost 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:
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 workedUser password change failed: admin — password validation errorAn error occurred while handling UserPasswordChangeRequestedEto — exception during processingYou 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
Hi,
I checked our Hangfire module source code and also went through the Hangfire.SqlServer source code itself. There's no connection leak on either side — all connections are properly released with try-finally blocks, and the connection pool is handled by ADO.NET as expected.
ABP also doesn't have any hidden background polling that would create connections on its own. Things like SecurityStampValidator and MultiTenantConnectionStringResolver only run during HTTP requests, not on timers.
So the socket growth you're seeing is most likely not coming from ABP or Hangfire itself. Here are a few things worth checking:
1. Check your own code for connection leaks
Look for any place where you manually create SqlConnection, HttpClient, or other disposable objects without wrapping them in using. This is the most common cause of socket growth. For example:
// Bad - connection never disposed
var conn = new SqlConnection(connString);
conn.Open();
conn.Execute("...");
// Good
using var conn = new SqlConnection(connString);
conn.Open();
conn.Execute("...");
Also check if you're creating HttpClient instances per request instead of reusing them — each new HttpClient opens a new socket.
2. Azure SNAT port exhaustion
This is a very common issue on Azure App Service. When your app makes many short-lived outbound connections (to SQL, Redis, blob storage, etc.), Azure's SNAT ports can get exhausted, which causes exactly the symptoms you described. You can check this in Azure Portal → your App Service → Diagnose and Solve Problems → search for "SNAT".
More info: https://learn.microsoft.com/en-us/azure/app-service/troubleshoot-intermittent-outbound-connection-errors
3. Monitor what's actually using the sockets
You can use dotnet-counters to see what's happening:
dotnet-counters monitor --process-id <PID> --counters "Microsoft.Data.SqlClient.EventSource,System.Net.Sockets,System.Net.Http"
This will show you SQL connection pool stats, socket counts, and HTTP connection counts separately, so you can tell which component is actually leaking.
4. Reduce Hangfire connection usage
Even though Hangfire isn't leaking, you can reduce its connection throughput:
services.AddHangfire(config =>
config.UseSqlServerStorage(connectionString, new SqlServerStorageOptions
{
QueuePollInterval = TimeSpan.FromSeconds(30)
}));
services.AddHangfireServer(options =>
{
options.WorkerCount = 5;
});
Thanks
hi
We will release it within three days.
Thanks.