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):
- reads the static
wwwroot/appsettings.json, - merges the server's
ClientConfigurationsection into the root (recursively, viaMergeConfigurationSectionToRoot), - merges only the
[PublicConfiguration]-marked subset ofWhiteLabelingOptionsunder aWhiteLabelingOptionskey, usingPublicConfigurationAttribute.GetPublicConfigurationPropertyPaths(...)plus a:\d+array-index regex to match paths, - 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.
Navigation: menus¶
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.GroupNameand 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.
Navigation: toolbar¶
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 clientLeptonXThemeBlazorOptions.Layout = LeptonXBlazorLayouts.SideMenu. Default style isLeptonXStyleNames.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 aMudTheme. Semantic colors are shared across LeptonX styles (e.g. primary#355dff,DefaultBorderRadius = "0.5rem"). - Light/dark bridge:
App.razorinjects a JS functionsubscribeToLeptonXThemethat forwards the nativelpx:appearance-settingevent to Blazor.Routes.razorreads the initial style fromILeptonXStyleProviderand flipsMudThemeProvider'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:
CargonerdsBrandingProviderreplaces ABP'sDefaultBrandingProvider(AppName/LogoUrl).LeptonXFooterreplaces ABP'sFooter(version, copyright/footer links).- The
ApplyWhiteLabelinglayout hook (LayoutHooks.Body.FirstforStandardLayouts.Application) sets the--lpx-logo-iconCSS variable fromWhiteLabelingOptions.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 disablesWasmFingerprintAssets; Release setsCompressionEnabled=false,PublishTrimmed=false,TrimMode=partial. - Trimming is deliberately disabled.
MudBlazor+MudBlazor.Extensionsare also pinned asTrimmerRootAssembly(plus anILLink.Descriptors.xmlTrimmerRootDescriptor) to work around a productionCtorNotLocated, MudBlazor.MudExpansionPanelserror: Blazorise'sIComponentActivatorusesActivatorUtilities/reflection while MudBlazor.Extensions renders Mud components dynamically viaDynamicComponent + 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
*.Bundlingmodule dependencies, andAbpMvcLibsOptions.CheckLibs = falseskips 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.
Cookie consent¶
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):
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:
- Modularity —
AbpModule+[DependsOn(...)]compose the host from*.Bundlingmodules and the client from feature WASM modules;ConfigureServices/OnApplicationInitializationlifecycle hooks. Configure<TOptions>(...)forAbpBundlingOptions,AbpMvcLibsOptions,RouteOptions,AbpNavigationOptions,AbpToolbarOptions,AbpRouterOptions,AbpLayoutHookOptions,AbpAutoMapperOptions,AbpLocalizationOptions,SettingManagementComponentOptions,LeptonXThemeOptions,LeptonXThemeBlazorOptions.IMenuContributor(3 implementations) andIToolbarContributorfor navigation.- Service replacement / DI override
(dependency injection):
context.Services.Replace(...)forExtensionProperties<,>;[Dependency(ReplaceServices = true)]onCargonerdsBrandingProvider;[ExposeServices(typeof(Footer))] + [Dependency(ReplaceServices = true)]onLeptonXFooter. - Layout hooks (
AbpLayoutHookOptions.Add(LayoutHooks.Body.First, ...)),ISettingComponentContributor(settings), andIBrandingProvideroverride. - Localization resources
(
CargonerdsResourcewithAddBaseTypes(AbpUiResource)) — see Localization. - ABP component base chain
CargonerdsComponentBase : HubComponentBase : AbpComponentBase. - Auto-generated HTTP API client proxies
via
CargonerdsHttpApiClientModule. - Aspire ServiceDefaults
(host
AddServiceDefaults(); AppHostAddProjectWithDefaults).
For a cross-cutting catalogue of these patterns, see ABP Patterns and the Architecture Overview.
Gotchas¶
- Two layout settings must stay in sync (
LeptonXTheme.Layouthost parameter vs.LeptonXThemeBlazorOptions.Layoutclient) — set in two files; a comment warns to update both. - Trimming is off on purpose; re-enabling it reintroduces the MudBlazor
CtorNotLocatedfailure. - Prerendering is disabled (
prerender: false) in bothApp.razorandRoutes.razor. - Runtime config vs. static config — the static
wwwroot/appsettings.jsonis a local-dev fallback; the effective endpoints are merged server-side byAppSettingsHandler. - White-label secret-leakage guard — only
[PublicConfiguration]properties are shipped to the browser; non-public options (e.g.EntraLogins) are filtered out. - Blazorise
ProductTokenis hardcoded and embedded in the shipped WASM. - Pricing surfaces routes but no menu —
PricingMenuContributoris 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/
unpkginApp.razor(maplibre-gl 5.10.0, Google Fonts) — external runtime dependencies of the admin shell. OrganizationToggle/GlobalSearchBarcome from Hub.UI, not Cargonerds — the toolbar depends on Hub services; removing the Hub dependency breaks it.
Related pages¶
- Theming — LeptonX side-menu, light/dark bridge,
MudLeptonTheme, white-labeling. - Localization —
CargonerdsResource/HubResource/AccountResource,BlazorWebAssemblyLoadAllGlobalizationData. - Realtime (Next.js) Frontend —
ConnectivityMonitor,NotificationHubService, global search/streaming. - Hub module & Pricing module — how
Hub.UIandPricing.Blazor.WebAssemblyplug into the shell. - Aspire Integration —
adminservice registration,{admin}/{auth}/{api}token substitution, health checks. - API Authentication / API Clients — OIDC code
flow, scopes,
ClientId=admin, dynamic client proxies. - appsettings reference / configuration reference
— client config keys,
ClientConfiguration,WhiteLabelingOptions/[PublicConfiguration]. - ABP Patterns — contributor / options / service-replacement / layout-hook patterns.