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:
DomainandApplicationnever referenceEntityFrameworkCore. Persistence is reached only through repository interfaces declared inDomain; the implementations live inEntityFrameworkCoreand are wired up by DI. See ABP Repositories.- The presentation tier depends on
Application.Contracts, notApplication. A UI either calls the application services in-process (a server-side host that also loadsApplication) or over HTTP throughHttpApi.Clientproxies — but it only compiles against theApplication.Contractsinterfaces 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 (PreConfigureServices → ConfigureServices
→ PostConfigureServices → OnPreApplicationInitialization → OnApplicationInitialization) 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]—CargonerdsDbContextfolds 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(...)(theIIdentityUserRepositoryswap inCargonerdsEntityFrameworkCoreModule), and Scrutorcontext.Services.Decorate<IPermissionAppService, ApiKeyPermissionAppService>()inCargonerdsApplicationModule. - The options pattern across modules —
Configure<TOptions>/PreConfigure<TOptions>lets an outer module tune an inner module without a service-locator dependency (e.g.CargonerdsDomainModulemutatingCmsKitCommentOptionsandAbpPermissionOptions). - Cross-module permission/group augmentation —
CargonerdsPermissionDefinitionProviderreaches into the Identity and CmsKit permission groups withGetGroup(...).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:
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.
AbpEntityFrameworkCoreSqlServerModuleis referenced only byCargonerdsEntityFrameworkCoreModule. TheHub/PricingEF Core modules depend on the provider-agnosticAbpEntityFrameworkCoreModule; they assume a host composes them with a provider. Running a Hub/Pricing layer without the shell would have no DBMS configured. AbpStudioAnalyzeHelper.IsInAnalyzeModeearly-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 isUIBlazorModule, notHubUIBlazorModule(namespaceHub.UI, fileUIBlazorModule.cs) — easy to miss when searching the presentation layer.Cargonerds.AppHostis not an ABP module. It is the .NET Aspire orchestrator; it has noAbpModuleand no[DependsOn]. Do not treat it as part of the layer graph.abpSrc/is framework source. A naive**/*Module.cssearch returns hundreds ofVolo.*framework modules. OnlyCargonerds.*,Hub.*, andPricing.*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¶
- Architecture Overview — the big picture and how the pieces fit at runtime.
- Solution Structure — every project, the root modules, and bootstrapping.
- ABP Patterns — the framework conventions (
ReplaceDbContext,AddDefaultRepositories, options pattern, design-time factory) used to implement these layers. - Modules Overview and Module Structure —
the
Cargonerds/Hub/Pricingfamilies in detail. - DDD Overview, Application Services, and Entity Framework Core — deep dives into individual layers.