Skip to content

Blazor Admin UI

Cargonerds.Blazor is the administration / internal console. It is an ABP Blazor "Web App" built on the LeptonX theme and rendered with the interactive WebAssembly render mode. In the Aspire model it is registered as the admin service.

Two cooperating projects

The admin UI is split across two projects — a thin ASP.NET Core host and the WebAssembly client that holds almost all of the UI.

Project SDK Role
src/Cargonerds.Blazor Microsoft.NET.Sdk.Web The host. Boots the ABP app, serves the Razor host page (App.razor), wires LeptonX WASM bundling, and serves a runtime-merged appsettings.json to the browser.
src/Cargonerds.Blazor.Client Microsoft.NET.Sdk.BlazorWebAssembly The WebAssembly client. Pages, routing, navigation, OIDC auth, MudBlazor + Blazorise setup, branding/theming, and all Cargonerds-specific admin pages (Bookings, Reports, Dashboards, OU management, API keys, CodeEntities).

The newer customer-facing UIs are the Next.js realtime app and the public web site (see Realtime (Next.js) Frontend); the Blazor app is the internal/admin console.

Render model: "Web App", not standalone WASM

The host calls AddRazorComponents().AddInteractiveWebAssemblyComponents() and the host page loads _framework/blazor.web.js — this is the .NET 8+ Blazor Web App unified hosting model, not the classic standalone Microsoft.AspNetCore.Components.WebAssembly bootstrap. Components run client-side only: both the root <Routes> component and <HeadOutlet> are rendered with new InteractiveWebAssemblyRenderMode(prerender: false), so prerendering is disabled.

The host's OnApplicationInitialization maps the components and pulls in every routable client assembly (src/Cargonerds.Blazor/CargonerdsBlazorModule.cs):

app.UseConfiguredEndpoints(builder =>
{
    builder
        .MapRazorComponents<App>()
        .AddInteractiveWebAssemblyRenderMode()
        .AddAdditionalAssemblies(
            WebAppAdditionalAssembliesHelper.GetAssemblies<CargonerdsBlazorClientModule>()
        );
});

WebAppAdditionalAssembliesHelper.GetAssemblies<CargonerdsBlazorClientModule>() (ABP) discovers all routable assemblies reachable from the client module. This is how Hub.UI and Pricing pages become routable from the host side. On the client side the same set is surfaced through AbpRouterOptions.AdditionalAssemblies, which Routes.razor feeds to the Blazor <Router>.

Prerendering is disabled

prerender: false is set in both src/Cargonerds.Blazor/Components/App.razor and src/Cargonerds.Blazor.Client/Routes.razor. Components that assume a server prerender pass won't get one.

Tech stack

Concern What is used
Hosting model Blazor Web App, interactive WebAssembly render mode (prerender: false)
Host SDK Microsoft.NET.Sdk.Web (Cargonerds.Blazor)
Client SDK Microsoft.NET.Sdk.BlazorWebAssembly (Cargonerds.Blazor.Client)
Theme ABP LeptonX (LeptonX theme)
Default layout LeptonXBlazorLayouts.SideMenu (client) + "side-menu" (host bundling)
Default style LeptonXStyleNames.Light
Component libraries Blazorise (Bootstrap 5 + Font Awesome) and MudBlazor (+ MudBlazor.Extensions)
DI container Autofac (host UseAutofac(); client AbpAutofacWebAssemblyModule)
Authentication OIDC (authorization-code flow) against Cargonerds.AuthServer
Localization ABP CargonerdsResource (see Localization)
HTTP API access Auto-generated typed proxies from CargonerdsHttpApiClientModule

The host wiring lives in src/Cargonerds.Blazor/CargonerdsBlazorModule.cs (a AbpModule); the client composition root is src/Cargonerds.Blazor.Client/CargonerdsBlazorClientModule.cs.

Architecture at a glance

