Hi,
ABP doesn't have a dedicated enum select component like abp-lookup-search. The standard approach is to use mapEnumToOptions combined with a regular HTML <select> element — this is exactly how ABP's own extensible form system handles enum dropdowns internally.
Here's how you can use it:
Component (TypeScript):
import { mapEnumToOptions } from '@abp/ng.core';
export enum GeometryTypeEnum {
None = 1,
Point = 2,
Line = 3,
Polygon = 4,
All = 5,
}
// In your component class:
geometryTypeOptions = mapEnumToOptions(GeometryTypeEnum);
Template (HTML):
<select class="form-select form-control" formControlName="geometryType">
<option *ngFor="let option of geometryTypeOptions" [ngValue]="option.value">
{{ option.key }}
</option>
</select>
If you want to localize the enum display names, you can use the abpLocalization pipe:
<option *ngFor="let option of geometryTypeOptions" [ngValue]="option.value">
{{ '::Enum:GeometryType.' + option.key | abpLocalization }}
</option>
Then define the corresponding localization keys in your localization JSON files (e.g., Enum:GeometryType.None, Enum:GeometryType.Point, etc.).
Thanks
Hi,
Yes, ABP automatically wraps all public AppService methods in a Unit of Work, including Get methods. However, for methods whose names start with Get, the UoW is non-transactional by default (no explicit DB
transaction is created).
That said, SaveChangesAsync() is still called at the end of the method. So if your Get method also calls a repository update, the changes will be persisted — and that would generate the UPDATE ... SET ConcurrencyStamp ... WHERE Id AND ConcurrencyStamp SQL you're seeing.
Could you share more details about what the Get method is doing? Specifically, why is it calling a repository update method? That would help us understand the root cause.
Also, could you enable EF Core debug logging to see the actual SQL being executed? This will help us understand the full picture. You can refer to this guide:
https://abp.io/support/questions/8622/How-to-enable-Debug-logs-for-troubleshoot-problems
Thanks
hi
After initializing the database for the first time and starting all the applications, some permissions might not be added. You can manually grant all permissions to the admin role.
Thanks.
Hi,
You only need to add it to your adminserver — that's the only service the browser is calling for localization.
That said, I'd actually recommend holding off on this change for now. Looking at your network panel again, static assets like JS files and fonts are also taking 500–700ms, which have no server-side processing at all. This tells us the main bottleneck is network latency between your users and the server, not the localization API itself. Adding browser caching for localization would help a little, but it won't solve the underlying latency issue.
I'd suggest testing the app locally first (client and server on the same machine). If everything feels fast locally, the slowness you're seeing in production is a network/infrastructure issue that needs to be addressed at that level.
Thanks
Hi,
based on what you've described, the timeout on a simple single-row UPDATE ... WHERE Id AND ConcurrencyStamp with only ~900 records is very unlikely to be a code issue — that statement should complete in milliseconds under normal conditions.
The issue might not be related to your code or ABP at all. Could you try reproducing it against a local SQL Server instance with the same data to help narrow down the root cause?
Thanks
Hi,
Why does opening a new tab always take time?
Since this is an MVC application (not a SPA), every page navigation or new tab triggers a full page load. The browser re-runs all the initialization scripts, which call application-configuration and application-localization on every single page load — there's no in-memory state carried over between tabs. This is expected MVC behavior.
The slowness might be largely network latency
Looking at your network panel, static assets like JS bundles and font files are also taking 500–700ms:
main.js (522 kB) → 982msruntime.js (25.5 kB) → 605msThese files require almost zero server-side processing, so the 500–700ms is essentially pure network round-trip time. This suggests the delay is largely from the distance between your users and the server, not from the application code itself.
Suggestion: test locally first
Run the app locally (client and server on the same machine) and check the API response times. If application-localization responds in ~200–400ms locally, the remaining slowness in production is a network/infrastructure issue, not application code.
Optimizing the localization API for MVC
The application-localization response contains static localization data that rarely changes. In an MVC app where every page load re-fetches it, you can tell the browser to cache it locally and skip the request on subsequent navigations.
Add the following middleware in your module's OnApplicationInitialization, before app.UseConfiguredEndpoints():
app.Use(async (ctx, next) =>
{
if (ctx.Request.Path.StartsWithSegments("/api/abp/application-localization"))
{
ctx.Response.OnStarting(() =>
{
ctx.Response.Headers.CacheControl = "public, max-age=600"; // cache for 10 minutes
return Task.CompletedTask;
});
}
await next();
});
After this change, the browser will cache the localization response for 10 minutes. Every page navigation within that window will skip the API call entirely — no network request, no server processing. This should eliminate the 1.43s from subsequent page loads.
Note: If your application is multi-tenant and different tenants have different dynamic localization texts (via the Language Management module), be cautious with this approach — the browser cache does not include tenant context. In that case, you may want to skip browser caching or reduce
max-ageto a smaller value.
Thanks
Hi,
How the cache works:
It caches all granted permissions per provider in a single Redis entry:
U:{userId} — direct permissions for that specific userR:{roleName} — permissions for that role (shared across all users with the same role)C:{clientName} — permissions for that clientWhy does opening a new tab always take time?
The first time any user/role is accessed, it's a cold cache — one DB query runs to load and cache all permissions. After that, it's a single Redis GET. This is why the first request per user is a bit slower, but subsequent ones are instant.
Why still 1.5–2 seconds?
/api/abp/application-configuration does a lot more than just permission checks — it also loads localization resources, feature grants, settings, auth info, and more. The permission part is now optimized (single Redis GET), but the remaining time comes from those other operations. This is expected behavior.
From your logs, the permission-related work is now effectively instant on cache hits. The overall request time is now in the 600–800ms range, which is a significant improvement from the original 10+ seconds.
Thanks
hi
No problem.
Please build and run it, then check the logs.
Thanks.
hi
Please try to add new files to the TMSMS.AdministrationService project.
Thanks.
Hi,
We have identified the root cause and developed a proper fix that has been tested against real ABP module infrastructure. Here is everything you need.
Root cause
ABP's default PermissionStore stores each permission result as a separate Redis Hash entry. With 4,233 permissions in your system, every request triggers thousands of individual Redis commands:
This is a scalability limitation in ABP's current permission caching design.
The fix
We override PermissionStore to store all granted permissions per provider as a single Redis entry. For example, instead of 4,233 separate entries for role "admin", it stores one entry: bulk_pn:R,pk:admin → ["Orders.View", "Orders.Create", ...]
Step 1 — Revert your previous workarounds
In TMSMSAdministrationServiceModule.cs:
typeof(AbpCachingStackExchangeRedisModule) to your module dependencies if you removed itWebRemoteDynamicClaimsPrincipalContributorOptions.IsEnabled = false line from PreConfigureServicesStep 2 — Add these 3 files to your AdminService project
Replace YourNamespace with your actual project namespace.
PermissionGrantBulkCacheItem.cs
using System;
using System.Collections.Generic;
using System.Linq;
namespace YourNamespace;
[Serializable]
public class PermissionGrantBulkCacheItem
{
public string[] GrantedPermissions { get; set; }
public PermissionGrantBulkCacheItem()
{
GrantedPermissions = Array.Empty<string>();
}
public PermissionGrantBulkCacheItem(IEnumerable<string> grantedPermissions)
{
GrantedPermissions = grantedPermissions.ToArray();
}
public static string CalculateCacheKey(string providerName, string providerKey)
{
return $"bulk_pn:{providerName},pk:{providerKey}";
}
}
BulkPermissionStore.cs
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Volo.Abp.Authorization.Permissions;
using Volo.Abp.Caching;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Domain.Repositories;
using Volo.Abp.PermissionManagement;
namespace YourNamespace;
[Dependency(ReplaceServices = true)]
public class BulkPermissionStore : PermissionStore
{
protected IDistributedCache<PermissionGrantBulkCacheItem> BulkCache { get; }
public BulkPermissionStore(
IPermissionGrantRepository permissionGrantRepository,
IDistributedCache<PermissionGrantCacheItem> cache,
IDistributedCache<PermissionGrantBulkCacheItem> bulkCache,
IPermissionDefinitionManager permissionDefinitionManager)
: base(permissionGrantRepository, cache, permissionDefinitionManager)
{
BulkCache = bulkCache;
}
protected override async Task<PermissionGrantCacheItem> GetCacheItemAsync(
string name, string providerName, string providerKey)
{
var bulkItem = await GetOrLoadBulkCacheItemAsync(providerName, providerKey);
return new PermissionGrantCacheItem(bulkItem.GrantedPermissions.Contains(name));
}
protected override async Task<List<KeyValuePair<string, PermissionGrantCacheItem>>> GetCacheItemsAsync(
string[] names, string providerName, string providerKey)
{
var bulkItem = await GetOrLoadBulkCacheItemAsync(providerName, providerKey);
var grantedSet = new HashSet<string>(bulkItem.GrantedPermissions);
return names
.Select(name => new KeyValuePair<string, PermissionGrantCacheItem>(
CalculateCacheKey(name, providerName, providerKey),
new PermissionGrantCacheItem(grantedSet.Contains(name))))
.ToList();
}
protected virtual async Task<PermissionGrantBulkCacheItem> GetOrLoadBulkCacheItemAsync(
string providerName, string providerKey)
{
var cacheKey = PermissionGrantBulkCacheItem.CalculateCacheKey(providerName, providerKey);
var cacheItem = await BulkCache.GetAsync(cacheKey);
if (cacheItem != null)
{
Logger.LogDebug("BulkPermissionStore: cache hit for {ProviderName}:{ProviderKey}.",
providerName, providerKey);
return cacheItem;
}
Logger.LogDebug("BulkPermissionStore: cache miss for {ProviderName}:{ProviderKey}, loading from DB.",
providerName, providerKey);
using (PermissionGrantRepository.DisableTracking())
{
var grantedPermissions = await PermissionGrantRepository.GetListAsync(providerName, providerKey);
cacheItem = new PermissionGrantBulkCacheItem(grantedPermissions.Select(p => p.Name));
}
await BulkCache.SetAsync(cacheKey, cacheItem);
Logger.LogDebug("BulkPermissionStore: cached {Count} granted permissions for {ProviderName}:{ProviderKey}.",
cacheItem.GrantedPermissions.Length, providerName, providerKey);
return cacheItem;
}
}
PermissionGrantBulkCacheItemInvalidator.cs
using System.Threading.Tasks;
using Volo.Abp.Caching;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Domain.Entities.Events;
using Volo.Abp.EventBus;
using Volo.Abp.MultiTenancy;
using Volo.Abp.PermissionManagement;
namespace YourNamespace;
public class PermissionGrantBulkCacheItemInvalidator :
ILocalEventHandler<EntityChangedEventData<PermissionGrant>>,
ITransientDependency
{
protected IDistributedCache<PermissionGrantBulkCacheItem> BulkCache { get; }
protected ICurrentTenant CurrentTenant { get; }
public PermissionGrantBulkCacheItemInvalidator(
IDistributedCache<PermissionGrantBulkCacheItem> bulkCache,
ICurrentTenant currentTenant)
{
BulkCache = bulkCache;
CurrentTenant = currentTenant;
}
public virtual async Task HandleEventAsync(EntityChangedEventData<PermissionGrant> eventData)
{
var cacheKey = PermissionGrantBulkCacheItem.CalculateCacheKey(
eventData.Entity.ProviderName,
eventData.Entity.ProviderKey);
using (CurrentTenant.Change(eventData.Entity.TenantId))
{
await BulkCache.RemoveAsync(cacheKey, considerUow: true);
}
}
}
Step 3 — No other changes needed
[Dependency(ReplaceServices = true)] on BulkPermissionStore tells ABP to automatically replace the default PermissionStore. PermissionGrantBulkCacheItemInvalidator is auto-registered via ITransientDependency. No module file changes are required.
After deployment, please check the logs for BulkPermissionStore: cache hit and cache miss messages to confirm it is working.
Thanks