Module Structure¶
Cargonerds is built from ABP modules. A module is a plain C# class that derives from
Volo.Abp.Modularity.AbpModule and carries a [DependsOn(...)] attribute listing the other modules
it needs. ABP walks this dependency graph from a single root module per host, topologically
sorts it, and runs each module's lifecycle hooks in dependency order. See the ABP
module system / modularity
docs for the framework concept.
This page documents the anatomy of the application's own modules — the Hub.* module under
modules/hub/, the Pricing.* module under modules/pricing/, and (for contrast) the
Cargonerds.* application shell under src/. The Hub module is the canonical, fully-featured
example; Pricing mirrors it and builds on top of Hub's domain.
What is not a module
The large abpSrc/ tree contains hundreds of Volo.Abp.* / Volo.* *Module.cs files — that
is the vendored ABP/LeptonX commercial framework source checked into the repo. It is
referenced as dependencies but is not Cargonerds code. Likewise, Cargonerds.AppHost is a
.NET Aspire orchestrator, not an ABP module — it has no AbpModule class and never calls
AddApplicationAsync. See Aspire integration.
Layers¶
Every module family repeats the standard ABP layered module convention. Each layer is a separate
.NET project with its own *Module.cs class. The Hub module's projects (under modules/hub/src/):
| Project | Module class | Responsibility |
|---|---|---|
Hub.Domain.Shared |
HubDomainSharedModule |
Constants, enums, error codes, settings keys, localization resources |
Hub.Domain |
HubDomainModule |
Entities, aggregate roots, value objects, repository interfaces, domain services |
Hub.EntityFrameworkCore |
HubEntityFrameworkCoreModule |
HubDbContext, entity mappings, repository implementations, migrations |
Hub.Application.Contracts |
HubApplicationContractsModule |
App-service interfaces, DTOs (incl. generated), permissions, validators |
Hub.Application |
HubApplicationModule |
App-service implementations, mapping profiles, background jobs, workers |
Hub.HttpApi |
HubHttpApiModule |
API controllers (mostly ABP auto-generated) |
Hub.HttpApi.Client |
HubHttpApiClientModule |
Strongly-typed C# client proxies for consuming the APIs |
Hub.UI |
UIBlazorModule |
Razor/Blazor component library (cards, lists, pages, menus, OData filters) |
Hub.Installer |
HubInstallerModule |
Helper module that ships embedded static assets/config when added to a host |
Hub.SourceGenerators |
(none) | Roslyn source generator (see Source generators) |
Domain.Shared¶
- Constants, enums and error codes.
- Settings keys and localization resources (e.g.
HubResource,Localization/Hub/*.json). - Shared, dependency-free types — this is the bottom layer; everything else depends on it.
HubDomainSharedModule registers its localization resources and embedded files here. The pattern
(Resources.Add<TResource>("en").AddBaseTypes(...).AddVirtualJson(path)) is the ABP
localization and
virtual file system
mechanism:
// modules/hub/src/Hub.Domain.Shared/HubDomainSharedModule.cs
public override void ConfigureServices(ServiceConfigurationContext context)
{
Configure<AbpVirtualFileSystemOptions>(options =>
{
options.FileSets.AddEmbedded<HubDomainSharedModule>();
});
Configure<AbpLocalizationOptions>(options =>
{
options
.Resources.Add<HubResource>("en")
.AddBaseTypes(typeof(AbpValidationResource))
.AddVirtualJson("/Localization/Hub");
// ... CodeEntitiesResource, RealtimeResource
});
Configure<AbpExceptionLocalizationOptions>(options =>
{
options.MapCodeNamespace("Hub", typeof(HubResource));
options.MapCodeNamespace("CodeEntities", typeof(CodeEntitiesResource));
});
}
Domain¶
- Entities and aggregate roots (e.g. Hub's
Shipment,Consol,Container,Organization). See DDD: Entities & aggregate roots. - Value objects
(e.g.
Coordinates,Dimension). - Repository interfaces, domain services and EF Core query filters.
- The
CodeGen.config.jsonthat drives DTO generation (see DTO code generation).
HubDomainModule itself is intentionally tiny — just dependencies, no service configuration:
// modules/hub/src/Hub.Domain/HubDomainModule.cs
[DependsOn(
typeof(AbpDddDomainModule),
typeof(AbpIdentityDomainModule),
typeof(VoloAbpCommercialSuiteTemplatesModule),
typeof(HubDomainSharedModule),
typeof(FileManagementDomainModule)
)]
public class HubDomainModule : AbpModule { }
Application.Contracts¶
- Application service interfaces and DTOs (including generated DTOs under
Generated/). See data transfer objects. - Permission definitions (
HubPermissions,PricingPermissions). See authorization & permissions. - Filter/request DTOs and FluentValidation validators (
HubApplicationContractsModuledepends onAbpFluentValidationModule).
Application¶
- Application service implementations and AutoMapper profiles.
- Generated mapping extensions under
Extensions/Generated/. - Background jobs, background workers, search providers and domain-facing helpers.
HubApplicationModule is where Hub wires up object mapping,
background jobs (Hangfire) and
a background worker. It also does meaningful work in the lifecycle hooks (see
Lifecycle hooks):
// modules/hub/src/Hub.Application/HubApplicationModule.cs
Configure<AbpAutoMapperOptions>(options => options.AddMaps<HubApplicationModule>(validate: true));
Configure<AbpBackgroundJobOptions>(options => options.AddJob<ExcelExportDispatcherJob>());
// ... Hangfire storage on the "Default" connection string, blob client, hosted consumer
EntityFrameworkCore¶
- The module
DbContext(e.g.HubDbContext) and its interface (IHubDbContext), entity mappings, repository implementations and migrations. See Entity Framework for the data-access deep dive.
This is the heaviest layer in Hub. HubEntityFrameworkCoreModule.ConfigureServices registers the
DbContext for ABP repositories and a second, pooled registration for the read path:
// modules/hub/src/Hub.EntityFrameworkCore/EntityFrameworkCore/HubEntityFrameworkCoreModule.cs
context.Services.AddAbpDbContext<HubDbContext>(options =>
{
options.AddDefaultRepositories<IHubDbContext>(includeAllEntities: true);
options.Entity<Shipment>(opt => opt.DefaultWithDetailsFunc = e => e.IncludeDefaultDetails());
// ... Consol, CustomsDeclaration, Order, Address, Tag, Quotation, Offer
options.AddRepository<Organization, OrganizationRepository>();
options.AddRepository<Margin, MarginRepository>();
options.AddRepository<ShipmentBase, ShipmentBaseRepository>();
options.AddRepository<CalculatedShipment, CalculatedShipmentRepository>();
});
context.Services.AddEFSecondLevelCache(options => { /* memory provider, 1-min absolute */ });
// Interceptors registered as singletons, then a SEPARATE pooled registration:
context.Services.AddDbContextPool<HubDbContext>((sp, opts) => { /* UseSqlServer + interceptors */ },
poolSize: 256);
AddDefaultRepositories is ABP's convention-based repository generation;
includeAllEntities: true generates repos for non-aggregate entities too, and custom repositories
are registered via options.AddRepository<TEntity, TRepo>(). The interface variant
AddDefaultRepositories<IHubDbContext>(...) ties repos to the module's DbContext interface.
Hub registers the DbContext twice — on purpose
HubEntityFrameworkCoreModule calls both AddAbpDbContext<HubDbContext> (for ABP's
repositories and unit of work) and AddDbContextPool<HubDbContext>(poolSize: 256) (for the
pooled, second-level-cached read path). Interceptors are resolved from DI inside the pool
factory. This dual registration is specific to Hub's performance design — see
Caching. The IHubDbContext interface carries
[ConnectionStringName("Hub")].
HttpApi¶
- API controllers. Most are not hand-written — ABP generates REST endpoints from the application services via the auto API controllers convention. The host opts each application assembly in explicitly (see Auto-API controllers are opted-in at the host).
HttpApi.Client¶
- Strongly-typed C# dynamic client proxies
for consuming the module's APIs from .NET.
HubHttpApiClientModulegenerates them from the contracts assembly:
// modules/hub/src/Hub.HttpApi.Client/HubHttpApiClientModule.cs
context.Services.AddHttpClientProxies(
typeof(HubApplicationContractsModule).Assembly,
HubRemoteServiceConsts.RemoteServiceName);
See API clients.
UI¶
- Hub:
Hub.UI— a Razor/Blazor component library (ShipmentCard, lists, pages, menus, OData filters) referenced byCargonerds.Blazor.Client. Its module class isUIBlazorModule(notHubUIBlazorModule) in namespaceHub.UI. It depends onAbpAspNetCoreComponentsWebThemingModule, registers an OData client, scoped UI state services, a menu contributor and the Blazor router assembly:
// modules/hub/src/Hub.UI/UIBlazorModule.cs
Configure<AbpNavigationOptions>(o => o.MenuContributors.Add(new UIMenuContributor()));
Configure<AbpRouterOptions>(o => o.AdditionalAssemblies.Add(typeof(UIBlazorModule).Assembly));
- Pricing:
Pricing.Blazor,Pricing.Blazor.WebAssemblyandPricing.Blazor.WebAssembly.Bundling, also referenced byCargonerds.Blazor.Client.
See Blazor UI.
Installer¶
- A helper module (
Hub.Installer,Pricing.Installer) used when adding the module to a host. It depends only onAbpVirtualFileSystemModuleand ships embedded static assets/configuration via the virtual file system:
// modules/hub/src/Hub.Installer/HubInstallerModule.cs
[DependsOn(typeof(AbpVirtualFileSystemModule))]
public class HubInstallerModule : AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
Configure<AbpVirtualFileSystemOptions>(options =>
{
options.FileSets.AddEmbedded<HubInstallerModule>();
});
}
}
The Module.cs class and [DependsOn]¶
Every layer's *Module.cs declares its dependencies with [DependsOn(...)]. ABP resolves the
transitive closure of these and de-duplicates diamond dependencies automatically (Hub is reached
via both Pricing and Cargonerds, but loaded once).
The dependency chains follow the layered convention, and the Pricing → Hub → ABP chaining repeats
per layer:
flowchart TB
subgraph Hub["modules/hub"]
HDS[HubDomainSharedModule]
HD[HubDomainModule]
HEF[HubEntityFrameworkCoreModule]
HAC[HubApplicationContractsModule]
HA[HubApplicationModule]
end
subgraph Pricing["modules/pricing"]
PDS[PricingDomainSharedModule]
PD[PricingDomainModule]
PEF[PricingEntityFrameworkCoreModule]
end
HDS --> HD --> HEF
HDS --> HAC --> HA
HD --> HAC
PDS --> PD --> PEF
PD -.depends on.-> HD
PDS -.depends on.-> HDS
PEF -.depends on.-> HEF
HD --> ABP[(ABP framework<br/>modules)]
HDS --> ABP
Concrete examples of the same layer in each family:
// Hub EF Core layer:
[DependsOn(typeof(HubDomainModule), typeof(AbpEntityFrameworkCoreModule))]
public class HubEntityFrameworkCoreModule : AbpModule { ... }
// Pricing EF Core layer — note it depends on the Hub layer below it:
[DependsOn(typeof(HubEntityFrameworkCoreModule), typeof(PricingDomainModule),
typeof(AbpEntityFrameworkCoreModule))]
public class PricingEntityFrameworkCoreModule : AbpModule { ... }
// Pricing domain builds on Hub's domain:
[DependsOn(typeof(HubDomainModule), typeof(AbpDddDomainModule),
typeof(VoloAbpCommercialSuiteTemplatesModule), typeof(PricingDomainSharedModule))]
public class PricingDomainModule : AbpModule { }
The Cargonerds.* shell variant of each layer always pulls in the Pricing* + Hub* module of
that layer plus the matching ABP commercial modules. For the full module map and root-module
bootstrapping, see Solution structure,
Layered architecture and the
Modules overview.
Lifecycle hooks¶
ABP runs hooks in dependency order:
PreConfigureServices → ConfigureServices → PostConfigureServices →
OnPreApplicationInitialization → OnApplicationInitialization. The
options pattern
(Configure<TOptions> / PreConfigure<TOptions>) is the dominant configuration mechanism.
Most Cargonerds module layers only override ConfigureServices. Hub's Application layer is the
notable exception — it uses the async initialization hooks to seed "super type" CodeEntity data via
reflection over ISuperType<> implementations and to register a background worker:
// modules/hub/src/Hub.Application/HubApplicationModule.cs
public override async void OnApplicationInitialization(ApplicationInitializationContext context)
{
await context.AddBackgroundWorkerAsync<ExcelExportSchedulerWorker>();
}
public override async Task OnPreApplicationInitializationAsync(ApplicationInitializationContext context)
{
await base.OnPreApplicationInitializationAsync(context);
await InitializeSuperTypes(context.ServiceProvider); // reflects over ISuperType<>
}
OnApplicationInitialization here is async void
HubApplicationModule.OnApplicationInitialization is async void — exceptions thrown after the
first await are not observed by the caller. The properly-awaited initialization path is
OnPreApplicationInitializationAsync (super-type seeding). Prefer the async hook for anything
that must not silently fail.
Source generators¶
The Hub module uses two distinct, unrelated code-generation mechanisms. Do not confuse them. See Code generation for the broader picture.
Hub.SourceGenerators (Roslyn)¶
Hub.SourceGenerators is a Roslyn incremental source generator project. It targets
netstandard2.0 with LangVersion=preview, references Microsoft.CodeAnalysis.CSharp, and is not
packed / not output as a normal assembly (IncludeBuildOutput=false):
<!-- modules/hub/src/Hub.SourceGenerators/Hub.SourceGenerators.csproj -->
<TargetFramework>netstandard2.0</TargetFramework>
<LangVersion>preview</LangVersion>
<IsPackable>false</IsPackable>
<IncludeBuildOutput>false</IncludeBuildOutput>
It is consumed by Hub.Domain as an analyzer reference (not a normal project reference), so it
runs at compile time and emits source rather than being linked:
<!-- modules/hub/src/Hub.Domain/Hub.Domain.csproj -->
<ProjectReference Include="..\Hub.SourceGenerators\Hub.SourceGenerators.csproj"
OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
The single generator, StaticEnumGenerator ([Generator(LanguageNames.CSharp)],
IIncrementalGenerator), scans for enums annotated with
Hub.Domain.Shared.Attributes.StaticEnumAttribute and, for each, emits a Static{EnumName} class
(file Static{EnumName}.g.cs) with one nested sealed type per enum member plus a ToEnum<T>()
lookup. The marker attribute is trivial:
// modules/hub/src/Hub.Domain.Shared/Attributes/StaticEnumAttribute.cs
[AttributeUsage(AttributeTargets.Enum)]
public sealed class StaticEnumAttribute : Attribute { }
DTO code generation¶
A separate mechanism generates DTOs. Hub.Domain references the Nextended.CodeGen NuGet package
and exposes CodeGen.config.json as an MSBuild AdditionalFiles item, which the generator reads:
<!-- modules/hub/src/Hub.Domain/Hub.Domain.csproj -->
<PackageReference Include="Nextended.CodeGen" />
...
<AdditionalFiles Include="CodeGen.config.json" />
CodeGen.config.json configures the DTO generation — namespace, suffix, and crucially the output
paths into the sibling layers:
// modules/hub/src/Hub.Domain/CodeGen.config.json (excerpt)
{
"DtoGeneration": {
"Namespace": "Hub.Application.Contracts.Shipments",
"Suffix": "Dto",
"OutputPath": "../Hub.Application.Contracts/Generated/",
"MappingOutputPath": "../Hub.Application/Extensions/Generated/"
}
}
This is why the layer descriptions mention generated DTOs under Hub.Application.Contracts/Generated/
and mapping extensions under Hub.Application/Extensions/Generated/. (Note: the codebase generally
uses Mapperly for runtime mapping; Hub.Domain references Riok.Mapperly.)
Adding a new module¶
The fastest, correct path is to copy an existing module and adjust it. Pricing is the simplest template (it has no custom repositories yet); Hub is the most complete.
- Scaffold the layers. Create one project per layer you need
(
MyModule.Domain.Shared,.Domain,.EntityFrameworkCore,.Application.Contracts,.Application,.HttpApi,.HttpApi.Client, plus UI/Installer if applicable). Mirror themodules/pricing/src/Pricing.*folder layout andcommon.propsimport. - Write the
*Module.csper layer deriving fromAbpModule, each with the appropriate[DependsOn(...)]. Chain the layers (Domain.Shared → Domain → EntityFrameworkCore, etc.) and add the ABP base module per layer (AbpDddDomainModule,AbpEntityFrameworkCoreModule,AbpDddApplicationModule, …). If your module builds on Hub, depend on the matchingHub*layer like Pricing does. - Register the DbContext in the EF Core module via
AddAbpDbContext<MyDbContext>+AddDefaultRepositories<IMyDbContext>(includeAllEntities: true). Add a[ConnectionStringName(...)]on the DbContext/interface. - Localization & virtual files: in
Domain.Shared,Configure<AbpVirtualFileSystemOptions>withFileSets.AddEmbedded<MyDomainSharedModule>()and register anAbpLocalizationOptionsresource. - Wire it into the shell. Add the new layer modules to the corresponding
Cargonerds.*layer's[DependsOn(...)](e.g. addMyEntityFrameworkCoreModuletoCargonerdsEntityFrameworkCoreModule). Folding a module into the graph there means every host that roots on aCargonerds.*module picks it up transitively. - Expose the API. Auto-API controllers are opted in at the host, not in the module. Add a
ConventionalControllers.Create(typeof(MyApplicationModule).Assembly)line — see below. - Add it to a solution file (
Cargonerds.sln/Cargonerds.All.sln).
Auto-API controllers are opted-in at the host¶
The host enumerates each application assembly explicitly — a new module's endpoints will not appear until you add its line here:
// src/Cargonerds.HttpApi.Host/CargonerdsHttpApiHostModule.cs (~line 270)
options.ConventionalControllers.Create(typeof(CargonerdsApplicationModule).Assembly);
options.ConventionalControllers.Create(typeof(HubApplicationModule).Assembly);
options.ConventionalControllers.Create(typeof(PricingApplicationModule).Assembly);
Gotchas¶
abpSrc/ is framework source, not app code
A naive **/*Module.cs search returns hundreds of Volo.* modules from the vendored
ABP/LeptonX source under abpSrc/. Only Cargonerds.*, Hub.* and Pricing.* are application
code. (Tooling note: in this repo the Glob tool reliably matched module files only when given
an absolute search path.)
Class/namespace mismatch in Hub.UI
The Blazor UI module class is UIBlazorModule (not HubUIBlazorModule), in namespace Hub.UI,
file UIBlazorModule.cs. Easy to miss when searching by the expected Hub* prefix.
Pricing's Pricing connection string falls back to Default
PricingDbContext is annotated [ConnectionStringName("Pricing")]; if no "Pricing" connection
string is configured, ABP falls back to "Default". Pricing is not merged via
ReplaceDbContext, so it is a logically separate context even when it physically points at the
Default DB. The three names live in
src/Cargonerds.Domain.Shared/Configuration/ConnectionStringNames.cs
(HubDb = "Hub", SparkDb = "Default").
Provider + interceptors must be configured together
In CargonerdsEntityFrameworkCoreModule the SQL Server provider and the
ArithAbortConnectionInterceptor are set in one options.Configure(ctx => ...). The inline
code comment warns that a separate, later Configure overwrites the provider configuration →
"No database provider configured".
Two code generators, two mechanisms
Hub.SourceGenerators (Roslyn, OutputItemType="Analyzer") and the Nextended.CodeGen DTO
generator (AdditionalFiles + CodeGen.config.json) are independent. Generated DTOs land in
Hub.Application.Contracts/Generated/ and mappings in Hub.Application/Extensions/Generated/
because of the output paths in CodeGen.config.json, not because of the Roslyn generator.
ReplaceDbContext is a shell concern, not a module concern
Only CargonerdsDbContext uses [ReplaceDbContext(...)] (for IIdentityProDbContext,
ISaasDbContext, IPermissionManagementDbContext) to fold those ABP modules' tables into the
Default database. No ReplaceDbContext exists anywhere under modules/ — Hub and Pricing keep
their own DbContexts. See Entity Framework.
See also¶
- Modules overview — the three module families and how they relate.
- Hub module and Pricing module — per-module detail.
- Cargonerds shell — the application host modules.
- Solution structure and Layered architecture — the full module map and root-module bootstrapping.
- ABP patterns — framework patterns implemented across these layers.
- Code generation — source generators and DTO generation in depth.