Hi,
You're right — provideCmsKitAdminConfig() and provideCmsKitProAdminConfig() both drag the heavy admin entry into the initial bundle, exactly as you described. The chain runs through setting-tab.provider.ts, which top-level imports CmsSettingsComponent from @abp/ng.cms-kit/admin / @volo/abp.ng.cms-kit-pro/admin. Those entries export * from ./components, and a few of those components (page-form, blog-post-form, global-resources, and Pro's faq-question-modal) statically import ToastuiEditorComponent and CodeMirrorEditorComponent. So registering the menu eagerly is enough to anchor the editor + grids in the initial graph, even when the actual /cms routes are lazy-loaded.
We'll track this and move the menu/route registration out of the heavy entry in a future release — the model @volo/abp.ng.identity/config already uses, where IdentitySettingsComponent lives inside the config entry itself.
In the meantime, drop this file into your app and call these in place of provideCmsKitAdminConfig() / provideCmsKitProAdminConfig():
import { eLayoutType, RoutesService } from '@abp/ng.core';
import {
EnvironmentProviders,
inject,
makeEnvironmentProviders,
provideAppInitializer,
} from '@angular/core';
// Enum values inlined from @abp/ng.cms-kit/admin/config and
// @volo/abp.ng.cms-kit-pro/admin/config. Importing any symbol from those
// entries pulls setting-tab.provider.ts in, which top-level imports
// CmsSettingsComponent from the heavy admin entry — that's the import that
// drags the editor + grids into the initial bundle.
const CmsKitRouteNames = {
Cms: 'CmsKit::Menu:CMS',
BlogPosts: 'CmsKit::BlogPosts',
Blogs: 'CmsKit::Blogs',
Comments: 'CmsKit::CmsKit.Comments',
GlobalResources: 'CmsKit::GlobalResources',
Menus: 'CmsKit::Menus',
Pages: 'CmsKit::Pages',
Tags: 'CmsKit::CmsKit.Tags',
} as const;
const CmsKitPolicyNames = {
BlogPosts: 'CmsKit.BlogPosts',
Blogs: 'CmsKit.Blogs',
Comments: 'CmsKit.Comments',
GlobalResources: 'CmsKit.GlobalResources',
Menus: 'CmsKit.Menus',
Pages: 'CmsKit.Pages',
Tags: 'CmsKit.Tags',
} as const;
const CmsKitProRouteNames = {
Cms: 'CmsKit::Menu:CMS',
Newsletters: 'CmsKit::NewsletterUsers',
Polls: 'CmsKit::Polls',
Faqs: 'CmsKit::Faqs',
PageFeedbacks: 'CmsKit::PageFeedbacks',
UrlShorting: 'CmsKit::UrlForwarding',
} as const;
const CmsKitProPolicyNames = {
Cms: 'CmsKit.Newsletter || CmsKit.Poll || CmsKit.Faq || CmsKit.PageFeedback || CmsKit.UrlShorting',
Newsletters: 'CmsKit.Newsletter',
Polls: 'CmsKit.Poll',
Faqs: 'CmsKit.Faq',
PageFeedbacks: 'CmsKit.PageFeedback',
UrlShorting: 'CmsKit.UrlShorting',
} as const;
export function provideCmsKitAdminRoutesLight(): EnvironmentProviders {
return makeEnvironmentProviders([
provideAppInitializer(() => {
const routes = inject(RoutesService);
routes.add([
{ path: '/cms/blog-posts', name: CmsKitRouteNames.BlogPosts, parentName: CmsKitRouteNames.Cms, order: 1, layout: eLayoutType.application, iconClass: 'fa fa-file-alt', requiredPolicy: CmsKitPolicyNames.BlogPosts },
{ path: '/cms/blogs', name: CmsKitRouteNames.Blogs, parentName: CmsKitRouteNames.Cms, order: 2, layout: eLayoutType.application, iconClass: 'fa fa-blog', requiredPolicy: CmsKitPolicyNames.Blogs },
{ path: '/cms/comments', name: CmsKitRouteNames.Comments, parentName: CmsKitRouteNames.Cms, order: 3, layout: eLayoutType.application, iconClass: 'fa fa-comments', requiredPolicy: CmsKitPolicyNames.Comments },
{ path: '/cms/global-resources', name: CmsKitRouteNames.GlobalResources, parentName: CmsKitRouteNames.Cms, order: 5, layout: eLayoutType.application, iconClass: 'fa fa-globe', requiredPolicy: CmsKitPolicyNames.GlobalResources },
{ path: '/cms/menus', name: CmsKitRouteNames.Menus, parentName: CmsKitRouteNames.Cms, order: 6, layout: eLayoutType.application, iconClass: 'fa fa-bars', requiredPolicy: CmsKitPolicyNames.Menus },
{ path: '/cms/pages', name: CmsKitRouteNames.Pages, parentName: CmsKitRouteNames.Cms, order: 9, layout: eLayoutType.application, iconClass: 'fa fa-file', requiredPolicy: CmsKitPolicyNames.Pages },
{ path: '/cms/tags', name: CmsKitRouteNames.Tags, parentName: CmsKitRouteNames.Cms, order: 11, layout: eLayoutType.application, iconClass: 'fa fa-tags', requiredPolicy: CmsKitPolicyNames.Tags },
]);
}),
]);
}
export function provideCmsKitProAdminRoutesLight(): EnvironmentProviders {
return makeEnvironmentProviders([
provideAppInitializer(() => {
const routes = inject(RoutesService);
routes.add([
{ path: '/cms', name: CmsKitProRouteNames.Cms, order: 100, layout: eLayoutType.application, iconClass: 'fa fa-newspaper', requiredPolicy: CmsKitProPolicyNames.Cms },
{ path: '/cms/faqs', name: CmsKitProRouteNames.Faqs, parentName: CmsKitProRouteNames.Cms, order: 4, layout: eLayoutType.application, iconClass: 'fa fa-question-circle', requiredPolicy: CmsKitProPolicyNames.Faqs },
{ path: '/cms/newsletters', name: CmsKitProRouteNames.Newsletters, parentName: CmsKitProRouteNames.Cms, order: 7, layout: eLayoutType.application, iconClass: 'fa fa-newspaper', requiredPolicy: CmsKitProPolicyNames.Newsletters },
{ path: '/cms/page-feedbacks', name: CmsKitProRouteNames.PageFeedbacks, parentName: CmsKitProRouteNames.Cms, order: 8, layout: eLayoutType.application, iconClass: 'fa fa-comment', requiredPolicy: CmsKitProPolicyNames.PageFeedbacks },
{ path: '/cms/polls', name: CmsKitProRouteNames.Polls, parentName: CmsKitProRouteNames.Cms, order: 10, layout: eLayoutType.application, iconClass: 'fa fa-poll', requiredPolicy: CmsKitProPolicyNames.Polls },
{ path: '/cms/url-shorting', name: CmsKitProRouteNames.UrlShorting, parentName: CmsKitProRouteNames.Cms, order: 12, layout: eLayoutType.application, iconClass: 'fa fa-forward', requiredPolicy: CmsKitProPolicyNames.UrlShorting },
]);
}),
]);
}
A few things to keep in mind when wiring this up:
import anything from @abp/ng.cms-kit/admin/config or @volo/abp.ng.cms-kit-pro/admin/config in this file (or anywhere else loaded eagerly). The moment you import any symbol from those entries — even the enum — the whole fesm comes in and brings CmsSettingsComponent (and the heavy admin entry) with it./cms parent menu node (the OSS provider only registers child items hanging off parentName: 'CmsKit::Menu:CMS'). If you ever stop calling provideCmsKitProAdminRoutesLight(), add an equivalent top-level entry yourself, otherwise the OSS children won't have a parent to attach to. Both Cms names resolve to the same key, so the two route sets render as a single "CMS" group when both are registered.Require comment approval toggle; the Pro tab renders Contact settings and Page Feedback settings. The easiest replacement is to expose the switches you actually use from your own settings page, or call the underlying APIs (CommentAdminService.updateSettings from @abp/ng.cms-kit/proxy, ContactSettingService.update and PageFeedbackAdminService.updateSettings from @volo/abp.ng.cms-kit-pro/proxy) directly.loadChildren for @abp/ng.cms-kit/admin / @volo/abp.ng.cms-kit-pro/admin — those still serve the actual pages lazily.In a stock 10.4.1 + Angular 21.2 + @angular/build project this brings the production initial bundle from 3.13 MB / 738 kB transfer down to 1.84 MB / 424 kB transfer (−1.29 MB / −42%), with the 940 kB toastui chunk moving from initial to lazy.
Thanks
Great
Hi,
Sounds good. Once you've had a chance to try it, let us know how it goes. If anything doesn't work as expected, share whatever details you can — timings, a small repro, or send the code via email if that's easier — and we'll keep digging on our side.
Thanks
Hi,
Could you share _installer.InstallAsync? We'd like to see exactly how the rows are being written — IRepository<T>.InsertAsync in a loop, InsertManyAsync, _dbContext.AddRange + SaveChangesAsync, or something else.
One thing to flag up front — [DisableEntityChangeTracking] and repository.DisableTracking() only affect read queries, not InsertAsync / UpdateAsync. The docs are explicit about this:
Change tracking behavior doesn't affect tracking entity objects returned from
InsertAsyncandUpdateAsyncmethods.
So those attributes won't change anything on the insert path.
Here's an approach you can try — a small replacement for IEntityChangeEventHelper that suppresses entity events for an explicit using scope. Could you drop this into your project, wrap the import call site like below, and let us know whether CompleteAsync drops to something reasonable on your 1M case?
[Dependency(ReplaceServices = true)]
[ExposeServices(typeof(IEntityChangeEventHelper), typeof(EntityChangeEventHelper))]
public class ScopedEntityChangeEventHelper : EntityChangeEventHelper
{
private static readonly AsyncLocal<bool> _suppressed = new();
public ScopedEntityChangeEventHelper(
IUnitOfWorkManager uowManager,
IEntityToEtoMapper etoMapper,
IOptions<AbpDistributedEntityEventOptions> options)
: base(uowManager, etoMapper, options) { }
public static IDisposable Suppress()
{
var prev = _suppressed.Value;
_suppressed.Value = true;
return new DisposeAction(() => _suppressed.Value = prev);
}
public override void PublishEntityCreatedEvent(object entity)
{
if (!_suppressed.Value) base.PublishEntityCreatedEvent(entity);
}
public override void PublishEntityUpdatedEvent(object entity)
{
if (!_suppressed.Value) base.PublishEntityUpdatedEvent(entity);
}
public override void PublishEntityDeletedEvent(object entity)
{
if (!_suppressed.Value) base.PublishEntityDeletedEvent(entity);
}
}
Usage at your import call site — _installer.InstallAsync itself stays unchanged:
using var _ = ScopedEntityChangeEventHelper.Suppress();
using var uow = _unitOfWorkManager.Begin(requiresNew: true, isTransactional: false);
await _installer.InstallAsync(data);
await uow.CompleteAsync();
The effect is scoped to the using block, so the rest of your app keeps behaving normally.
To be clear about what this does and doesn't touch — it only intercepts the per-entity broadcast events (EntityCreatedEventData<T> / EntityUpdatedEventData<T> / EntityDeletedEventData<T> and their distributed Eto counterparts). Everything else ABP normally does for tracked entities keeps running: CreationTime / CreatorId / LastModificationTime / ConcurrencyStamp are still set automatically, multi-tenancy and soft-delete still work, and any domain events you raise yourself with entity.AddLocalEvent(...) / AddDistributedEvent(...) go through a different pipeline and are not affected.
If you do have any local or distributed event handlers subscribed to EntityCreatedEventData<T> / EntityCreatedEto<T> for the entities being inserted, those will be skipped during the import — usually fine for a bulk install, but worth confirming on your side.
Thanks
Hi Stefano,
Glad to hear SaveTokens = false brought the app back. That's actually the right fix here — let me explain why so the picture is complete.
I was looking at the wrong cookie in my previous reply. The 8.8KB you saw is the External cookie (.AspNetCore.Identity.External, often chunked into Identity.ExternalC1 / C2), not the Application cookie. That External cookie is short-lived — it's created when Okta's callback hits /signin-okta, lives just long enough to ride one redirect to /Account/Login?handler=ExternalLoginCallback, and then ABP signs it out as part of the external login flow. But that one redirect is the request where Azure App Service's header limit gets hit, which is where the 502 came from.
SaveTokens = true takes Okta's id_token and access_token and stores them inside that External cookie's AuthenticationProperties. Okta's tokens can each be a few KB on their own (especially when the access_token or id_token carries a lot of claims — groups, profile attributes, app-specific scopes, etc.). Once the cookie gets serialized, encrypted, and chunked, it lands in the 6-9KB range, which lines up with what you saw. Setting SaveTokens = false strips the tokens out and the cookie drops to a few hundred bytes.
So please ignore my previous questions about role counts and custom claims — that line of investigation doesn't apply to your case. Sorry for the noise.
A couple of things to keep in mind going forward:
As long as your server-side code doesn't need to call Okta APIs (/userinfo, Okta Management API, etc.) on behalf of the user, SaveTokens = false is a clean permanent fix. From your CalendarWebModule it doesn't look like you're using the tokens on the server, so you're good.
If you ever do need server-side access to Okta tokens later, two options:
id_token / access_token from the Okta authorization server configuration. Often you can cut a lot just by not embedding the full groups list, or by moving large attributes out of the token.SaveTokens = false, then in the Okta scheme use options.Events.OnTokenValidated to copy the tokens into IDistributedCache (Redis on Azure for multi-instance setups) keyed by user, and read them from there in your code. Keeps the cookie small while still giving you tokens on the server.Thanks
Hi,
The default abp login uses the OIDC authorization code flow, which tries to open a browser on the local machine. On a headless Linux server there is no browser to open, so gio open / xdg-open fail.
Use the device code flow instead:
abp login --device
You will see something like:
[INF] Open your browser, go to: "https://account.abp.io/connect/verify"
[INF] and enter the following one-time code:
[INF] 4071-9666-6235
Open the URL on any device with a browser (e.g. your laptop), enter the code, sign in, and the CLI on the Linux box will pick up the login automatically.
Thanks
Hi,
Pass --skip-extension-version-check (or -sevc) to abp install-libs. install-libs doesn't use the Studio extensions at all, so the auto-update/respawn path can be safely skipped and the step will run synchronously without the second run or the continueOnError trick.
We'll also add install-libs to the auto-skip list in the next CLI release, so the flag won't be needed.
Thanks
Hi,
Let me start with what's actually in that 8.8KB cookie so we can narrow down where it's coming from.
The Okta tokens and claims don't end up in the application cookie at all. ABP's external login callback signs out the External cookie right after ExternalLoginSignInAsync succeeds, and then SignInManager.SignInAsync(user, false) creates a fresh Application cookie from the local user record only. So SaveTokens = true on the Okta scheme isn't what's growing it, and neither is anything else in the OIDC config.
What goes into the Application cookie on sign-in is:
role claim for every role the user is inAbpUserClaims for that userAbpRoleClaims for every role that user hasDynamic claims (UseDynamicClaims() and IsDynamicClaimsEnabled = true) only enrich the principal per request — they never write to the cookie, so that feature isn't growing it either.
The first item is roughly the same in any ABP project. The other three are where 8KB+ typically comes from. To narrow it down, could you tell us:
AbpUserClaims / AbpRoleClaims?Once we have those answers we can pick the right next step.
Thanks
Hi,
To narrow this down, could you share a few things:
MyApplication.Domain.Shared.csproj (full file).MyApplicationDomainSharedModule.cs — specifically the ConfigureServices method./api/abp/application-localization?cultureName=en — we'd like to see whether other resources (like AbpIdentity, AbpUi, etc.) also have empty texts, or only your MyApplication resource.In the meantime, you can try this. Your symptom usually comes from Microsoft.Extensions.FileProviders.Embedded being missing or pruned in Domain.Shared after upgrading across .NET versions. Without that package, the manifest XML isn't embedded into your Domain.Shared.dll, ABP's virtual file system can't find your en.json, and the endpoint returns texts: {} while the resource itself is still registered (which matches what you're seeing — the resource shows up with the correct baseResources, just no texts).
Make sure MyApplication.Domain.Shared.csproj has all three pieces below:
<PropertyGroup>
<GenerateEmbeddedFilesManifest>true</GenerateEmbeddedFilesManifest>
</PropertyGroup>
<ItemGroup>
<EmbeddedResource Include="Localization\MyApplication\*.json" />
<Content Remove="Localization\MyApplication\*.json" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.FileProviders.Embedded" Version="10.0.7" />
</ItemGroup>
Then run dotnet clean && dotnet build and restart the host. The MyApplication resource should start returning its texts.
If all three pieces are already in place, send over the csproj and module file and we'll dig deeper.
Thanks