Skip to content

Modules Overview

Cargonerds is a modular monolith built on the ABP Framework 10.x and .NET 10. A single deployable system is assembled at boot time from many small, self-describing units called ABP modules. Three of those units are Cargonerds' own application code:

Module Location Responsibility
Cargonerds (shell) src/ Host applications (API host, auth server, Blazor, public web, DB migrator), authentication, Identity/SaaS, dashboards, API keys, white-label settings, and the composition of the business modules into one deployable system.
Hub modules/hub The logistics bounded context: shipments, consols, containers, organizations/contacts, documents, orders, tracking, invoicing — and the pricing domain entities — plus the SPARK integration machinery.
Pricing modules/pricing Quotation/offer workflows, margin and pricing calculation. An application/UI/permissions layer that sits on top of Hub (its entities live in Hub).

What 'modular monolith' means here

Every business capability is packaged as an independent ABP module with explicit dependencies, the same way you would package a microservice — but all modules are loaded into one process and (mostly) share databases. You get strong internal boundaries and the option to extract a module later, without paying the operational cost of distributed services today.

The modular-monolith concept in ABP

A module is a plain C# class that derives from Volo.Abp.Modularity.AbpModule and carries a [DependsOn(...)] attribute naming the other modules it needs. At startup, ABP is handed a single root module, walks its transitive [DependsOn] closure, topologically sorts it, and runs each module's lifecycle hooks in dependency order:

PreConfigureServices → ConfigureServices → PostConfigureServices
        → OnPreApplicationInitialization → OnApplicationInitialization

This is the ABP module system. Modules never reference each other through a service locator; they collaborate through the options pattern (Configure<TOptions> / PreConfigure<TOptions>) and dependency injection.

abpSrc/ is framework source, not application code

The repository vendors the full ABP/LeptonX framework under abpSrc/, which contains hundreds of Volo.Abp.* / Volo.* *Module.cs files. Those are dependencies, not Cargonerds code. Only the Cargonerds.*, Hub.*, and Pricing.* modules under src/ and modules/ are application modules (about 41 *Module.cs files, including test modules). The Cargonerds.AppHost is a .NET Aspire orchestrator and is not an ABP module.

The module set

Each of the three families follows the standard ABP layered module template, so the same project layout repeats three times:

Domain.Shared → Domain → EntityFrameworkCore
Domain.Shared → Application.Contracts → Application
Application.Contracts → HttpApi / HttpApi.Client
…all of the above → Blazor / Blazor.Client / AuthServer / HttpApi.Host / DbMigrator (the hosts)

See Module Structure for the per-layer responsibilities, and Layered Architecture for the dependency rules between layers.

Root (host) modules

Six runnable hosts each call AddApplicationAsync<TRootModule>() in their Program.cs; ABP then loads the entire dependency closure of that root. Server hosts use Autofac (.UseAutofac()); the WASM client uses AbpAutofacWebAssemblyModule.

Host project Root module
Cargonerds.HttpApi.Host CargonerdsHttpApiHostModule
Cargonerds.AuthServer CargonerdsAuthServerModule
Cargonerds.Blazor CargonerdsBlazorModule
Cargonerds.Blazor.Client CargonerdsBlazorClientModule
Cargonerds.Web.Public CargonerdsWebPublicModule
Cargonerds.DbMigrator CargonerdsDbMigratorModule (via DbMigratorHostedService)
// src/Cargonerds.HttpApi.Host/Program.cs
await builder.AddApplicationAsync<CargonerdsHttpApiHostModule>();

Dependency graph

