Skip to content

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 CoreCargonerdsDomainModule / CargonerdsEntityFrameworkCoreModule depend on Pricing* + Hub*, so Hub and Pricing entities are part of the application's domain model. They are not merged into CargonerdsDbContext via [ReplaceDbContext] — Hub lives on its own Hub connection (with a pooled, second-level-cached read path) and Pricing's entities are physically owned by HubDbContext. See Hub module, Pricing module and Caching.
  • Application & APICargonerdsApplicationModule depends on PricingApplicationModule + HubApplicationModule; the API host registers conventional controllers for all three assemblies, so Hub and Pricing app services are exposed through the one api host.
  • UICargonerds.Blazor.Client references Hub.UI and Pricing.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:

  1. 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.
  2. Starts the servicesdb-migrator (migrate + seed), then auth, api and admin, plus the auto-discovered frontend/ apps. The documentation site is available on demand.
  3. Wires dependencies — injects the Default connection string, Redis and RabbitMQ references, white-label settings and frontend environment variables, and registers health checks / service discovery.

To run locally:

dotnet run --project src/Cargonerds.AppHost

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:

  1. Add the keys to the default en.json.
  2. 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
  1. 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 in CargonerdsDbContext — Hub entities are intentionally not mapped into the Default context. They live on the Hub connection.
  • Provider + interceptors must be one Configure action. Splitting UseSqlServer(...) from a later Configure(...) overwrites the SQL Server provider ("No database provider configured").
  • API-key auth fights Identity Pro dynamic claims. PostConfigureServices swaps the session contributor by index in DynamicContributors; this is fragile if ABP changes the default registration order.
  • AbpStudioAnalyzeHelper.IsInAnalyzeMode short-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 and ISaasDbContext is replaced — the tenant migration loop is dead code.
  • Cargonerds.Web.Public exists 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.