flowchart TD
    Browser["Browser (WASM)"]
    subgraph HostProj["Cargonerds.Blazor (host, ASP.NET Core)"]
        App["App.razor host page<br/>blazor.web.js, loader, theme bridge"]
        Settings["/appsettings.json<br/>AppSettingsHandler merge"]
        Bundling["LeptonX *.Bundling modules<br/>side-menu layout"]
    end
    subgraph ClientProj["Cargonerds.Blazor.Client (WASM client)"]
        ClientMod["CargonerdsBlazorClientModule<br/>OIDC, Blazorise, MudBlazor, menu, toolbar"]
        Routes["Routes.razor<br/>Router + Auth + Mud providers"]
    end
    Hub["Hub.UI (UIBlazorModule)<br/>menu, toolbar, OData, search"]
    Pricing["Pricing.Blazor.WebAssembly"]
    Shared["Cargonerds.UI.Shared (RCL)<br/>white-label static assets"]
    AuthServer["Cargonerds.AuthServer<br/>OpenIddict"]
    Api["Cargonerds.HttpApi.Host"]

    Browser -->|GET host page| App
    Browser -->|GET /appsettings.json| Settings
    App --> Routes
    ClientMod --> Routes
    ClientMod -->|DependsOn| Hub
    ClientMod -->|DependsOn| Pricing
    ClientProj -->|references| Shared
    HostProj -->|references| Shared
    Browser -->|OIDC code flow| AuthServer
    Browser -->|REST proxies| Api

Module composition

The client module's [DependsOn(...)] list is the central composition root for the whole admin shell. Beyond the two Cargonerds-owned domain modules it pulls in roughly 18 ABP commercial/pro WASM modules and the HTTP API client module (from src/Cargonerds.Blazor.Client/CargonerdsBlazorClientModule.cs):

[DependsOn(
    typeof(PricingBlazorWebAssemblyModule),
    typeof(UIBlazorModule),                              // Hub.UI
    typeof(AbpSettingManagementBlazorWebAssemblyModule),
    typeof(AbpFeatureManagementBlazorWebAssemblyModule),
    typeof(AbpAutofacWebAssemblyModule),
    typeof(AbpAccountAdminBlazorWebAssemblyModule),
    typeof(AbpAccountPublicBlazorWebAssemblyModule),
    typeof(AbpIdentityProBlazorWebAssemblyModule),
    typeof(SaasHostBlazorWebAssemblyModule),
    typeof(ChatBlazorWebAssemblyModule),
    typeof(AbpOpenIddictProBlazorWebAssemblyModule),
    typeof(AbpAuditLoggingBlazorWebAssemblyModule),
    typeof(AbpGdprBlazorWebAssemblyModule),
    typeof(CmsKitProAdminBlazorWebAssemblyModule),
    typeof(TextTemplateManagementBlazorWebAssemblyModule),
    typeof(LanguageManagementBlazorWebAssemblyModule),
    typeof(FileManagementBlazorWebAssemblyModule),
    typeof(LeptonXThemeManagementBlazorWebAssemblyModule),
    typeof(AbpAspNetCoreComponentsWebAssemblyLeptonXThemeModule),
    typeof(CargonerdsHttpApiClientModule)
)]
public class CargonerdsBlazorClientModule : AbpModule

Modules surface inside the shell purely by dependency

Hub.UI (UIBlazorModule) and Pricing.Blazor.WebAssembly (PricingBlazorWebAssemblyModule) appear inside the admin shell only by being on this [DependsOn] list — there is no host-side wiring for them. Each contributing module adds its own assembly to AbpRouterOptions.AdditionalAssemblies and registers its own IMenuContributor, so its pages and menu entries appear automatically. See Hub module and Pricing module.

The host module (CargonerdsBlazorModule) instead depends on the matching *.Bundling modules (LeptonX, Account.Pro.Public, Saas.Host, Chat, AuditLogging, CmsKit.Pro.Admin, FileManagement) plus CargonerdsDomainSharedModule and AbpAspNetCoreMvcUiBundlingModule.

ConfigureServices orchestration

The client module's ConfigureServices is a sequence of focused private configuration methods, each wrapping one ABP options class or service registration:

ConfigureLocalization();          // AbpLocalizationOptions
ConfigureSettingManagement();     // SettingManagementComponentOptions
ConfigureAuthentication(builder); // AddOidcAuthentication
ConfigureHttpClient(context, environment);
ConfigureBlazorise(context);
ConfigureMudBlazor(context);
ConfigureRouter(context);         // AbpRouterOptions.AppAssembly
ConfigureToolbar(context);        // AbpToolbarOptions
ConfigureMenu(context);           // AbpNavigationOptions
ConfigureAutoMapper(context);     // AbpAutoMapperOptions
ConfigureCookieConsent(context);
ConfigureTheme();                 // LeptonX + layout hook

