Introduction¶
Welcome to Cargonerds - a logistics and freight management platform built as an ABP Framework layered, tiered modular monolith and orchestrated with .NET Aspire.
This page is the orientation for a new developer: what Cargonerds is, the big-picture architecture, the full tech stack, and how all the pieces fit together. Each section links onward to the deeper pages.
What is Cargonerds?¶
Cargonerds is the "SPARK" application: a multi-customer logistics platform that ingests
shipment, consignment and organisation data from external forwarding systems (the Hub
domain), and adds quoting and pricing workflows (the Pricing domain) on top of it. It is
white-labeled, so a single deployment can be branded per customer (the host reads
WhiteLabelingOptions from an appsettings.rohlig.json settings file).
The platform exposes its data through REST and ABP OData APIs that are consumed by two front ends: a Blazor WebAssembly admin UI and a customer-facing Next.js application.
Where the name SPARK shows up
"Spark" is the internal codename and surfaces throughout configuration, not in the C#
namespaces (which are Cargonerds.*). You will see it in the environment selector
SPARK_ENVIRONMENT, the per-environment files appsettings.spark.<env>.json, and Aspire
resource names such as the spark-db SQL container. See
Running Locally and the
Configuration reference.
The technology stack¶
The exact versions are centralised in
common.props at the repository root (AbpVersion,
AspireVersion, LeptonXVersion) and in Directory.Packages.props (every other package). The
table below is taken from those files.
| Layer | Technology | Version / notes |
|---|---|---|
| Runtime | .NET | net10.0 for all first-party projects (<TargetFramework> in every .csproj) |
| App framework | ABP Framework (commercial) | 10.1.1 (<AbpVersion> in common.props) |
| UI theme | LeptonX (commercial) | 5.1.1 (<LeptonXVersion>) |
| Orchestration | .NET Aspire | packages 13.2.2 (<AspireVersion>); AppHost SDK pinned 13.1.0 |
| ORM | Entity Framework Core | SQL Server provider; second-level cache in Hub |
| Database | SQL Server | three logical connection strings - Default, Hub, Pricing |
| Auth server | OpenIddict (via ABP) | OAuth 2.0 / OpenID Connect |
| Cache & locking | Redis | distributed cache + Medallion distributed locks |
| Messaging | RabbitMQ | ABP distributed event bus |
| Object mapping | AutoMapper (shell) + Riok.Mapperly (modules) | Mapperly is source-generated |
| Admin UI | Blazor WebAssembly | LeptonX theme |
| Public site | ASP.NET Core MVC | CmsKit Pro public pages |
| Customer UI | Next.js 15 / React 19 | frontend/realtime, shared @cargonerds/ui-library |
| Tests | xUnit v3 + Shouldly + Testcontainers | Microsoft.Testing.Platform |
Don't trust the .abpsln / README for versions
Cargonerds.abpsln and the root README still report ABP 10.0.0, LeptonX 5.0.0 and
net9.0. Those values are stale. The build truth is common.props (ABP 10.1.1,
net10.0) and the <TargetFramework> in each .csproj.
The application is generated from the ABP app template (<AbpProjectType>app</AbpProjectType>)
with Blazor WebAssembly UI, SQL Server, the RabbitMQ distributed event bus, the
tiered deployment shape and multi-tenancy enabled. ABP commercial modules in use include
Identity Pro, Account Pro, OpenIddict Pro, Audit Logging Pro, CmsKit Pro, SaaS, Chat, File
Management, GDPR, Language Management, Text Template Management and the LeptonX theme.
Architecture at a glance¶
Cargonerds is a single deployable solution composed of many ABP modules. An ABP module is a
class deriving from Volo.Abp.Modularity.AbpModule that declares its dependencies with
[DependsOn(...)]; ABP resolves the transitive graph from one root module per host and runs
each module's lifecycle hooks in dependency order. See ABP's
module system docs.
The code is organised into three module families, all in one repository:
Cargonerds.*- the application shell, undersrc/. It owns the runnable hosts and depends on everything else.Hub-modules/hub/. Shipments, consignments, organisations, documents, orders and tracking. Uses theHubconnection string.Pricing-modules/pricing/. Quotations, offers and margin calculation. Built on top of Hub (Pricing depends on Hub's domain).
flowchart TD
subgraph Hosts["Runnable hosts (src/)"]
AuthServer["Cargonerds.AuthServer<br/>(OpenIddict OAuth/OIDC)"]
ApiHost["Cargonerds.HttpApi.Host<br/>(REST + OData API)"]
Blazor["Cargonerds.Blazor<br/>(WASM admin UI)"]
WebPublic["Cargonerds.Web.Public<br/>(CmsKit MVC site)"]
Migrator["Cargonerds.DbMigrator<br/>(migrate + seed)"]
end
subgraph Frontends["Front ends (frontend/)"]
Realtime["realtime (Next.js 15)"]
UiLib["ui-library (React)"]
end
subgraph Modules["Business modules"]
ShellMod["Cargonerds.* shell"]
PricingMod["Pricing.*"]
HubMod["Hub.*"]
end
subgraph Infra["Backing services"]
Sql[("SQL Server<br/>Default / Hub / Pricing")]
Redis[("Redis<br/>cache + locks")]
Rabbit[("RabbitMQ<br/>event bus")]
end
ShellMod --> PricingMod --> HubMod
ApiHost --> ShellMod
Blazor --> ShellMod
WebPublic --> ShellMod
AuthServer --> ShellMod
Migrator --> ShellMod
Realtime --> UiLib
Realtime -->|REST/OData| ApiHost
Realtime -->|OIDC| AuthServer
Blazor -->|REST/OData| ApiHost
ApiHost --- Sql
ApiHost --- Redis
ApiHost --- Rabbit
AuthServer --- Sql
AppHost["Cargonerds.AppHost<br/>(.NET Aspire orchestrator)"] -.orchestrates.-> Hosts
AppHost -.orchestrates.-> Frontends
AppHost -.provisions.-> Infra
Layered, per module¶
Every module family repeats the canonical ABP layered DDD structure:
Domain.Shared -> Domain -> EntityFrameworkCore
Domain.Shared -> Application.Contracts -> Application
Application.Contracts -> HttpApi / HttpApi.Client
... -> Blazor / Blazor.Client / Web.Public / AuthServer / HttpApi.Host / DbMigrator (hosts)
The shell layers reference both module families. For example
src/Cargonerds.Domain/Cargonerds.Domain.csproj references Hub.Domain, Pricing.Domain and
Pricing.Application.Contracts by ProjectReference, and Cargonerds.EntityFrameworkCore
references Hub.EntityFrameworkCore + Pricing.EntityFrameworkCore. The full diagram and an
explanation of each layer is in Layered Architecture.
Runnable hosts¶
Each host is a separate process with its own ABP root module that it bootstraps via
AddApplicationAsync<TRootModule>():
| Host project | Root module | Purpose |
|---|---|---|
Cargonerds.AuthServer |
CargonerdsAuthServerModule |
OpenIddict OAuth 2.0 / OIDC server, external login providers |
Cargonerds.HttpApi.Host |
CargonerdsHttpApiHostModule |
REST + OData API, Swagger, CORS, Redis, RabbitMQ, health checks |
Cargonerds.Blazor |
CargonerdsBlazorModule |
Blazor WebAssembly admin UI (server host + WASM client) |
Cargonerds.Web.Public |
CargonerdsWebPublicModule |
Public MVC site (CmsKit Pro) |
Cargonerds.DbMigrator |
CargonerdsDbMigratorModule |
Console app: applies migrations and seeds data |
All server hosts use ABP's Autofac integration (.UseAutofac()); the WASM client uses
AbpAutofacWebAssemblyModule. The host startup order is consistent - Aspire/Service-Defaults
config is layered in before ABP boots:
var builder = WebApplication.CreateBuilder(args);
builder.AddServiceDefaults(); // Aspire: OTel, health, service discovery, config layering
builder.AddRedisClient("redis"); // Aspire-resolved Redis for ABP's cache
builder
.Host.AddAppSettingsSecretsJson()
.UseAutofac() // ABP DI container
.UseSerilog(/* ... */);
await builder.AddApplicationAsync<CargonerdsHttpApiHostModule>(); // ABP module bootstrap
var app = builder.Build();
await app.InitializeApplicationAsync();
await app.RunAsync();
The module map, the [DependsOn] topology and what each module configures are documented in
Modules Overview, Hub and
Pricing.
How the pieces fit together¶
One database "Default" - several DbContexts¶
There are three logical SQL connection strings, defined in
src/Cargonerds.Domain.Shared/Configuration/ConnectionStringNames.cs:
public static class ConnectionStringNames
{
public const string HubServiceBus = "hubServiceBus";
public const string HubDb = "Hub";
public const string SparkDb = "Default";
public const string BlobStorage = nameof(BlobStorage);
}
Default- the shell database.CargonerdsDbContextowns it and, crucially, folds the Identity Pro, SaaS and Permission Management tables into the same physical database using ABP's[ReplaceDbContext]attribute. This is what makes cross-module JOINs possible:
[ReplaceDbContext(typeof(IIdentityProDbContext))]
[ReplaceDbContext(typeof(ISaasDbContext))]
[ReplaceDbContext(typeof(IPermissionManagementDbContext))]
[ConnectionStringName(ConnectionStringNames.SparkDb)]
public class CargonerdsDbContext
: AbpDbContext<CargonerdsDbContext>,
ISaasDbContext,
IIdentityProDbContext,
IPermissionManagementDbContext
Hub-HubDbContext(shipments, organisations, tracking). Hub registers this context twice: once viaAddAbpDbContext<HubDbContext>for normal ABP repositories/unit of work, and once viaAddDbContextPool<HubDbContext>(poolSize: 256)for a pooled, second-level-cached read path (1-minute cache TTL). This dual registration is specific to Hub's performance design.Pricing-PricingDbContext, annotated[ConnectionStringName("Pricing")]. It is not merged viaReplaceDbContext; if noPricingconnection string is configured ABP falls back toDefault.
EF Core details, the repository registration conventions and the second-level cache are covered in Entity Framework Core.
Why fold modules into one DB?
[ReplaceDbContext] lets a module reuse this application's DbContext at runtime instead of
its own. Folding Identity/SaaS/Permissions into Default means user, tenant and permission
rows live alongside application data, so the app can JOIN across them in a single query. Hub
and Pricing stay separate because they don't need that coupling.
Authentication¶
Cargonerds.AuthServer is the OpenIddict OAuth 2.0 / OpenID Connect server (see ABP's
OpenIddict module). The API host validates JWT
bearer tokens (audience "Cargonerds") and additionally supports a custom API-key scheme for
machine-to-machine calls. Both the Blazor admin UI and the Next.js app authenticate against the
same AuthServer authority. API-key and JWT details are in
API Authentication.
APIs are generated, not hand-written¶
The host turns application services into REST endpoints with ABP's auto API controllers:
options.ConventionalControllers.Create(typeof(CargonerdsApplicationModule).Assembly);
options.ConventionalControllers.Create(typeof(HubApplicationModule).Assembly);
options.ConventionalControllers.Create(typeof(PricingApplicationModule).Assembly);
The matching strongly-typed C# clients are produced by ABP's
dynamic client proxies,
and the dashboard exposes a command that runs abp generate-proxy -t ng to regenerate TypeScript
proxies for the front ends. Hub also exposes OData on top of HubDbContext. See
REST API and OData, Filtering & Facets.
Orchestration with .NET Aspire¶
Cargonerds.AppHost is the .NET Aspire
orchestrator. Its top-level Program.cs declares the whole resource graph - SQL Server, Redis,
RabbitMQ, the runnable .NET hosts, the Node front ends and a documentation container - and wires
their endpoints, environment variables, health checks and start order. Cargonerds.AppHost is
not an ABP module; it never calls AddApplicationAsync.
Resource and service names live in one place,
src/Cargonerds.Domain.Shared/CargonerdsConsts.cs:
public static Service Auth = new("auth");
public static Service Public = new("web");
public static Service Admin = new("admin");
public static Service Api = new("api");
public static Service Redis = new("redis");
public static Service Messaging = new("messaging");
public static Service Migrator = new("db-migrator");
Cargonerds.ServiceDefaults is the shared Aspire library every host calls via
AddServiceDefaults(); it adds OpenTelemetry, health checks, HTTP resilience, service discovery
and a multi-layer configuration pipeline. The full orchestration story - run vs publish mode,
service-discovery token replacement, the spark-DB-against-Azure mode - is in
.NET Aspire Integration.
Tests and local runs need Docker
Aspire run mode provisions real SQL Server, Redis, RabbitMQ and Azurite containers, and the
test suite uses Testcontainers to do the same. Docker must be running. The AppHost even
fails fast (EnsureDockerRunningIfLocalDebug) when Docker is down during local debug.
Front ends¶
frontend/ is a separate npm-workspaces root ("workspaces": ["realtime","ui-library"]), not
part of the .NET build:
realtime- the customer-facing Next.js 15 / React 19 app. It authenticates against the AuthServer via OIDC (react-oidc-context/oidc-client-ts) and consumes the REST/OData API.ui-library(@cargonerds/ui-library) - the shared React component/design system it consumes.
See Realtime (Next.js) Frontend and Blazor WebAssembly.
Repository layout¶
A quick map of the top-level folders (the full breakdown, solution files and central build configuration are in Solution & Build):
| Path | Contents |
|---|---|
src/ |
The Cargonerds.* shell projects + Cargonerds.AppHost + Cargonerds.ServiceDefaults |
modules/hub/ |
The Hub ABP module (own src/, test/, .sln, common.props) |
modules/pricing/ |
The Pricing ABP module (built on Hub) |
test/ |
Main-app tests (xUnit v3 + Testcontainers) |
frontend/ |
npm workspaces: realtime (Next.js) + ui-library (React) |
etc/ |
Ops: ABP Studio profiles, Docker, Helm charts, Bruno API client |
docs/ |
This MkDocs site |
common.props |
Root shared MSBuild props (versions, conventions), imported by every .csproj |
Directory.Packages.props |
Central Package Management - every package version |
Which solution to open
Cargonerds.sln is the app-only solution (shell + Aspire). Cargonerds.All.sln /
Cargonerds.All.slnx is the superset that also includes the Hub and Pricing module projects
and is what CI uses (dotnet test "Cargonerds.All.sln"). Each module also has its own
Hub.sln / Pricing.sln.
Gotchas worth knowing on day one¶
- Versions live in
common.props, notDirectory.Packages.props. Bumping ABP, LeptonX or Aspire is a one-line edit to$(AbpVersion)/$(LeptonXVersion)/$(AspireVersion)incommon.props; the CPM file references those variables. - The Aspire AppHost SDK version (
13.1.0) differs fromAspireVersion(13.2.2). They are two separate knobs - the SDK is hard-coded inCargonerds.AppHost.csproj, the packages come from CPM. appsettings.azure.*.jsoninCargonerds.ServiceDefaultscontain real, committed credentials. Treat them as a secret-exposure risk; rotated values must not be re-committed.- Pre-commit formatting is mandatory and self-modifying. The git hook runs CSharpier over all
C# and Prettier over staged
frontend/files, then re-stages them, so the committed bytes are the formatted ones. Create git worktrees only under.claude/worktrees/or.worktrees/(see the repositoryAGENTS.md), never via.gitignore- CSharpier honours.gitignoreand would otherwise go blind.
Next steps¶
- Prerequisites - tools you need installed.
- Quick Start - get it running fast.
- Running Locally - the day-to-day local workflow and
SPARK_ENVIRONMENT. - Architecture Overview - the deeper architecture tour.
- Solution & Build - repo layout and the build system.
- ABP Framework Patterns - the ABP conventions this code uses.
- .NET Aspire Integration - orchestration in detail.