Skip to content

Layered Architecture

Cargonerds follows the ABP layered architecture: dependencies flow inward, toward the domain. Outer layers (UI, HTTP API) depend on inner layers (application, domain); inner layers never depend on outer ones. The same layering is repeated three times — once for the application shell (Cargonerds.* under src/) and once for each business module (Hub.* and Pricing.* under modules/).

Each layer is an ABP module: a class deriving from Volo.Abp.Modularity.AbpModule that carries a [DependsOn(...)] attribute. The [DependsOn] attributes are the layer boundaries — there is no separate enforcement mechanism. A project can only use types from another project if a project reference exists, and the project references mirror the [DependsOn] graph, so the dependency rules are enforced at compile time by the C# project graph itself.

Scope of this page

This page documents the application's own layers (Cargonerds.*, Hub.*, Pricing.*). The abpSrc/ tree is the vendored ABP/LeptonX framework source checked into the repo; those Volo.Abp.* modules are referenced as dependencies but are not Cargonerds code. For the project inventory see the Solution Structure and Modules Overview.

Dependency flow

flowchart TD
    subgraph Hosts["Host layer (root modules)"]
        Host["HttpApi.Host / AuthServer<br/>Blazor / DbMigrator / Web.Public"]
    end

    UI["Presentation<br/>Blazor / Web.Public / Controllers"]
    HttpApi["HttpApi<br/>(auto API controllers)"]
    HttpApiClient["HttpApi.Client<br/>(dynamic proxies)"]
    App["Application<br/>(app services, mappers)"]
    AppContracts["Application.Contracts<br/>(DTOs, service interfaces, permissions)"]
    Domain["Domain<br/>(entities, repos interfaces, domain services)"]
    DomainShared["Domain.Shared<br/>(consts, enums, errors, localization)"]
    Ef["EntityFrameworkCore<br/>(DbContext, repo impls, migrations)"]

    Host --> UI
    Host --> HttpApi
    Host --> App
    Host --> Ef
    UI --> AppContracts
    UI --> HttpApiClient
    HttpApi --> AppContracts
    HttpApiClient --> AppContracts
    App --> AppContracts
    App --> Domain
    AppContracts --> DomainShared
    Domain --> DomainShared
    Ef --> Domain

The arrows are [DependsOn] edges (transitive ABP framework dependencies omitted). Note the two rules that make the architecture work:

  • Domain and Application never reference EntityFrameworkCore. Persistence is reached only through repository interfaces declared in Domain; the implementations live in EntityFrameworkCore and are wired up by DI. See ABP Repositories.
  • The presentation tier depends on Application.Contracts, not Application. A UI either calls the application services in-process (a server-side host that also loads Application) or over HTTP through HttpApi.Client proxies — but it only compiles against the Application.Contracts interfaces and DTOs.

The layers, project by project

Every business capability is split across this stack of projects. The table below names the concrete project for each of the three families and what it owns in this codebase. (Module class names and paths are listed in the Solution Structure and Module Structure pages.)

Layer Cargonerds.* (shell) Hub.* Pricing.* What lives here
Domain.Shared Cargonerds.Domain.Shared Hub.Domain.Shared Pricing.Domain.Shared Consts, enums, error codes, localization resources, attributes, feature/global-feature definitions
Domain Cargonerds.Domain Hub.Domain Pricing.Domain Entities & aggregate roots, value objects, domain services, repository interfaces, settings providers
EntityFrameworkCore Cargonerds.EntityFrameworkCore Hub.EntityFrameworkCore Pricing.EntityFrameworkCore DbContext, IEntityTypeConfigurations, repository implementations, interceptors, migrations
Application.Contracts Cargonerds.Application.Contracts Hub.Application.Contracts Pricing.Application.Contracts DTOs, app-service interfaces, permission definitions, FluentValidation validators
Application Cargonerds.Application Hub.Application Pricing.Application App-service implementations, Mapperly/AutoMapper mappers, background jobs/workers
HttpApi Cargonerds.HttpApi Hub.HttpApi Pricing.HttpApi Auto API controllers, API localization, OData (Hub)
HttpApi.Client Cargonerds.HttpApi.Client Hub.HttpApi.Client Pricing.HttpApi.Client Dynamic C# client proxies
Presentation / hosts Cargonerds.Blazor(.Client), Cargonerds.Web.Public, Cargonerds.HttpApi.Host, Cargonerds.AuthServer, Cargonerds.DbMigrator Hub.UI (UIBlazorModule) Pricing.Blazor, Pricing.Blazor.WebAssembly(.Bundling) Blazor UI, MVC public site, runnable hosts

