Skip to content

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

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 (HubApplicationContractsModule depends on AbpFluentValidationModule).

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

HttpApi.Client

  • Strongly-typed C# dynamic client proxies for consuming the module's APIs from .NET. HubHttpApiClientModule generates 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 by Cargonerds.Blazor.Client. Its module class is UIBlazorModule (not HubUIBlazorModule) in namespace Hub.UI. It depends on AbpAspNetCoreComponentsWebThemingModule, 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.WebAssembly and Pricing.Blazor.WebAssembly.Bundling, also referenced by Cargonerds.Blazor.Client.

See Blazor UI.

Installer

  • A helper module (Hub.Installer, Pricing.Installer) used when adding the module to a host. It depends only on AbpVirtualFileSystemModule and 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: PreConfigureServicesConfigureServicesPostConfigureServicesOnPreApplicationInitializationOnApplicationInitialization. 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.

  1. 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 the modules/pricing/src/Pricing.* folder layout and common.props import.
  2. Write the *Module.cs per layer deriving from AbpModule, 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 matching Hub* layer like Pricing does.
  3. Register the DbContext in the EF Core module via AddAbpDbContext<MyDbContext> + AddDefaultRepositories<IMyDbContext>(includeAllEntities: true). Add a [ConnectionStringName(...)] on the DbContext/interface.
  4. Localization & virtual files: in Domain.Shared, Configure<AbpVirtualFileSystemOptions> with FileSets.AddEmbedded<MyDomainSharedModule>() and register an AbpLocalizationOptions resource.
  5. Wire it into the shell. Add the new layer modules to the corresponding Cargonerds.* layer's [DependsOn(...)] (e.g. add MyEntityFrameworkCoreModule to CargonerdsEntityFrameworkCoreModule). Folding a module into the graph there means every host that roots on a Cargonerds.* module picks it up transitively.
  6. 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.
  7. 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