Key files and types

Host (Cargonerds.Blazor)

File Purpose
CargonerdsBlazorModule.cs AbpModule. Depends on the *.Bundling modules; adds Razor components, sets the LeptonX layout bundling parameter, builds the HTTP pipeline, maps /appsettings.json.
Program.cs Serilog + builder.AddServiceDefaults() (Aspire) + AddApplicationAsync<CargonerdsBlazorModule>().
Components/App.razor Razor host page. Title/favicon/loader driven by IOptionsMonitor<WhiteLabelingOptions>; loads MapLibre, MudBlazor CSS/JS, Hub.UI/hub*.css, the loader, and the subscribeToLeptonXTheme JS bridge.
AppSettingsHandler.cs partial class, mapped at /appsettings.json. Merges the server ClientConfiguration section + the public subset of WhiteLabelingOptions into wwwroot/appsettings.json.
CargonerdsStyleBundleContributor.cs Adds main.css as a minified bundle file. CargonerdsScriptBundleContributor.cs is an empty ConfigureBundle.
appsettings.aspire.json Token template ({admin}, {auth}, {api}) that Aspire substitutes into ClientConfiguration.

Client (Cargonerds.Blazor.Client)

File Purpose
CargonerdsBlazorClientModule.cs The composition root described above; configures every ABP options class and replaces ABP's generic ExtensionProperties<,> component.
Program.cs WebAssemblyHostBuilder.CreateDefault + AddApplicationAsync<CargonerdsBlazorClientModule>(o => o.UseAutofac()).
Routes.razor The router host: CascadingAuthenticationState + Router, AuthorizeRouteView with login/403/404 handling, an ErrorBoundary, the MudBlazor providers, Hub's <ConnectivityMonitor/>, ABP's <NotificationProvider/>, and the LeptonX→MudBlazor dark-mode bridge.
Navigation/CargonerdsMenuContributor.cs IMenuContributor: builds the Main + User menus and reorders foreign module groups.
Navigation/CargonerdsToolbarContributor.cs IToolbarContributor: inserts Hub's GlobalSearchBar and appends OrganizationToggle.
Navigation/CargonerdsMenus.cs Menu-name constants. CargonerdsRouteConstants.cs holds page routes.
CargonerdsBrandingProvider.cs [Dependency(ReplaceServices = true)] over DefaultBrandingProvider; AppName/LogoUrl from WhiteLabelingOptions.
MudLeptonTheme.cs MudTheme mirroring LeptonX CSS variables (light + dark), used by Routes.razor.
Components/Base/CargonerdsComponentBase.cs abstract CargonerdsComponentBase : HubComponentBase (which is AbpComponentBase). Sets CargonerdsResource as default localization, routes errors to Blazorise INotificationService, tracks caller-supplied parameters.
Components/ApplyWhiteLabeling.razor(.cs) Layout-hook component that sets the --lpx-logo-icon CSS var from WhiteLabelingOptions.Favicon.
Components/Layout/LeptonXFooter.razor @inherits Footer, replaces ABP's Footer; renders version + copyright/footer links from WhiteLabelingOptions.
Components/OrganizationToggle.razor(.cs) Multi-select OU tree toggle in the toolbar; uses Hub's ICurrentOrganizationAppService and raises FilterContextEvent.
Settings/CustomScriptSettingComponentContributor.cs ISettingComponentContributor adding a "CustomScript" settings group.
wwwroot/appsettings.json Client config local-dev fallbacks only (see Runtime configuration).

Authentication

The client authenticates through OpenIddict on the AuthServer using the standard AddOidcAuthentication (authorization-code flow). ProviderOptions is bound from the AuthServer configuration section, and four scopes are added on top (CargonerdsBlazorClientModule.ConfigureAuthentication):