The Hub module is where the domain lives

Hub.* carries essentially all real business modeling (~160 entity files: shipments, consols, containers, organizations, tracking, and the pricing entities). The Cargonerds.* shell is mostly framework plumbing (identity, API keys, data seeding). Pricing.* is thin — its Quotation/Offer/Margin entities actually live under modules/hub/src/Hub.Domain/Entities/Pricing/. See Modules Overview.

Domain.Shared

The innermost layer. It depends on nothing above it and only on leaf ABP framework modules, so it can be referenced by every other layer and shipped to the WebAssembly client. HubDomainSharedModule declares exactly that:

// modules/hub/src/Hub.Domain.Shared/HubDomainSharedModule.cs
[DependsOn(
    typeof(AbpValidationModule),
    typeof(AbpDddDomainSharedModule),
    typeof(AbpAccountPublicApplicationContractsModule),
    typeof(FileManagementDomainSharedModule)
)]
public class HubDomainSharedModule : AbpModule

This layer registers the localization resources and the embedded virtual file system file set:

Configure<AbpVirtualFileSystemOptions>(options =>
{
    options.FileSets.AddEmbedded<HubDomainSharedModule>();
});

Configure<AbpLocalizationOptions>(options =>
{
    options.Resources.Add<HubResource>("en")
        .AddBaseTypes(typeof(AbpValidationResource))
        .AddVirtualJson("/Localization/Hub");
    // CodeEntitiesResource, RealtimeResource ...
});

Domain

Entities, value objects, domain services and repository interfaces. HubDomainModule adds only domain-flavoured dependencies — it never references an EntityFrameworkCore module:

// modules/hub/src/Hub.Domain/HubDomainModule.cs
[DependsOn(
    typeof(AbpDddDomainModule),
    typeof(AbpIdentityDomainModule),
    typeof(VoloAbpCommercialSuiteTemplatesModule),
    typeof(HubDomainSharedModule),
    typeof(FileManagementDomainModule)
)]
public class HubDomainModule : AbpModule { }

Repository contracts sit here (e.g. IShipmentBaseRepository, IOrganizationRepository under modules/hub/src/Hub.Domain/Repositories/); their EF Core implementations live one layer out. The single ABP-style DomainService in the whole solution is ApiKeyManager (src/Cargonerds.Domain/ApiKeys/ApiKeyManager.cs). For the entity taxonomy see Aggregate Roots and Entities; for the repository pattern see Repositories.

EntityFrameworkCore (infrastructure)

The infrastructure layer. It depends on Domain (to implement its repository interfaces and map its entities) plus AbpEntityFrameworkCoreModule:

// modules/hub/src/Hub.EntityFrameworkCore/.../HubEntityFrameworkCoreModule.cs
[DependsOn(typeof(HubDomainModule), typeof(AbpEntityFrameworkCoreModule))]
public class HubEntityFrameworkCoreModule : AbpModule

This is where the dependency rule pays off: Hub.Domain knows nothing about SQL Server, second-level caching, or connection pooling — all of that is configured here:

context.Services.AddAbpDbContext<HubDbContext>(options =>
{
    options.AddDefaultRepositories<IHubDbContext>(includeAllEntities: true);
    options.Entity<Shipment>(opt => opt.DefaultWithDetailsFunc = e => e.IncludeDefaultDetails());
    // ...
    options.AddRepository<Organization, OrganizationRepository>();
    options.AddRepository<ShipmentBase, ShipmentBaseRepository>();
    // ...
});