Pricing.* depends on Hub.* (Pricing builds on Hub's domain), and the Cargonerds.* shell depends on both business families plus the full ABP commercial suite (Identity Pro, OpenIddict Pro, SaaS, CMS Kit Pro, Chat, Feature Management, GDPR, …). Each shell layer pulls in the matching Pricing* and Hub* layer. ABP de-duplicates diamond dependencies automatically (Hub is reached via both Pricing and the shell, but loads once).

flowchart TB
    subgraph Hosts
        Host[CargonerdsHttpApiHostModule<br/>root]
        Auth[CargonerdsAuthServerModule]
        Blazor[CargonerdsBlazor / Blazor.Client]
        Pub[CargonerdsWebPublicModule]
        Mig[CargonerdsDbMigratorModule]
    end

    Host --> App[CargonerdsApplicationModule]
    Host --> Efc[CargonerdsEntityFrameworkCoreModule]
    Host --> Api[CargonerdsHttpApiModule]

    App --> PApp[PricingApplicationModule]
    Efc --> PEfc[PricingEntityFrameworkCoreModule]
    Api --> PApi[PricingHttpApiModule]

    PApp --> HApp[HubApplicationModule]
    PEfc --> HEfc[HubEntityFrameworkCoreModule]
    PApi --> HApi[HubHttpApiModule]

    App --> ABP[(ABP commercial suite:<br/>Identity Pro, SaaS, OpenIddict Pro,<br/>CMS Kit Pro, Chat, Features, GDPR…)]
    Efc --> ABP

    Blazor --> App
    Auth --> App
    Pub --> App
    Mig --> Efc

The chaining is the same in every layer (Cargonerds* → Pricing* → Hub* → ABP). Confirmed examples:

// src/Cargonerds.EntityFrameworkCore/.../CargonerdsEntityFrameworkCoreModule.cs
[DependsOn(
    typeof(PricingEntityFrameworkCoreModule),
    typeof(HubEntityFrameworkCoreModule),
    typeof(CargonerdsDomainModule),
    typeof(AbpEntityFrameworkCoreSqlServerModule),
    typeof(AbpIdentityProEntityFrameworkCoreModule),
    typeof(AbpOpenIddictProEntityFrameworkCoreModule),
    typeof(SaasEntityFrameworkCoreModule),
    typeof(CmsKitProEntityFrameworkCoreModule),
    /* …~10 more ABP EF Core modules… */ )]
public class CargonerdsEntityFrameworkCoreModule : AbpModule { /* … */ }
// modules/pricing/src/Pricing.Domain/PricingDomainModule.cs
[DependsOn(
    typeof(HubDomainModule),
    typeof(AbpDddDomainModule),
    typeof(VoloAbpCommercialSuiteTemplatesModule),
    typeof(PricingDomainSharedModule)
)]
public class PricingDomainModule : AbpModule { }

How modules compose into the host

The shell does not stop at [DependsOn]. The API host module is where the three families are turned into a running web application. Three composition mechanisms are worth knowing.

1. Auto-API controllers

REST endpoints are generated from application-service classes via ABP's auto API controllers. The host opts each module's application assembly in explicitly:

// src/Cargonerds.HttpApi.Host/CargonerdsHttpApiHostModule.cs
Configure<AbpAspNetCoreMvcOptions>(options =>
{
    options.ConventionalControllers.Create(typeof(CargonerdsApplicationModule).Assembly);
    options.ConventionalControllers.Create(typeof(HubApplicationModule).Assembly);
    options.ConventionalControllers.Create(typeof(PricingApplicationModule).Assembly);
});

This is how QuotationAppService and OfferAppService (Pricing) and the Hub services become HTTP endpoints with no hand-written controllers. The matching client side uses dynamic C# client proxies. See REST API and API Clients.

Satellite modules need no host [DependsOn] on their Application/EF layers

The host module does not list PricingApplicationModule or PricingEntityFrameworkCoreModule in its [DependsOn]. Pricing is wired in through the client/domain/EF dependency chains (CargonerdsApplicationModule → PricingApplicationModule, CargonerdsEntityFrameworkCoreModule → PricingEntityFrameworkCoreModule, etc.), and its controllers are surfaced solely by the ConventionalControllers.Create(...) call above. This is the pattern to copy when adding a new module.

2. DbContexts and connection strings

Modules persist through ABP DbContexts registered with AddAbpDbContext<T> + AddDefaultRepositories(includeAllEntities: true) (which generates repositories for non-aggregate entities too). The codebase uses three logical databases, named by ConnectionStringNames in src/Cargonerds.Domain.Shared/Configuration/ConnectionStringNames.cs:

Connection string DbContext Holds
Default (SparkDb) CargonerdsDbContext Identity Pro, SaaS, Permission Management, CMS Kit, settings, audit logs, API keys, dashboards.
Hub (HubDb) HubDbContext All freight entities and the pricing entities. Pooled + second-level cached.
Pricing PricingDbContext Nothing — an empty ABP scaffold (no DbSets); falls back to Default if unconfigured.

CargonerdsDbContext folds several ABP module schemas into the one Default database using [ReplaceDbContext], so cross-module JOINs work against a single physical store:

// src/Cargonerds.EntityFrameworkCore/.../CargonerdsDbContext.cs
[ReplaceDbContext(typeof(IIdentityProDbContext))]
[ReplaceDbContext(typeof(ISaasDbContext))]
[ReplaceDbContext(typeof(IPermissionManagementDbContext))]
[ConnectionStringName(ConnectionStringNames.SparkDb)]   // "Default"
public class CargonerdsDbContext : ... { /* implements those module interfaces */ }

Hub and Pricing are not ReplaceDbContext-merged into the shared context — they remain logically separate DbContexts (IHubDbContext, IPricingDbContext). See Entity Framework Core for the full data-access story and Migrations for how schemas are created.

Pricing entities live in Hub, not Pricing

Despite its name, Pricing.Domain defines no entities and PricingDbContext has no DbSets; ConfigurePricing() is a no-op placeholder. The Quotation, Offer, ChargeLine, Margin, and MarginRateCalculator entities are declared in Hub.Domain and persisted by HubDbContext. The Pricing module is an application/permissions/UI layer over Hub's data. See Pricing Module.

3. The host pipeline and cross-cutting wiring

CargonerdsHttpApiHostModule.ConfigureServices delegates to private helpers that wire the infrastructure every module relies on: authentication (JWT bearer, audience Cargonerds), the custom API-key scheme, Swagger/OIDC, Redis caching (key prefix Cargonerds:), RabbitMQ (messaging), distributed locking (Medallion + Redis), data protection, CORS, OData, and health checks. Its OnApplicationInitialization builds the middleware pipeline (UseAbpRequestLocalization, UseMultiTenancy, UseUnitOfWork, UseDynamicClaims, the custom FilterContextMiddleware, Swagger UI). The auth server is a separate root module (CargonerdsAuthServerModule) running OpenIddict.

Module summaries

Cargonerds shell

The src/ projects (AuthServer, HttpApi.Host, Blazor, Blazor.Client, Web.Public, DbMigrator) plus the framework wiring:

  • OpenIddict authentication (Cargonerds.AuthServer).
  • ABP commercial modules: Identity, SaaS, Account, Chat, CMS Kit, Audit Logging, File Management, and the LeptonX theme.
  • Cross-cutting features: API keys, dashboards, white-label settings.
  • The cross-module global search orchestrator (GlobalSearchAppService) lives here, in src/Cargonerds.Application, even though the search providers live in Hub.

See Cargonerds Module.

Hub module

The logistics bounded context and the largest module. Key entities include Shipment, Consol, ConsolLeg, Container, Organization, Contact, Address, Document, and Order, plus the polymorphic External*Identifier and Transmission families that sync with the external SPARK integration platform. HubDbContext (connection Hub) is pooled (poolSize: 256) and wrapped in an EF Core second-level cache (1-minute TTL).

See Hub Module and Caching.

Pricing module

Quotation/offer management and margin calculation. Application services include QuotationAppService, OfferAppService, QuotationLegacyAppService (the engine that calls the external pricing API), and a MarginCalculationService, plus a QuoteGenerationJob background job. Permissions are defined in PricingPermissions (Quotation and Offer groups).

Pricing's Blazor UI is now redirect stubs

Pricing ships Pricing.Blazor / Pricing.Blazor.WebAssembly projects referenced by Cargonerds.Blazor.Client, but the quotation pages are now redirect stubs that forward /pricing/... routes to the Hub.UI equivalents under /hub/.... The real screens live in Hub.UI.

See Pricing Module.

Gotchas

Things that surprise newcomers to the module graph

  • Cargonerds.AppHost is not an ABP module. It is the .NET Aspire orchestrator; it never calls AddApplicationAsync and has no AbpModule. Don't treat it as part of the dependency graph. See Aspire Integration.
  • AbpStudioAnalyzeHelper.IsInAnalyzeMode early-returns. Several shell modules short-circuit Redis/SQL-touching configuration when ABP Studio statically analyzes the solution; forgetting this guard makes analysis try to connect to Redis/SQL and fail.
  • Hub registers its DbContext twice — both AddAbpDbContext<HubDbContext> (for ABP repos/UoW) and AddDbContextPool<HubDbContext>(poolSize: 256) (for the pooled, second-level-cached read path). Interceptor wiring exists in two places and must be kept in sync.
  • The Hub.UI Blazor module class is UIBlazorModule (not HubUIBlazorModule) in namespace Hub.UI, file UIBlazorModule.cs — easy to miss when searching.
  • PricingDbContext's Pricing connection string falls back to Default when unconfigured. It is a logically separate (and empty) context, never merged via ReplaceDbContext.