Skip to content

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, under src/. It owns the runnable hosts and depends on everything else.
  • Hub - modules/hub/. Shipments, consignments, organisations, documents, orders and tracking. Uses the Hub connection 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:

src/Cargonerds.HttpApi.Host/Program.cs (excerpt)
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:

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. CargonerdsDbContext owns 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:
src/Cargonerds.EntityFrameworkCore/EntityFrameworkCore/CargonerdsDbContext.cs
[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 via AddAbpDbContext<HubDbContext> for normal ABP repositories/unit of work, and once via AddDbContextPool<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 via ReplaceDbContext; if no Pricing connection string is configured ABP falls back to Default.

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:

CargonerdsHttpApiHostModule (excerpt)
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:

CargonerdsConsts.Aspire.Service
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, not Directory.Packages.props. Bumping ABP, LeptonX or Aspire is a one-line edit to $(AbpVersion) / $(LeptonXVersion) / $(AspireVersion) in common.props; the CPM file references those variables.
  • The Aspire AppHost SDK version (13.1.0) differs from AspireVersion (13.2.2). They are two separate knobs - the SDK is hard-coded in Cargonerds.AppHost.csproj, the packages come from CPM.
  • appsettings.azure.*.json in Cargonerds.ServiceDefaults contain 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 repository AGENTS.md), never via .gitignore - CSharpier honours .gitignore and would otherwise go blind.

Next steps