builder.Services.AddOidcAuthentication(options =>
{
    builder.Configuration.Bind("AuthServer", options.ProviderOptions);
    options.UserOptions.NameClaim = OpenIddictConstants.Claims.Name;
    options.UserOptions.RoleClaim = OpenIddictConstants.Claims.Role;

    options.ProviderOptions.DefaultScopes.Add("Cargonerds");
    options.ProviderOptions.DefaultScopes.Add("roles");
    options.ProviderOptions.DefaultScopes.Add("email");
    options.ProviderOptions.DefaultScopes.Add("phone");
});

Name and role claims map to the standard OpenIddict claim names. The AuthServer section supplies Authority, ClientId (admin) and ResponseType (code) — note these come from the merged runtime appsettings.json, not necessarily the baked-in file (see below).

Routes.razor enforces authorization with CascadingAuthenticationState + AuthorizeRouteView: unauthenticated users hit <RedirectToLogin/>, while authenticated-but-unauthorized users see a localized 403 via ABP's ErrorView; unknown routes render a localized 404.

The AuthServer is configured as an OpenIddict application; see API Authentication and API Clients for the server side and the dynamic C# client proxies that CargonerdsHttpApiClientModule brings in.

Runtime client configuration (/appsettings.json merge)

The static wwwroot/appsettings.json holds only local dev fallbacks (Authority https://localhost:44345, ClientId: admin, ResponseType: code; RemoteServices.Default https://localhost:44354; App.SelfUrl https://localhost:44381). At runtime the host overrides these so the browser learns the correct endpoints from the server.

The host maps app.Map("/appsettings.json", AppSettingsHandler.Run). When the WASM app fetches its appsettings.json, AppSettingsHandler.BuildAppSettings (src/Cargonerds.Blazor/AppSettingsHandler.cs):

  1. reads the static wwwroot/appsettings.json,
  2. merges the server's ClientConfiguration section into the root (recursively, via MergeConfigurationSectionToRoot),
  3. merges only the [PublicConfiguration]-marked subset of WhiteLabelingOptions under a WhiteLabelingOptions key, using PublicConfigurationAttribute.GetPublicConfigurationPropertyPaths(...) plus a :\d+ array-index regex to match paths,
  4. serializes camelCase and returns it.

This is the bridge that lets the Aspire host inject runtime endpoints. appsettings.aspire.json contains the tokens Aspire substitutes:

{
  "ClientConfiguration": {
    "App": { "SelfUrl": "{admin}" },
    "AuthServer": {
      "Authority": "{auth}",
      "WellKnownConfigAddress": "{auth}/.well-known/openid-configuration"
    },
    "RemoteServices": {
      "Default": { "BaseUrl": "{api}" },
      "AbpAccountPublic": { "BaseUrl": "{api}" }
    }
  }
}

In src/Cargonerds.AppHost/Program.cs the host is registered as the admin service and gets the white-label settings path that Aspire substitutes:

var adminFrontend = builder.AddProjectWithDefaults<Cargonerds_Blazor>(
    CargonerdsConsts.Aspire.Service.Admin);

adminFrontend
    .IfRunMode(b => b.WithEndpoint("https", endpoint => endpoint.Port = RunModeServicePorts.AdminHttpsPort))
    .WithExternalHttpEndpoints()
    .WithHttpHealthCheck("/", 200)
    .WaitForIf(!skipWaitingInBackend, authServer, apiHost)
    .WithExplicitStartIf(explicitFrontendStart)
    .WithWhiteLabelSettingsPath();

See Aspire Integration for the full token-substitution and service-orchestration story, and appsettings reference / configuration reference for the client config keys.

Runtime config vs. static config

Editing only wwwroot/appsettings.json will mislead in Aspire/deployed runs. The effective AuthServer / RemoteServices / App.SelfUrl come from the server-merged /appsettings.json (AppSettingsHandler + appsettings.aspire.json token substitution). The static file is a local-dev fallback only.

White-label secret-leakage guard

WhiteLabelingOptions properties without [PublicConfiguration] (e.g. EntraLogins, which carries client secrets) are intentionally not shipped to the browser; AppSettingsHandler filters via PublicConfigurationAttribute. Adding a new sensitive option without omitting the attribute would expose it client-side.

The Main menu is assembled by three IMenuContributors registered in three different modules' AbpNavigationOptions. Ordering is coordinated purely by integer order values and CssClass = "mt-1 border-top" separators.

Contributor Module Adds (Main menu)
UIMenuContributor Hub.UI Insights, Shipments, Purchase Orders, Quotations, Container Tracking, Documents, Inbox (orders 5–20)
CargonerdsMenuContributor Cargonerds.Blazor.Client Home, Host/Tenant Dashboard, Bookings submenu (13), Reports (21), Files reorder, Administration items
PricingMenuContributor Pricing.Blazor no-op (quotation entries removed)

CargonerdsMenuContributor (src/Cargonerds.Blazor.Client/Navigation/CargonerdsMenuContributor.cs) does three notable things beyond adding its own items:

  • Reorders foreign module groups with context.Menu.SetSubItemOrder(...) (e.g. SaaS 90, CmsKit 91; and inside Administration: OpenIddict 3, Language 4, TextTemplate 5, AuditLogging 6, Settings 7).
  • Augments the Identity submenu: it finds IdentityProMenus.GroupName and inserts the extended OU-management page next to ABP's default "Organization Units" entry, falling back to a top-level Administration item if the Identity submenu isn't present (e.g. the user lacks access):
var identityMenu = context.Menu.FindMenuItem(IdentityProMenus.GroupName);
var orgManagementItem = new ApplicationMenuItem(
    CargonerdsMenus.OrganizationManagement,
    l["Menu:OrganizationManagement"],
    url: CargonerdsRouteConstants.OrganizationManagement,
    order: 5
).RequirePermissions(CargonerdsPermissions.OrganizationUnits.AdvancedManagement);

if (identityMenu != null)
    identityMenu.AddItem(orgManagementItem);
else
    administration.AddItem(orgManagementItem);
  • Gates items with .RequirePermissions(...) / .RequireAuthenticated() — e.g. CargonerdsPermissions.Dashboard.Host, CargonerdsPermissions.ApiKeys.ManageAll, HubPermissions.CodeEntities.View. Permission gating uses ABP's authorization system; see the permissions reference.

The User menu (ConfigureUserMenuAsync) links to AuthServer Account pages (Account/Manage, Account/SecurityLogs, Account/Sessions) using the AuthServer:Authority URL with target="_blank", plus in-app API-key and preferences entries.

CargonerdsToolbarContributor adds two custom ToolbarItems to StandardToolbars.Main:

if (context.Toolbar.Name == StandardToolbars.Main)
{
    context.Toolbar.Items.Insert(0, new ToolbarItem(typeof(GlobalSearchBar)));   // Hub.UI
    context.Toolbar.Items.Add(new ToolbarItem(typeof(OrganizationToggle)));
}

OrganizationToggle reads the org tree via Hub's ICurrentOrganizationAppService, persists the selection, and raises Hub's FilterContextEvent (and listens to OrganizationListChangedEvent) so Hub list pages re-filter when the selected organizations change.

The toolbar depends on Hub.UI

GlobalSearchBar and OrganizationToggle both come from Hub.UI and rely on Hub services (ICurrentOrganizationAppService, FilterContextEvent, GlobalSearchState). Removing the Hub dependency breaks the toolbar.

Theming (LeptonX + MudBlazor)

LeptonX is configured as a side-menu, Light-default theme, and a small bridge keeps MudBlazor's dark mode in sync with LeptonX's appearance setting.

  • Layout is set in two places (and must stay in sync): the host bundling parameter options.Parameters["LeptonXTheme.Layout"] = "side-menu" and the client LeptonXThemeBlazorOptions.Layout = LeptonXBlazorLayouts.SideMenu. Default style is LeptonXStyleNames.Light.
  • MudBlazor is added via AddMudServicesWithExtensions(assemblies) because MudBlazor.Extensions powers file-preview dialogs. MudLeptonTheme.Create() (src/Cargonerds.Blazor.Client/MudLeptonTheme.cs) reproduces LeptonX's CSS variables as a MudTheme. Semantic colors are shared across LeptonX styles (e.g. primary #355dff, DefaultBorderRadius = "0.5rem").
  • Light/dark bridge: App.razor injects a JS function subscribeToLeptonXTheme that forwards the native lpx:appearance-setting event to Blazor. Routes.razor reads the initial style from ILeptonXStyleProvider and flips MudThemeProvider's dark mode on each change:
[JSInvokable]
public void OnLeptonXThemeChanged(string theme)
{
    var isDark = IsDarkStyle(theme);
    if (_isDarkMode != isDark)
    {
        _isDarkMode = isDark;
        InvokeAsync(StateHasChanged);
    }
}

private static bool IsDarkStyle(string style)
    => style is LeptonXStyleNames.Dark or LeptonXStyleNames.Dim;

Two layout settings must stay in sync

LeptonXTheme.Layout (host bundling parameter, string "side-menu") and LeptonXThemeBlazorOptions.Layout (client, LeptonXBlazorLayouts.SideMenu) live in two different files. A comment in ConfigureTheme() explicitly warns to update both together.

White-labeling and the deeper theming story (WhiteLabelingOptions, branding provider, footer, layout hook, Cargonerds.UI.Shared assets) are covered in detail in Theming. In short, branding is applied three ways:

  • CargonerdsBrandingProvider replaces ABP's DefaultBrandingProvider (AppName / LogoUrl).
  • LeptonXFooter replaces ABP's Footer (version, copyright/footer links).
  • The ApplyWhiteLabeling layout hook (LayoutHooks.Body.First for StandardLayouts.Application) sets the --lpx-logo-icon CSS variable from WhiteLabelingOptions.Favicon.

Component library setup (Blazorise)

Blazorise is configured with Bootstrap 5 + Font Awesome providers and a license ProductToken:

context.Services.AddBlazorise(o => o.ProductToken = "…");
context.Services.AddBootstrap5Providers().AddFontAwesomeIcons();

ABP's BlazoriseUI components are used throughout. The generic Volo.Abp.BlazoriseUI.Components.ObjectExtending.ExtensionProperties<,> component is replaced with a Cargonerds variant — and, because ABP/Blazorise resolve some closed generics that the open-generic replacement alone doesn't cover, two closed types are also registered explicitly (OrganizationUnitCreateDto/OrganizationUnitUpdateDto paired with IdentityResource):

context.Services.Replace(
    ServiceDescriptor.Transient(
        typeof(Volo.Abp.BlazoriseUI.Components.ObjectExtending.ExtensionProperties<,>),
        typeof(Cargonerds.Blazor.Client.Components.ExtensionProperties<,>)
    )
);
// + explicit closed registrations for OU Create/Update × IdentityResource

Blazorise ProductToken is embedded in the WASM

The token is hardcoded in CargonerdsBlazorClientModule.ConfigureBlazorise and is therefore shipped inside the published WebAssembly payload.

WASM bundling, trimming & build behavior

  • Client csproj (Cargonerds.Blazor.Client.csproj): BlazorWebAssemblyLoadAllGlobalizationData=true; Debug disables WasmFingerprintAssets; Release sets CompressionEnabled=false, PublishTrimmed=false, TrimMode=partial.
  • Trimming is deliberately disabled. MudBlazor + MudBlazor.Extensions are also pinned as TrimmerRootAssembly (plus an ILLink.Descriptors.xml TrimmerRootDescriptor) to work around a production CtorNotLocated, MudBlazor.MudExpansionPanels error: Blazorise's IComponentActivator uses ActivatorUtilities/reflection while MudBlazor.Extensions renders Mud components dynamically via DynamicComponent + typeof(...), so the trimmer would otherwise strip their constructors. (The csproj comments documenting this are in German.)
  • Host csproj (Cargonerds.Blazor.csproj): CompressionEnabled=true; AOT is present but commented out.
  • The LeptonX/Account/Saas/Chat/AuditLogging/CmsKit/FileManagement WASM bundles are pulled in via the host's *.Bundling module dependencies, and AbpMvcLibsOptions.CheckLibs = false skips lib-manifest validation. See ABP bundling & minification.

Trimming is off on purpose

Re-enabling PublishTrimmed (or an SDK default doing so) reintroduces the CtorNotLocated, MudBlazor.MudExpansionPanels runtime failure. The TrimmerRootAssembly/ TrimmerRootDescriptor entries are belt-and-suspenders no-ops while trimming is off.

Cargonerds.UI.Shared (white-label RCL)

A minimal Razor Class Library (Microsoft.NET.Sdk.Razor, SupportedPlatform browser) whose only real payload is wwwroot static assets — a generic logo/favicon plus per-customer folders (e.g. wwwroot/rohlig/...) with css/styles.css, js/scripts.js, images and a background video. These are served at /_content/Cargonerds.UI.Shared/... and referenced by WhiteLabelingOptions.For(...) / ContentFor(...). The RCL is referenced by both the host and the client.

ABP cookie consent is enabled in the client module with policy URLs /CookiePolicy and /PrivacyPolicy:

context.Services.AddAbpCookieConsent(options =>
{
    options.IsEnabled = true;
    options.CookiePolicyUrl = "/CookiePolicy";
    options.PrivacyPolicyUrl = "/PrivacyPolicy";
});

Running

The admin UI is started automatically by the Aspire AppHost as the admin service. In local run mode it is pinned to a fixed HTTPS port (RunModeServicePorts.AdminHttpsPort):

https://localhost:44381

See Running Locally for the full Aspire workflow, or Debugging for running it standalone from an IDE.

ABP patterns used

This page is a concentrated example of several ABP UI/framework patterns:

  • ModularityAbpModule + [DependsOn(...)] compose the host from *.Bundling modules and the client from feature WASM modules; ConfigureServices / OnApplicationInitialization lifecycle hooks.
  • Configure<TOptions>(...) for AbpBundlingOptions, AbpMvcLibsOptions, RouteOptions, AbpNavigationOptions, AbpToolbarOptions, AbpRouterOptions, AbpLayoutHookOptions, AbpAutoMapperOptions, AbpLocalizationOptions, SettingManagementComponentOptions, LeptonXThemeOptions, LeptonXThemeBlazorOptions.
  • IMenuContributor (3 implementations) and IToolbarContributor for navigation.
  • Service replacement / DI override (dependency injection): context.Services.Replace(...) for ExtensionProperties<,>; [Dependency(ReplaceServices = true)] on CargonerdsBrandingProvider; [ExposeServices(typeof(Footer))] + [Dependency(ReplaceServices = true)] on LeptonXFooter.
  • Layout hooks (AbpLayoutHookOptions.Add(LayoutHooks.Body.First, ...)), ISettingComponentContributor (settings), and IBrandingProvider override.
  • Localization resources (CargonerdsResource with AddBaseTypes(AbpUiResource)) — see Localization.
  • ABP component base chain CargonerdsComponentBase : HubComponentBase : AbpComponentBase.
  • Auto-generated HTTP API client proxies via CargonerdsHttpApiClientModule.
  • Aspire ServiceDefaults (host AddServiceDefaults(); AppHost AddProjectWithDefaults).

For a cross-cutting catalogue of these patterns, see ABP Patterns and the Architecture Overview.

Gotchas

  • Two layout settings must stay in sync (LeptonXTheme.Layout host parameter vs. LeptonXThemeBlazorOptions.Layout client) — set in two files; a comment warns to update both.
  • Trimming is off on purpose; re-enabling it reintroduces the MudBlazor CtorNotLocated failure.
  • Prerendering is disabled (prerender: false) in both App.razor and Routes.razor.
  • Runtime config vs. static config — the static wwwroot/appsettings.json is a local-dev fallback; the effective endpoints are merged server-side by AppSettingsHandler.
  • White-label secret-leakage guard — only [PublicConfiguration] properties are shipped to the browser; non-public options (e.g. EntraLogins) are filtered out.
  • Blazorise ProductToken is hardcoded and embedded in the shipped WASM.
  • Pricing surfaces routes but no menuPricingMenuContributor is a no-op; Pricing pages are reachable by route and quotations are intentionally served under Hub.UI (/hub/quotations).
  • ExtensionProperties<,> needs explicit closed registrations beyond the open-generic replacement (OU create/update × IdentityResource).
  • MapLibre + MudBlazor assets load from CDN/unpkg in App.razor (maplibre-gl 5.10.0, Google Fonts) — external runtime dependencies of the admin shell.
  • OrganizationToggle / GlobalSearchBar come from Hub.UI, not Cargonerds — the toolbar depends on Hub services; removing the Hub dependency breaks it.