: )
Hi,
The root cause is that you are using the old _Host.cshtml hosting model with render-mode="Server", which does not have server-side prerendering. This means the page body is empty until the Blazor SignalR circuit is established.
<leptonx-theme-appearance />: The LeptonX theme variant CSS (dark/dim/light) is loaded dynamically after Blazor connects, causing the FOUC.<leptonx-theme-appearance />: The theme CSS is loaded in the initial HTML (including background color), but the body content is still empty waiting for Blazor to connect, which causes the black screen.ABP 9.x uses the new Blazor Web App pattern with App.razor instead of _Host.cshtml. The new pattern uses @rendermode="InteractiveServer" which has SSR prerendering by default — the initial HTML response already contains both the CSS and the rendered content, so there is no FOUC or black screen.
Here is the App.razor structure used in ABP 9.x Blazor Server projects with LeptonX:
I'd recommend creating a fresh ABP 9.1.3 Blazor Server project using ABP CLI or ABP Studio, then compare its structure with your current project. The new project will use App.razor by default and should not have the FOUC / black screen issue. You can use it as a reference to migrate your existing project.
Thanks
Hi,
Could you share the following details so we can better assist you?
_Host.cshtml hosting model or the newer App.razor (Blazor Web App) pattern?Thanks
Hi,
Thanks for confirming. Before we go the reproduction project route, we found two more bottlenecks in the code and have optimizations ready for you to try.
We analyzed the code path that runs after the permission cache is loaded, and found:
The built-in PermissionStore.IsGrantedAsync(string[]) takes the cached results and calls FormattedStringValueExtracter.Extract to parse each permission name from the cache key — with 3 providers (User, Role, Client) × 4000+ permissions, that's ~12,000 string parsing operations per request, even when the cache is warm.
The built-in PermissionChecker.IsGrantedAsync(string[]) looks up each permission definition individually (4000+ async calls) instead of loading them all at once.
Please make the following changes:
1. Replace your existing BulkPermissionStore.cs with this updated version (added IsGrantedAsync override):
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Volo.Abp;
using Volo.Abp.Authorization.Permissions;
using Volo.Abp.Caching;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Domain.Repositories;
using Volo.Abp.PermissionManagement;
[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();
}
public override async Task<MultiplePermissionGrantResult> IsGrantedAsync(
string[] names, string providerName, string providerKey)
{
Check.NotNullOrEmpty(names, nameof(names));
var result = new MultiplePermissionGrantResult();
var bulkItem = await GetOrLoadBulkCacheItemAsync(providerName, providerKey);
var grantedSet = new HashSet<string>(bulkItem.GrantedPermissions);
foreach (var name in names)
{
result.Result.Add(name,
grantedSet.Contains(name)
? PermissionGrantResult.Granted
: PermissionGrantResult.Undefined);
}
return result;
}
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;
}
}
2. Add this new file OptimizedPermissionChecker.cs:
using System.Collections.Generic;
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));
var result = new MultiplePermissionGrantResult();
if (!names.Any())
{
return result;
}
var multiTenancySide = CurrentTenant.GetMultiTenancySide();
var allPermissions = (await PermissionDefinitionManager.GetPermissionsAsync())
.ToDictionary(p => p.Name);
var permissionDefinitions = new List<PermissionDefinition>();
var permissionsNeedingStateCheck = 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))
{
if (permission.StateCheckers.Any())
{
permissionsNeedingStateCheck.Add(permission);
}
else
{
permissionDefinitions.Add(permission);
}
}
}
if (permissionsNeedingStateCheck.Any())
{
var stateCheckResult = await StateCheckerManager.IsEnabledAsync(
permissionsNeedingStateCheck.ToArray());
foreach (var permission in permissionsNeedingStateCheck)
{
if (stateCheckResult[permission])
{
permissionDefinitions.Add(permission);
}
}
}
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;
}
}
return result;
}
}
The other two files (PermissionGrantBulkCacheItem.cs and PermissionGrantBulkCacheItemInvalidator.cs) remain unchanged. Cache invalidation still works the same way.
Please try these changes first and share the application-configuration response time. If the improvement is still not enough, we can proceed with the reproduction project approach.
Thanks
Hi,
Thanks for testing locally! The good news is that application-localization is now only 113ms locally, which confirms localization itself is not a problem — the 1.43s you saw earlier was mainly network latency.
However, application-configuration is still taking 1.45s locally, so let me ask a few questions:
BulkPermissionStore deployed in this local environment? Can you check the logs for BulkPermissionStore: cache hit or BulkPermissionStore: cache miss messages?Also, would it be possible for you to provide a simple reproduction project that demonstrates this issue? It doesn't need to include your full business logic — just the basic setup with the same number of permissions and the same service configuration. This would allow me to download it and debug locally to pinpoint exactly where the time is being spent.
Thanks
Hi,
The easiest way to add an additional ABP-preconfigured Web project to your existing tiered solution is to use ABP CLI to generate a new tiered solution as a scaffold, then copy the Web project into your existing solution.
Here's the approach:
Use ABP CLI to create a new tiered MVC solution (e.g., abp new MyCompany.Public -t app -u mvc --tiered). This gives you a fully preconfigured Web project with all ABP module dependencies, OIDC configuration, HttpApi.Client references, theme integration, etc.
Copy the generated *.Web project folder into your existing solution and add it to your .sln file.
Update appsettings.json in the new Web project to point to your existing services:
AuthServer:Authority → your existing AuthServer URLRemoteServices:Default:BaseUrl → your existing HttpApi.Host URLApp:SelfUrl → the new Web project's own URLRegister a new OpenID Connect client in your existing AuthServer for the new Web project (with its own ClientId, redirect URI, and post-logout redirect URI).
If you used the same company/project name when generating the new solution, the generated Web project will have the same namespace as your existing Web project. In that case, you'll need to rename the project and adjust the namespaces (e.g., MyCompany.Web → MyCompany.PublicWeb). Alternatively, you can generate the new solution with a different name (e.g., MyCompany.Public) so the Web project comes with a distinct namespace out of the box.
This approach is much more reliable than manually creating an ABP Web project from scratch, since the generated project already has all the correct module dependencies, OIDC middleware setup, HttpApi.Client references, and theme configuration in place.
Thanks
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.