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, insrc/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.AppHostis not an ABP module. It is the .NET Aspire orchestrator; it never callsAddApplicationAsyncand has noAbpModule. Don't treat it as part of the dependency graph. See Aspire Integration.AbpStudioAnalyzeHelper.IsInAnalyzeModeearly-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) andAddDbContextPool<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(notHubUIBlazorModule) in namespaceHub.UI, fileUIBlazorModule.cs— easy to miss when searching. PricingDbContext'sPricingconnection string falls back toDefaultwhen unconfigured. It is a logically separate (and empty) context, never merged viaReplaceDbContext.
Related pages¶
- Module Structure — the per-layer project breakdown shared by all three families.
- Layered Architecture — layer dependency rules.
- Solution Structure — solution files and folder map.
- ABP Patterns — the framework conventions used throughout.
- Architecture Overview — the system from above.
- Entity Framework Core and Migrations.