Activities of "maliming"

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) → 982ms
  • runtime.js (25.5 kB) → 605ms
  • Font files → 562–704ms

These 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-age to 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 user
  • R:{roleName} — permissions for that role (shared across all users with the same role)
  • C:{clientName} — permissions for that client

Why 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:

  • Warm cache: 4,233 individual Redis HMGET commands (~650ms)
  • Cold cache: 4,233 HMGET + DB query + 4,233 HSET + 4,233 EXPIRE (~10+ seconds)

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", ...]

  • Warm cache: 1 Redis GET
  • Cold cache: 1 Redis GET + 1 DB query + 1 Redis SET
  • Cache invalidation when permissions change: works correctly across all instances via Redis ✅

Step 1 — Revert your previous workarounds

In TMSMSAdministrationServiceModule.cs:

  1. Re-add typeof(AbpCachingStackExchangeRedisModule) to your module dependencies if you removed it
  2. Remove the WebRemoteDynamicClaimsPrincipalContributorOptions.IsEnabled = false line from PreConfigureServices

Step 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

hi

What is your abp package version?

I will share some code with you. Thanks.

Hi,

I've analyzed your admin server logs. Your AdministrationService has 4,233 permissions registered from multiple microservices. This is expected in the microservice template with Dynamic Permission Store enabled.

From the logs:

  • Cache hit: ~650ms (e.g., request at 05:48:03)
  • Cache miss: 10+ seconds (e.g., request at 05:44:45)

We suspect the bottleneck is the Redis cache operations with this large number of permissions. To confirm, could you temporarily disable Redis caching and use in-memory cache instead to test the same API call?

You can do this by removing the AbpCachingStackExchangeRedisModule dependency and the related Redis configuration from your AdministrationService, then compare the response time.

This will help us confirm the root cause before providing a solution.

Thanks

hi

The recommended approach is to extend the built-in OpenLdapManager and LdapExternalLoginProvider to add tenant validation after LDAP authentication succeeds.

Step 1: Extend OpenLdapManager to query the user's organizational unit from LDAP

Following the same pattern as the existing GetUserEmailAsync method:

using System.Linq;
using System.Threading.Tasks;
using LdapForNet;
using Microsoft.Extensions.Logging;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Identity.ExternalLoginProviders.Ldap;
using Volo.Abp.Ldap;

namespace YourProject;

[Dependency(ReplaceServices = true)]
[ExposeServices(typeof(OpenLdapManager), typeof(ILdapManager), typeof(LdapManager))]
public class CustomOpenLdapManager : OpenLdapManager
{
    public CustomOpenLdapManager(ILdapSettingProvider ldapSettingProvider)
        : base(ldapSettingProvider)
    {
    }

    public virtual async Task<string> GetUserOrganizationalUnitAsync(string userName)
    {
        using (var conn = await CreateLdapConnectionAsync())
        {
            await AuthenticateLdapConnectionAsync(
                conn,
                await NormalizeUserNameAsync(await LdapSettingProvider.GetUserNameAsync()),
                await LdapSettingProvider.GetPasswordAsync());

            var searchResults = await conn.SearchAsync(
                await GetBaseDnAsync(),
                await GetUserFilterAsync(userName));

            try
            {
                var userEntry = searchResults.First();
                return GetUserOrganizationalUnit(userEntry);
            }
            catch (LdapException e)
            {
                Logger.LogException(e);
            }

            return null;
        }
    }

    protected virtual string GetUserOrganizationalUnit(LdapEntry ldapEntry)
    {
        // Option 1: Read "ou" or "department" attribute directly from the LDAP entry
        var ou = ldapEntry.ToDirectoryEntry().GetAttribute("ou")?.GetValue<string>();
        if (!ou.IsNullOrWhiteSpace())
        {
            return ou;
        }

        // Option 2: Parse OU from the user's DN
        // e.g. "uid=john,ou=Unit1,dc=company,dc=com" -> "Unit1"
        var dn = ldapEntry.Dn;
        var ouPart = dn?.Split(',')
            .FirstOrDefault(p => p.Trim().StartsWith("ou=", System.StringComparison.OrdinalIgnoreCase));

        return ouPart?.Split('=').LastOrDefault()?.Trim();
    }
}

Step 2: Create a tenant-restricted LDAP login provider

using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Options;
using Volo.Abp.Features;
using Volo.Abp.Guids;
using Volo.Abp.Identity;
using Volo.Abp.Identity.ExternalLoginProviders.Ldap;
using Volo.Abp.Ldap;
using Volo.Abp.MultiTenancy;
using Volo.Abp.Settings;