AddDefaultRepositories(includeAllEntities: true) auto-generates IRepository<T> implementations even for non-aggregate entities; custom repositories are registered with options.AddRepository<TEntity, TRepo>(). Hub additionally wires a pooled AddDbContextPool<HubDbContext>(poolSize: 256) and an EF second-level cache. The full persistence story is documented under Entity Framework Core and Caching.

Application.Contracts

The public surface of a use case: app-service interfaces, DTOs, permission definitions and FluentValidation validators. It depends only on Domain.Shared (never on Domain), which is why the UI can compile against it without dragging in entities:

// modules/hub/src/Hub.Application.Contracts/HubApplicationContractsModule.cs
[DependsOn(
    typeof(HubDomainSharedModule),
    typeof(AbpDddApplicationContractsModule),
    typeof(AbpFluentValidationModule),
    typeof(AbpAuthorizationModule)
)]
public class HubApplicationContractsModule : AbpModule { }

AbpFluentValidationModule here means the validators in *.Application.Contracts/Validators/ are auto-applied to incoming DTOs. See DTOs & Contracts and Authorization.

Application

The use-case implementations. It depends on its own Domain and Application.Contracts:

// modules/hub/src/Hub.Application/HubApplicationModule.cs
[DependsOn(
    typeof(HubDomainModule),
    typeof(HubApplicationContractsModule),
    typeof(AbpDddApplicationModule),
    typeof(AbpAutoMapperModule),
    typeof(AbpBackgroundWorkersModule),
    typeof(AbpBackgroundJobsHangfireModule)
)]
public class HubApplicationModule : AbpModule

Object mapping is mostly compile-time Riok.Mapperly (the DtoMapper static partial class); AutoMapper is residual. The custom HubEntityBaseService<TEntity, TDto, ...> — not ABP's CrudAppService — is the project's read/list engine (cache → org filtering → OData → facets). See Application Services, Service Patterns, and ABP object-to-object mapping & Mapperly.

HttpApi and HttpApi.Client

HttpApi exposes the application services as REST endpoints; HttpApi.Client consumes them as typed proxies. Both depend on Application.Contracts only — neither references Application.

// modules/hub/src/Hub.HttpApi.Client/HubHttpApiClientModule.cs
[DependsOn(typeof(HubApplicationContractsModule), typeof(AbpHttpClientModule))]
public class HubHttpApiClientModule : AbpModule
{
    public override void ConfigureServices(ServiceConfigurationContext context)
    {
        context.Services.AddHttpClientProxies(
            typeof(HubApplicationContractsModule).Assembly,
            HubRemoteServiceConsts.RemoteServiceName);   // "Hub"
        // ...
    }
}

The controllers themselves are not hand-written; the host generates them from the application assemblies with ConventionalControllers.Create(...) (see Hosts below). For details see REST API, API Clients, ABP auto API controllers and dynamic C# client proxies.

Presentation and hosts

The Blazor admin UI (Cargonerds.Blazor / .Client, which also hosts Hub.UI and the Pricing Blazor modules), the Cargonerds.Web.Public MVC site (CmsKit Pro public), and the runnable hosts. The customer-facing Next.js app under frontend/realtime is a separate React build that talks to the same HTTP API; it is not an ABP module. Cargonerds.Web.Public exists in the repo but is not part of the orchestrated runtime. See Blazor UI and Realtime Frontend.

How the three families compose

