Cargonerds module¶
The Cargonerds core is the host application (the application shell) of this solution. It is the
topmost of the three ABP modules
families — Cargonerds.* (shell), Hub.*, and Pricing.* — and its job is to compose: it pulls
in the full ABP Commercial module suite (Identity Pro, OpenIddict Pro, SaaS, CmsKit Pro, Setting
Management, Feature Management, …) and the two business modules (Hub, Pricing) into a single
deployable system, then runs them under separate hosts orchestrated by .NET Aspire
(Cargonerds.AppHost).
It owns relatively little domain logic of its own. The real freight domain lives in the
Hub module; quotations and margins live in the Pricing module. The Cargonerds
shell instead owns identity/users, the Default database, framework-feature wiring, and a thin glue
layer (API keys, comments, organization-unit extensions, client settings). See
Module structure for the layered convention these projects follow and
Architecture Overview for the big picture.
How this module composes the others
Pricing.* depends on Hub.* (Pricing builds on Hub's domain), and every Cargonerds.* layer
depends on both the matching Pricing.* and Hub.* layer plus the matching ABP modules. ABP
de-duplicates the diamond (Hub is reached via both Pricing and Cargonerds) automatically when it
resolves the [DependsOn]
graph from each host's root module.
graph TD
Host["CargonerdsHttpApiHostModule<br/>(root – API host)"]
App["CargonerdsApplicationModule"]
Ef["CargonerdsEntityFrameworkCoreModule"]
Dom["CargonerdsDomainModule"]
DomShared["CargonerdsDomainSharedModule"]
Pricing["Pricing.* modules"]
Hub["Hub.* modules"]
Abp["ABP Commercial suite<br/>(Identity Pro, OpenIddict Pro,<br/>SaaS, CmsKit Pro, Settings, Features…)"]
Host --> App
Host --> Ef
App --> Dom
Ef --> Dom
Dom --> DomShared
App --> Pricing
App --> Hub
Ef --> Pricing
Ef --> Hub
Pricing --> Hub
Dom --> Abp
App --> Abp
Ef --> Abp
Projects and their roles¶
The table lists the projects under src/. Names follow the standard ABP
layered-module convention.
| Project | Purpose |
|---|---|
| Cargonerds.Domain.Shared | Constants, enums, settings keys and localization resources shared across layers. No dependencies; referenced by everything. Holds CargonerdsConsts (DbTablePrefix = "App", Aspire resource names, seeded admin credentials, solution directories), MultiTenancyConsts, and ConnectionStringNames. |
| Cargonerds.Domain | The host-level domain: API-key management (ApiKey, ApiKeyManager), organization-unit extensions (OrganizationUnitOrgCode, OrganizationUnitOwnerUser), client settings, and data-seed contributors. References Hub.Domain and Pricing.Domain. |
| Cargonerds.Application.Contracts | Application-service interfaces, DTOs and permission definitions (CargonerdsPermissions). |
| Cargonerds.Application | Application-service implementations (e.g. the global-search orchestrator, API-key permission decoration) plus AutoMapper profiles. Decorates IPermissionAppService. |
| Cargonerds.EntityFrameworkCore | The CargonerdsDbContext (on the Default connection) and its mappings. References Hub.EntityFrameworkCore and Pricing.EntityFrameworkCore. Holds the only EF migrations project in the solution. |
| Cargonerds.DbMigrator | Console app (Aspire db-migrator) that applies migrations and seeds data. Run automatically by the AppHost. Also holds the seeded OpenIddict:Applications client list. |
| Cargonerds.HttpApi | API controllers (mostly auto-generated by ABP). |
| Cargonerds.HttpApi.Client | Strongly-typed C# client proxies for the HTTP APIs (RemoteServiceName = "Default"). |
| Cargonerds.AuthServer | OpenIddict-based OAuth 2.0 / OIDC server (Aspire auth). Issues tokens and hosts the LeptonX login/account UI. |
| Cargonerds.HttpApi.Host | The runnable API host (Aspire api). Wires Cargonerds.ServiceDefaults (health checks, OpenTelemetry, resilience). The root module for the API. |
| Cargonerds.Blazor / Cargonerds.Blazor.Client | The Blazor WebAssembly admin UI (Aspire admin). The client references Hub.UI and Pricing.Blazor.WebAssembly so module pages appear in the admin shell. |
| Cargonerds.UI.Shared | Shared UI building blocks used by the Blazor projects. |
| Cargonerds.Web.Public | An ASP.NET Core MVC/Razor Pages public site. Present in the repo but not registered in the AppHost, so it is not part of the orchestrated runtime. |
| Cargonerds.ServiceDefaults | AddServiceDefaults extension: service discovery, resilience, health checks and OpenTelemetry. Referenced by each service host. |
| Cargonerds.AppHost | The .NET Aspire orchestrator. Declares SQL Server, Redis and RabbitMQ, registers the service projects, discovers the frontend/ apps and wires configuration between them. Not an ABP module. |
| Theme | Shared theming assets for the UI. |
Frontend lives outside src/
The customer-facing UI is the Next.js app in frontend/realtime, consuming the shared
@cargonerds/ui-library (frontend/ui-library). It is discovered and run by the AppHost
rather than being a src/ project. See Architecture Overview and
Realtime frontend.
What the host module owns¶
Module composition ([DependsOn])¶
Each Cargonerds layer is an AbpModule
that depends on its Pricing/Hub counterpart plus the matching ABP modules. The domain module
(src/Cargonerds.Domain/CargonerdsDomainModule.cs) is representative — it brings in the whole
commercial stack:
[DependsOn(
typeof(PricingDomainModule),
typeof(HubDomainModule),
typeof(CargonerdsDomainSharedModule),
typeof(AbpAuditLoggingDomainModule),
typeof(AbpFeatureManagementDomainModule),
typeof(AbpPermissionManagementDomainIdentityModule),
typeof(AbpPermissionManagementDomainOpenIddictModule),
typeof(AbpSettingManagementDomainModule),
typeof(AbpIdentityProDomainModule),
typeof(AbpOpenIddictProDomainModule),
typeof(SaasDomainModule),
typeof(ChatDomainModule),
typeof(TextTemplateManagementDomainModule),
typeof(LanguageManagementDomainModule),
typeof(FileManagementDomainModule),
typeof(AbpGdprDomainModule),
typeof(CmsKitProDomainModule),
typeof(LeptonXThemeManagementDomainModule),
typeof(BlobStoringDatabaseDomainModule)
// …
)]
public class CargonerdsDomainModule : AbpModule
The same pattern repeats at every layer (CargonerdsApplicationModule,
CargonerdsEntityFrameworkCoreModule, CargonerdsHttpApiClientModule, …): each Cargonerds.* module
pulls in Pricing* + Hub* of that layer and the corresponding ABP application/EF/HTTP modules.
Identity, SaaS and Permission Management folded into one DbContext¶
The defining persistence decision of this module is that CargonerdsDbContext
(src/Cargonerds.EntityFrameworkCore/EntityFrameworkCore/CargonerdsDbContext.cs) replaces several
ABP module DbContexts using ABP's
[ReplaceDbContext] attribute
and implements their interfaces. This folds Identity Pro, SaaS, and Permission Management tables into
the single Default database so cross-module JOINs work through the repositories:
[ReplaceDbContext(typeof(IIdentityProDbContext))]
[ReplaceDbContext(typeof(ISaasDbContext))]
[ReplaceDbContext(typeof(IPermissionManagementDbContext))]
[ConnectionStringName(ConnectionStringNames.SparkDb)] // "Default"
public class CargonerdsDbContext
: AbpDbContext<CargonerdsDbContext>,
ISaasDbContext,
IIdentityProDbContext,
IPermissionManagementDbContext
OnModelCreating then calls the builder.ConfigureXxx() model extension for every framework module it
hosts — ConfigurePermissionManagement, ConfigureSettingManagement, ConfigureBackgroundJobs,
ConfigureAuditLogging, ConfigureFeatureManagement, ConfigureIdentityPro, ConfigureOpenIddictPro,
ConfigureLanguageManagement, ConfigureFileManagement, ConfigureSaas, ConfigureChat,
ConfigureTextTemplateManagement, ConfigureGdpr, ConfigureCmsKit / ConfigureCmsKitPro, and
ConfigureBlobStoring. App-owned tables use an explicit ToTable(...):
AppClientSettings, AppApiKeys, AppOuOrgCodes, AppOuOwnerUser, plus the notification
entity configurations.
ConfigureHub() is deliberately commented out
Line 108 of CargonerdsDbContext.cs is //builder.ConfigureHub();. Hub entities are not mapped
into the Default context — they belong to HubDbContext on the Hub connection. The Cargonerds
context calls builder.ConfigurePricing();, but Pricing entities are physically owned by
HubDbContext, so this is a thin model registration. See
Entity Framework for the full multi-DbContext story.
A SQL Server stored computed column is added to IdentityUser so the country can be filtered at the
query level:
builder.Entity<IdentityUser>(b =>
{
b.Property<string>(UserConsts.ComputedColumns.CountryIso)
.HasComputedColumnSql("LTRIM(RTRIM(JSON_VALUE(ExtraProperties, '$.country')))", stored: true);
b.HasIndex(UserConsts.ComputedColumns.CountryIso);
});
(migration 20260618140521_AddUserCountryIsoComputedColumn). To make the filter apply uniformly across
listing, counting and Excel/CSV export, the module replaces ABP's IIdentityUserRepository with
CargonerdsIdentityUserRepository.
The Default connection and repository registration¶
CargonerdsEntityFrameworkCoreModule registers the context and its repositories with
AddAbpDbContext + AddDefaultRepositories,
and replaces the identity user repository:
context.Services.AddAbpDbContext<CargonerdsDbContext>(options =>
{
options.AddDefaultRepositories(includeAllEntities: true);
options.AddRepository<ApiKey, EfCoreApiKeyRepository>();
});
context.Services.Replace(
ServiceDescriptor.Transient<IIdentityUserRepository, CargonerdsIdentityUserRepository>());
includeAllEntities: true generates default repositories for non-aggregate entities too. The SQL Server
provider and the ArithAbortConnectionInterceptor are then configured in a single
options.Configure(...) action — splitting them into two Configure calls would overwrite the provider
("No database provider configured"), as the inline comment warns:
Configure<AbpDbContextOptions>(options =>
{
options.Configure(ctx =>
{
ctx.UseSqlServer(b => b.UseParameterizedCollectionMode(ParameterTranslationMode.Parameter));
ctx.DbContextOptions.AddInterceptors(new ArithAbortConnectionInterceptor());
});
});
The three connection-string names are constants in
src/Cargonerds.Domain.Shared/Configuration/ConnectionStringNames.cs:
| Constant | Value | Used by |
|---|---|---|
SparkDb |
"Default" |
CargonerdsDbContext (host / Identity / SaaS / framework) |
HubDb |
"Hub" |
HubDbContext (freight domain) |
HubServiceBus |
"hubServiceBus" |
Hub messaging |
BlobStorage |
nameof(BlobStorage) |
Azure Blob storage client |
Default is the only connection string present in checked-in appsettings.json
(src/Cargonerds.DbMigrator/appsettings.json and Cargonerds.HttpApi.Host); Hub and Pricing are
injected at runtime by Aspire. See Configuration reference.
Migrations live here (and only here)¶
Cargonerds.EntityFrameworkCore is the only EF migrations project in the solution
(src/Cargonerds.EntityFrameworkCore/Migrations). The design-time
factory
(CargonerdsDbContextFactory) reads the Default connection string from the DbMigrator's
appsettings.json. Neither Hub nor Pricing ships a Migrations folder — only the Default DB is
EF-migrated; the Hub schema is managed externally. This is covered in depth on the
Migrations page.
CmsKit comments and SaaS/multi-tenancy toggle¶
CargonerdsDomainModule.ConfigureServices does three host-level configuration jobs:
Configure<AbpMultiTenancyOptions>(options =>
{
options.IsEnabled = MultiTenancyConsts.IsEnabled; // false
});
Configure<CmsKitCommentOptions>(options =>
{
var derivedTypes = typeof(ShipmentBase).Assembly.GetTypes()
.Where(t => t.IsClass && !t.IsAbstract && t.IsSubclassOf(typeof(ShipmentBase)))
.ToList();
options.EntityTypes.AddRange(derivedTypes.Select(t => new CommentEntityTypeDefinition(t.Name)));
options.EntityTypes.Add(new CommentEntityTypeDefinition("Quote"));
options.EntityTypes.Add(new CommentEntityTypeDefinition(PricingConsts.QuotationRequest));
});
Configure<PermissionManagementOptions>(options =>
{
options.ManagementProviders.Add<ApiKeyPermissionManagementProvider>();
options.ProviderPolicies[ApiKeyAuthorizationConsts.PermissionProviderName] =
ApiKeyAuthorizationConsts.PolicyName;
});
Note that multi-tenancy is disabled (MultiTenancyConsts.IsEnabled = false). The SaaS module is
still installed and CargonerdsDbContext still replaces ISaasDbContext, but the tenant loop in the
migration service is effectively dead code. CmsKit comments are enabled for every concrete
ShipmentBase subtype (discovered by reflection) plus Quote and the quotation-request type.
API-key authentication wiring¶
The host adds a second, hand-rolled credential type — an api-key authentication scheme — on top of the
JWT bearer pipeline. Most of the wiring is in CargonerdsDomainModule.PostConfigureServices (the only
custom module in the solution that overrides PostConfigureServices):
public override void PostConfigureServices(ServiceConfigurationContext context)
{
Configure<AbpPermissionOptions>(options =>
{
options.ValueProviders.AddFirst(typeof(ApiKeyPermissionValueProvider));
});
// Identity Pro drops any principal without a SessionId claim, which kills our
// stateless API-key auth. Swap the contributor for a subclass that short-circuits
// for "api-key" auth and falls through for everything else.
Configure<AbpClaimsPrincipalFactoryOptions>(options =>
{
var index = options.DynamicContributors.IndexOf(typeof(IdentitySessionDynamicClaimsPrincipalContributor));
if (index >= 0)
{
options.DynamicContributors[index] = typeof(ApiKeyAwareIdentitySessionDynamicClaimsContributor);
}
});
}
The scheme itself (header resolvers X-Api-Key / Api-Key, query resolvers in Development only) is
registered in CargonerdsHttpApiHostModule.ConfigureApiKeyAuthentication. A key can never exceed its
owner's permissions. The full subsystem — resolution, owner-capped permissions, the session-claim
adapter — is documented on the Authentication page.
Permission decoration in the application layer¶
CargonerdsApplicationModule registers AutoMapper maps and decorates the ABP permission app service
so API-key callers get a permission view filtered to what their key can actually exercise:
Configure<AbpAutoMapperOptions>(options =>
{
options.AddMaps<CargonerdsApplicationModule>();
});
context.Services.Decorate<IPermissionAppService, ApiKeyPermissionAppService>();
Localization, languages and white-labeling¶
CargonerdsDomainSharedModule registers the CargonerdsResource
localization resource, the
20 UI languages, the embedded
virtual file system set, and
binds WhiteLabelingOptions from configuration. It also registers a custom feature value-validator in
PreConfigureServices — this must be done in PreConfigure because
AbpFeatureManagementDomainSharedModule reads ValueValidatorFactoryOptions at config time to build its
JSON converter (needed both server-side and in the Blazor WASM client):
public override void PreConfigureServices(ServiceConfigurationContext context)
{
CargonerdsGlobalFeatureConfigurator.Configure();
CargonerdsModuleExtensionConfigurator.Configure();
PreConfigure<ValueValidatorFactoryOptions>(options =>
{
options.ValueValidatorFactory!.Add(
new ValueValidatorFactory<OptionalEmailAddressStringValueValidator>(
OptionalEmailAddressStringValueValidator.ValidatorName));
});
}
The Localization/Cargonerds folder ships one JSON file per culture (en.json, de-DE.json, fr.json,
es.json, …). See Localization for the translation workflow.
Conventional controllers and client proxies¶
The host explicitly generates
auto-API controllers for all
three module families in CargonerdsHttpApiHostModule:
options.ConventionalControllers.Create(typeof(CargonerdsApplicationModule).Assembly);
options.ConventionalControllers.Create(typeof(HubApplicationModule).Assembly);
options.ConventionalControllers.Create(typeof(PricingApplicationModule).Assembly);
CargonerdsHttpApiClientModule generates the matching
dynamic C# client proxies
under the "Default" remote service:
public const string RemoteServiceName = "Default";
context.Services.AddHttpClientProxies(
typeof(CargonerdsApplicationContractsModule).Assembly, RemoteServiceName);
The Blazor admin app uses these proxies; the Next.js realtime app calls the REST/OData endpoints
directly. JWT bearer validation in the API host uses audience "Cargonerds"
(options.Audience = "Cargonerds" in ConfigureAuthentication).
How it composes Hub and Pricing¶
The shell is the only place where the whole graph is assembled. Concretely:
- Domain & EF Core —
CargonerdsDomainModule/CargonerdsEntityFrameworkCoreModuledepend onPricing*+Hub*, so Hub and Pricing entities are part of the application's domain model. They are not merged intoCargonerdsDbContextvia[ReplaceDbContext]— Hub lives on its ownHubconnection (with a pooled, second-level-cached read path) and Pricing's entities are physically owned byHubDbContext. See Hub module, Pricing module and Caching. - Application & API —
CargonerdsApplicationModuledepends onPricingApplicationModule+HubApplicationModule; the API host registers conventional controllers for all three assemblies, so Hub and Pricing app services are exposed through the oneapihost. - UI —
Cargonerds.Blazor.ClientreferencesHub.UIandPricing.Blazor.WebAssembly, so module pages appear inside the admin shell. See Blazor admin UI.
This is the standard ABP modular-monolith composition; for the framework-level patterns (modularity,
AddApplicationAsync, the options pattern, ReplaceDbContext) see
ABP patterns.
Running the Cargonerds application with .NET Aspire¶
Cargonerds.AppHost/Program.cs declares the distributed application. The AppHost is not an ABP
module — it is the .NET Aspire orchestrator (see
Aspire integration and the
ABP Aspire docs).
Running it:
- Creates infrastructure — a SQL Server container (
spark-db), Redis (spark-redis, with RedisInsight + Redis Commander) and RabbitMQ (spark-rabbitmq). On publish these map to Azure SQL, Azure Cache for Redis and a RabbitMQ container. - Starts the services —
db-migrator(migrate + seed), thenauth,apiandadmin, plus the auto-discoveredfrontend/apps. The documentation site is available on demand. - Wires dependencies — injects the
Defaultconnection string, Redis and RabbitMQ references, white-label settings and frontend environment variables, and registers health checks / service discovery.
To run locally:
The Aspire dashboard opens automatically and shows every resource with its live URL and health
status. In local run mode the fixed HTTPS ports (from RunModeServicePorts.cs) are:
| Service | Aspire name | Local URL |
|---|---|---|
| AuthServer | auth |
https://localhost:44345 |
| HttpApi.Host | api |
https://localhost:44354 |
| Blazor admin | admin |
https://localhost:44381 |
| Next.js realtime | realtime |
http://localhost:4200 |
OpenIddict clients are seeded by the DbMigrator
The OpenIddict:Applications array (clients Cargonerds_App, admin, api, web) lives in
src/Cargonerds.DbMigrator/appsettings.json, not in the AuthServer. The DbMigrator seeds them
on first run. See API clients.
Localization and translations¶
The Cargonerds.Domain.Shared project contains a Localization/Cargonerds folder with a JSON file per
culture. The translation.options.json file at the project root (src/Cargonerds.Domain.Shared/)
configures Resourcetranslator.Cli, which can auto-generate missing translations. To translate new keys:
- Add the keys to the default
en.json. - Run the translator from the solution root:
resourcetranslator translate \
--options src/Cargonerds.Domain.Shared/translation.options.json \
--input src/Cargonerds.Domain.Shared/Localization/Cargonerds/en.json \
--output src/Cargonerds.Domain.Shared/Localization/Cargonerds
- Commit the updated culture files. ABP loads them automatically at runtime.
The Hub module follows the same pattern under Hub.Domain.Shared/Localization/Hub. See
Localization.
Generating DTOs with Nextended.CodeGen¶
The Hub and Pricing modules use the Nextended.CodeGen source generator to produce DTOs and mapping
extensions from domain entities at build time, configured via CodeGen.config.json (DtoGeneration
section). Output lands in the Generated/ folders of the contracts and application projects and must not
be edited by hand. See Code generation.
Gotchas¶
Things that bite
ConfigureHub()is commented out inCargonerdsDbContext— Hub entities are intentionally not mapped into theDefaultcontext. They live on theHubconnection.- Provider + interceptors must be one
Configureaction. SplittingUseSqlServer(...)from a laterConfigure(...)overwrites the SQL Server provider ("No database provider configured"). - API-key auth fights Identity Pro dynamic claims.
PostConfigureServicesswaps the session contributor by index inDynamicContributors; this is fragile if ABP changes the default registration order. AbpStudioAnalyzeHelper.IsInAnalyzeModeshort-circuits config.CargonerdsEntityFrameworkCoreModule(and the host/auth/web modules) early-return from Redis/SQL-touching config when ABP Studio statically analyzes the solution. Omitting this guard makes analysis connect to Redis/SQL and fail.- Multi-tenancy is disabled (
MultiTenancyConsts.IsEnabled = false) even though SaaS is installed andISaasDbContextis replaced — the tenant migration loop is dead code. Cargonerds.Web.Publicexists but is not orchestrated. It is a real project but the AppHost does not register it, so it is not part of the running system. See Public web.
Summary¶
The Cargonerds core is the composition root of the solution: a layered, modular ABP application that
hosts authentication and the API, folds Identity Pro / SaaS / Permission Management into the single
Default database via [ReplaceDbContext], and depends on the Hub and Pricing modules to assemble the
full domain, API surface and admin UI. It is run end-to-end with a single dotnet run against the
AppHost. The next page describes the Hub module, where the freight domain actually lives.