namespace YourProject;

public class TenantRestrictedLdapExternalLoginProvider : LdapExternalLoginProvider
{
    protected ITenantStore TenantStore { get; }

    public TenantRestrictedLdapExternalLoginProvider(
        IGuidGenerator guidGenerator,
        ICurrentTenant currentTenant,
        IdentityUserManager userManager,
        IIdentityUserRepository identityUserRepository,
        OpenLdapManager ldapManager,
        ILdapSettingProvider ldapSettingProvider,
        IFeatureChecker featureChecker,
        ISettingProvider settingProvider,
        IOptions<IdentityOptions> identityOptions,
        ITenantStore tenantStore)
        : base(guidGenerator, currentTenant, userManager, identityUserRepository,
            ldapManager, ldapSettingProvider, featureChecker, settingProvider, identityOptions)
    {
        TenantStore = tenantStore;
    }

    public override async Task<bool> TryAuthenticateAsync(string userName, string plainPassword)
    {
        // First, perform standard LDAP authentication
        var isAuthenticated = await base.TryAuthenticateAsync(userName, plainPassword);
        if (!isAuthenticated)
        {
            return false;
        }

        // Skip tenant validation for host users
        if (CurrentTenant.Id == null)
        {
            return true;
        }

        // Query LDAP for user's organizational unit
        var customLdapManager = (CustomOpenLdapManager)LdapManager;
        var userOu = await customLdapManager.GetUserOrganizationalUnitAsync(userName);
        if (userOu.IsNullOrWhiteSpace())
        {
            Logger.LogWarning($"Could not determine organizational unit for LDAP user: {userName}");
            return false;
        }

        // Get current tenant info
        var tenant = await TenantStore.FindAsync(CurrentTenant.Id.Value);
        if (tenant == null)
        {
            return false;
        }

        // Compare user's OU with the current tenant
        if (!IsUserAllowedForTenant(userOu, tenant.Name))
        {
            Logger.LogWarning(
                $"LDAP user '{userName}' belongs to OU '{userOu}' and is not allowed for tenant '{tenant.Name}'");
            return false;
        }

        return true;
    }

    protected virtual bool IsUserAllowedForTenant(string userOu, string tenantName)
    {
        // Simple approach: tenant name matches OU name (case-insensitive)
        // Customize this mapping based on your business requirements
        return string.Equals(userOu, tenantName, System.StringComparison.OrdinalIgnoreCase);
    }
}

Step 3: Register the custom provider in your module

public override void ConfigureServices(ServiceConfigurationContext context)
{
    // Replace the default LdapExternalLoginProvider with our tenant-restricted version
    Configure<AbpIdentityOptions>(options =>
    {
        options.ExternalLoginProviders.Remove(LdapExternalLoginProvider.Name);
        options.ExternalLoginProviders.Add<TenantRestrictedLdapExternalLoginProvider>(LdapExternalLoginProvider.Name);
    });
}

How it works:

  1. User submits credentials on the login page (with a tenant selected).
  2. AbpSignInManager.PasswordSignInAsync iterates external login providers and calls TryAuthenticateAsync.
  3. Our custom provider first validates credentials against LDAP (via base.TryAuthenticateAsync).
  4. If the current context is host (CurrentTenant.Id == null), it allows login directly.
  5. Otherwise, it queries LDAP for the user's OU using CustomOpenLdapManager.GetUserOrganizationalUnitAsync.
  6. It compares the OU with the current tenant's name via IsUserAllowedForTenant.
  7. Returns false if there's a mismatch, which blocks the sign-in.

Notes:

  • The CustomOpenLdapManager is automatically injected into TenantRestrictedLdapExternalLoginProvider because OpenLdapManager is replaced in the DI container via the [Dependency(ReplaceServices = true)] attribute.
  • The GetUserOrganizationalUnit method provides two strategies for determining the user's OU: reading a direct LDAP attribute (ou) or parsing the OU from the user's DN. You should adjust this based on your LDAP directory structure.
  • The IsUserAllowedForTenant method uses a simple name comparison. If your tenant names don't match OU names directly, you can implement a custom mapping (e.g., from a configuration file or database table).
  • LDAP settings in ABP are tenant-aware, but since you use a single LDAP service, make sure the LDAP connection settings (host, port, base DN, etc.) are configured at the host level so all tenants share the same configuration.

Thanks

Showing 691 to 700 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.