Pricing.* builds on Hub.* (Pricing extends Hub's domain), and the Cargonerds.* shell depends on both plus the full ABP commercial suite. The dependency is expressed at every layer — the Pricing layer depends on the matching Hub layer, and the Cargonerds layer depends on both:

flowchart LR
    subgraph C["Cargonerds.* (shell)"]
        direction TB
        CApp["Application"] --> CEf["EntityFrameworkCore"]
    end
    subgraph P["Pricing.*"]
        direction TB
        PApp["Application"] --> PEf["EntityFrameworkCore"]
    end
    subgraph H["Hub.*"]
        direction TB
        HApp["Application"] --> HEf["EntityFrameworkCore"]
    end

    PApp --> HApp
    PEf --> HEf
    CApp --> PApp
    CApp --> HApp
    CEf --> PEf
    CEf --> HEf

Concrete evidence, layer by layer:

// modules/pricing/src/Pricing.Domain/PricingDomainModule.cs
[DependsOn(typeof(HubDomainModule), typeof(AbpDddDomainModule),
    typeof(VoloAbpCommercialSuiteTemplatesModule), typeof(PricingDomainSharedModule))]

// modules/pricing/src/Pricing.EntityFrameworkCore/.../PricingEntityFrameworkCoreModule.cs
[DependsOn(typeof(HubEntityFrameworkCoreModule), typeof(PricingDomainModule),
    typeof(AbpEntityFrameworkCoreModule))]

// modules/pricing/src/Pricing.Application/PricingApplicationModule.cs
[DependsOn(typeof(HubApplicationModule), typeof(PricingDomainModule),
    typeof(PricingApplicationContractsModule), typeof(AbpDddApplicationModule),
    typeof(AbpAutoMapperModule))]

And the shell layers pull in both:

// src/Cargonerds.EntityFrameworkCore/.../CargonerdsEntityFrameworkCoreModule.cs
[DependsOn(
    typeof(PricingEntityFrameworkCoreModule),
    typeof(HubEntityFrameworkCoreModule),
    typeof(CargonerdsDomainModule),
    typeof(AbpPermissionManagementEntityFrameworkCoreModule),
    typeof(AbpEntityFrameworkCoreSqlServerModule),   // SQL Server provider lives only here
    /* + ~13 more ABP EF Core modules */ )]
public class CargonerdsEntityFrameworkCoreModule : AbpModule

Diamond dependencies are fine

Hub.* is reached both directly (Cargonerds → Hub) and transitively (Cargonerds → Pricing → Hub). ABP de-duplicates the module graph automatically, loading each module exactly once. You do not need to remove the "redundant" direct [DependsOn] edges.

Hosts compose the graph

A layer is not runnable on its own — a host (a root module) ties the layers together. Each host calls AddApplicationAsync<TRootModule>() and ABP loads the entire transitive [DependsOn] closure of that root, then runs each module's lifecycle hooks (PreConfigureServicesConfigureServicesPostConfigureServicesOnPreApplicationInitializationOnApplicationInitialization) in dependency order.

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

The API host is the canonical example. It is the only place that references all the layers at once (HttpApi, Application, EntityFrameworkCore) and where the auto-API controllers are generated from each family's application assembly:

// src/Cargonerds.HttpApi.Host/CargonerdsHttpApiHostModule.cs
[DependsOn(
    typeof(CargonerdsHttpApiModule),
    typeof(AbpAutofacModule),
    typeof(CargonerdsApplicationModule),
    typeof(CargonerdsEntityFrameworkCoreModule),
    /* + Redis, RabbitMQ, Swashbuckle, Serilog, ... */ )]
public class CargonerdsHttpApiHostModule : AbpModule

// inside ConfigureConventionalControllers:
options.ConventionalControllers.Create(typeof(CargonerdsApplicationModule).Assembly);
options.ConventionalControllers.Create(typeof(HubApplicationModule).Assembly);
options.ConventionalControllers.Create(typeof(PricingApplicationModule).Assembly);

All server hosts call .UseAutofac() (ABP's DI container with property injection); the WebAssembly client uses AbpAutofacWebAssemblyModule. Bootstrapping is covered in depth on the Solution Structure page. Note that Cargonerds.AppHost is the .NET Aspire orchestrator — it does not call AddApplicationAsync and is not an ABP module; see Aspire Integration.

Crossing layer boundaries on purpose

The layering is strict, but ABP gives sanctioned ways to reach across it. This codebase uses several:

  • [ReplaceDbContext]CargonerdsDbContext folds several ABP module DbContexts into one physical database so cross-module JOINs work:

    // src/Cargonerds.EntityFrameworkCore/.../CargonerdsDbContext.cs
    [ReplaceDbContext(typeof(IIdentityProDbContext))]
    [ReplaceDbContext(typeof(ISaasDbContext))]
    [ReplaceDbContext(typeof(IPermissionManagementDbContext))]
    [ConnectionStringName(ConnectionStringNames.SparkDb)]   // "Default"
    public class CargonerdsDbContext
        : AbpDbContext<CargonerdsDbContext>,
          ISaasDbContext, IIdentityProDbContext, IPermissionManagementDbContext
    
  • Service replacement & decoration[Dependency(ReplaceServices = true)] + [ExposeServices] (e.g. IdentityUserAppService, WhiteLabelingTemplateRenderer), context.Services.Replace(...) (the IIdentityUserRepository swap in CargonerdsEntityFrameworkCoreModule), and Scrutor context.Services.Decorate<IPermissionAppService, ApiKeyPermissionAppService>() in CargonerdsApplicationModule.

  • The options pattern across modulesConfigure<TOptions> / PreConfigure<TOptions> lets an outer module tune an inner module without a service-locator dependency (e.g. CargonerdsDomainModule mutating CmsKitCommentOptions and AbpPermissionOptions).
  • Cross-module permission/group augmentationCargonerdsPermissionDefinitionProvider reaches into the Identity and CmsKit permission groups with GetGroup(...).AddChild(...).

These are deliberate, framework-blessed extension points — not violations of the dependency rule.

Gotchas

Provider config and interceptors must share one Configure action

In CargonerdsEntityFrameworkCoreModule the SQL Server provider and the ArithAbortConnectionInterceptor are set in a single options.Configure(ctx => ...). An inline comment warns that a separate later Configure call overwrites the provider, yielding "No database provider configured" at runtime:

options.Configure(ctx =>
{
    ctx.UseSqlServer(b => b.UseParameterizedCollectionMode(ParameterTranslationMode.Parameter));
    ctx.DbContextOptions.AddInterceptors(new ArithAbortConnectionInterceptor());
});

Only the Default DB is EF-migrated

Cargonerds.EntityFrameworkCore is the only project with a Migrations/ folder. builder.ConfigureHub() is commented out in CargonerdsDbContext, and Hub/Pricing ship no migrations — the Hub schema is managed outside EF Core. See Migrations.

  • The SQL Server provider is registered once, in the shell. AbpEntityFrameworkCoreSqlServerModule is referenced only by CargonerdsEntityFrameworkCoreModule. The Hub/Pricing EF Core modules depend on the provider-agnostic AbpEntityFrameworkCoreModule; they assume a host composes them with a provider. Running a Hub/Pricing layer without the shell would have no DBMS configured.
  • AbpStudioAnalyzeHelper.IsInAnalyzeMode early-returns. CargonerdsEntityFrameworkCoreModule (and several host modules) short-circuit Redis/SQL-touching config when ABP Studio statically analyzes the solution. Forgetting this guard makes static analysis try to connect to Redis/SQL.
  • Hub.UI's module class is UIBlazorModule, not HubUIBlazorModule (namespace Hub.UI, file UIBlazorModule.cs) — easy to miss when searching the presentation layer.
  • Cargonerds.AppHost is not an ABP module. It is the .NET Aspire orchestrator; it has no AbpModule and no [DependsOn]. Do not treat it as part of the layer graph.
  • abpSrc/ is framework source. A naive **/*Module.cs search returns hundreds of Volo.* framework modules. Only Cargonerds.*, Hub.*, and Pricing.* are application layers.

Why it matters

Keeping persistence and HTTP concerns out of Domain and Application means business logic is testable without a database, and the application services stay the single entry point for use cases. Repeating the layering per module lets Hub and Pricing be reasoned about independently while still composing into one deployable host through the [DependsOn] graph.

See also