Skip to content

Services Overview

This section is the index of application services in the Cargonerds solution. Application services are the entry points to business logic: they implement use cases, orchestrate repositories and domain logic, accept and return DTOs, and are auto-exposed as REST endpoints by ABP.

This page explains the concept, lists every service grouped by the module that owns it, documents the base classes they derive from, and points at the deeper per-service pages.

ABP reference

See the ABP Application Services documentation for the framework concepts this page builds on.

What application services do

  • Implement use cases and coordinate domain objects
  • Work through repositories (generic IRepository<TEntity, Guid> or custom interfaces)
  • Return DTOs to the presentation layer (never raw entities)
  • Enforce authorization and validation
  • Run inside an ABP Unit of Work, and are turned into REST controllers by Auto API Controllers

In this codebase the application layer is split across three ABP modules stacked by [DependsOn] (see Module system):

graph TD
    Host["Cargonerds host<br/>(src/Cargonerds.Application)<br/>identity, API keys, comments, org units"]
    Pricing["Pricing<br/>(modules/pricing)<br/>quotations, offers"]
    Hub["Hub<br/>(modules/hub)<br/>shipments, orders, documents, …"]
    Host -->|DependsOn| Pricing
    Host -->|DependsOn| Hub
    Pricing -->|DependsOn| Hub
  • Hub (modules/hub) is the dominant layer — almost all real business logic lives here.
  • Pricing (modules/pricing) is a satellite of Hub; its PricingApplicationModule DependsOn HubApplicationModule and reuses Hub's base service and Mapperly mapper. Notably, the pricing domain entities live in the Hub module (Hub.Entities.Pricing.*, persisted by HubDbContext), not in Pricing.Domain — see Pricing module.
  • Cargonerds host (src) is a thin glue layer that mostly extends or replaces ABP framework services rather than owning domain logic.

Scale

There are roughly 35 *AppService classes across the three *.Application projects (about 34 types whose name ends in AppService, two of which are the abstract HubAppService / CargonerdsAppService bases). The dependency root is CargonerdsApplicationModule (src/Cargonerds.Application/CargonerdsApplicationModule.cs), which DependsOn both PricingApplicationModule and HubApplicationModule plus a long list of ABP Pro modules (Identity, Account, SaaS, Chat, Audit Logging, OpenIddict, FileManagement, CmsKit Pro, LeptonX, …).

Service organization

Cargonerds host services

Located in src/Cargonerds.Application/ (namespaces under Cargonerds.*). These extend or replace ABP framework services and wire identity/users into the freight domain.

Service Description Documentation
IdentityUserAppService Replaces the ABP identity-user service Internal
UserImportAppService Bulk user import Internal
OrganizationUnitAppService Replaces the ABP org-unit service; org-code / owner wiring Internal
ApiKeyAppService / ApiKeyPermissionAppService API keys and their permissions Internal
ClientSettingsAppService / CustomScriptSettingAppService Client-specific & custom-script settings Internal
CurrentOrganizationAppService Current organization context Internal
CommentPublicAppService CmsKit comments (public surface) Internal
GlobalSearchAppService Cross-entity search orchestrator (runs Hub providers) Internal
SecurityLogExportAppService Identity security-log export Internal

Hub module services

Located in modules/hub/src/Hub.Application/Services/ (namespace Hub.Services). This is where the bulk of the freight-domain use cases live.

Service Description Documentation
ShipmentAppService Shipments / consols and CW1 booking Shipment Service
HubOrganizationAppService Organizations (trading partners) Organization Service
DocumentAppService Document metadata, blob streaming, view tokens Document Service
OrderAppService / PurchaseOrderAppService Sales orders & purchase orders Internal
BrokerageAppService Customs brokerage Internal
MarginAppService / CalculatedShipmentAppService Calculated / financial read-models Internal
NotificationAppService / NotificationSettingAppService Notifications & notification settings Internal
DashboardAppService / MyInsightsAppService / InsightShipmentsAppService Dashboards & insights Internal
ProfileAppService / TagAppService User profile & tagging Internal
CommodityAppService Commodity code entity Internal
ExcelExportAppService Excel export (background-job backed) Internal
TemplatePreviewAppService Email/text-template preview & send Internal

Insights service naming

The file Services/Insights/InsightsChartAppService.cs actually declares the class InsightShipmentsAppService — the file name and class name differ.

Pricing module services

Located in modules/pricing/src/Pricing.Application/Quotation/ (namespace Pricing.Quotation).

Service Description
QuotationAppService Quotation CRUD; enqueues the quote-generation background job
QuotationLegacyAppService The engine: calls the external pricing API, maps the response, applies margins
OfferAppService Manual offer CRUD; recomputes totals

Base service classes

Every service ultimately derives from ABP's ApplicationService, usually through one of two module-specific bases, and many Hub/Pricing services derive from a third, custom CRUD base.

ApplicationService (ABP)

The framework base for all services; provides ObjectMapper, CurrentUser, CurrentTenant, Logger, LazyServiceProvider, AuthorizationService, and localization (L).

CargonerdsAppService and HubAppService

Thin module bases that just set the localization resource (and, for Hub, the object-mapper context):

// src/Cargonerds.Application/CargonerdsAppService.cs
public abstract class CargonerdsAppService : ApplicationService
{
    protected CargonerdsAppService()
    {
        LocalizationResource = typeof(CargonerdsResource);
    }
}

// modules/hub/src/Hub.Application/HubAppService.cs
public abstract class HubAppService : ApplicationService
{
    protected HubAppService()
    {
        LocalizationResource = typeof(HubResource);
        ObjectMapperContext = typeof(HubApplicationModule);
    }
}

HubEntityBaseService<…> — the project's real CRUD/list engine

Most Hub and Pricing read/list services do not derive from ABP's CrudAppService. They derive from the custom HubEntityBaseService in modules/hub/src/Hub.Application/Services/HubEntityBaseService.cs. It implements IReadOnlyAppService<TDto, Guid, TListParamDto> and centralizes list/paging, OData-style filtering, faceting, caching, org-unit filtering and a ReadPermission hook. Subclasses implement ToDto and override hooks as needed.

There are two generic overloads — a 4-generic full form and a 3-generic convenience form that defaults TListParamDto to PagedAndSortedResultRequestWithFacetOptionsDto:

// modules/hub/src/Hub.Application/Services/HubEntityBaseService.cs
[ExposeServices(IncludeSelf = true)]
public abstract class HubEntityBaseService<TEntity, TDto, TListParamDto, TService>(
    IRepository<TEntity, Guid> repository,
    IServiceProvider serviceProvider
) : HubAppService, IReadOnlyAppService<TDto, Guid, TListParamDto>
    where TEntity : class, IEntity, IEntity<Guid>
    where TListParamDto : PagedAndSortedResultRequestWithFacetOptionsDto
    where TService : HubEntityBaseService<TEntity, TDto, TListParamDto, TService>
{
    [return: NotNullIfNotNull(nameof(entity))]
    protected abstract TDto? ToDto(TEntity? entity);

    protected virtual string ReadPermission => string.Empty;

    public virtual async Task<PagedResultDtoWithFilters<TDto>> GetListAsync(TListParamDto input, bool force);
}

A concrete service is typically tiny — it points the base at a repository, supplies a ToDto delegate (usually a Mapperly extension method), and declares its read permission:

// modules/hub/src/Hub.Application/Services/OrderAppService.cs
public class OrderAppService(IRepository<Order, Guid> repository, IServiceProvider serviceProvider)
    : HubEntityBaseService<Order, OrderDto, OrderAppService>(repository, serviceProvider),
        IOrderAppService
{
    protected override OrderDto? ToDto(Order? entity) => entity?.ToDto();

    protected override string ReadPermission => HubPermissions.Orders.View;
}

ShipmentAppService (modules/hub/src/Hub.Application/Services/ShipmentAppService.cs) is the richest example: it extends HubEntityBaseService<ShipmentBase, CombinedShipmentDto, ShipmentPageRequestDto, ShipmentAppService>, implements IShipmentAppService : ICrudAppService<CombinedShipmentDto, Guid, ShipmentPageRequestDto>, and does permission-gated includes — e.g. it conditionally includes documents and invoices based on AuthorizationService.IsGrantedAnyAsync(...):

// modules/hub/src/Hub.Application/Services/ShipmentAppService.cs
protected override async Task<IQueryable<ShipmentBase>> WithDetailsAsync()
{
    IQueryable<ShipmentBase> query = await base.WithDetailsAsync();
    var filterCtx = Get<CurrentFilterContextProvider>();

    return query
        .IncludeFilteredDocumentsIf(filterCtx,
            await AuthorizationService.IsGrantedAnyAsync(HubPermissions.Document.View))
        .IncludeInvoicesAndRevenuesIf(filterCtx,
            await AuthorizationService.IsGrantedAnyAsync(HubPermissions.Invoice.View));
}

How a read flows through the base

For both GetAsync(string id) and GetListAsync(input, force) the pipeline is:

  1. CheckPermissionAsync(ReadPermission) — no-op when the permission string is empty, otherwise IAuthorizationService.CheckAsync(permission).
  2. Wrap the result in the distributed application cache (IAppCache) via Cache.GetOrAddAsync(...), keyed on the input, tagged with typeof(TService).Name. force || !IsCacheEnabledAsync bypasses the cache. Cache durations are feature-driven (HubFeatures.Cache.*, defaults 15 min absolute / 2 min refresh).
  3. Build the query through WithListDetailsAsync()FilterAllQueryFiltersAsync (applies org/tenant filters via .FilterAll(serviceProvider) and OrderBy) → PaginationAsync, which applies the OData query (ApplyAllQueryApplier), counts, skip/takes, maps via ToDto, then builds facets and returns a PagedResultDtoWithFilters<TDto>.

Helper hooks on the base

HubEntityBaseService also exposes ValidateUserAccessToOrgUnitAsync, ValidateUserAccessToShipmentDataAsync, ValidateIsCurrentUserAsync, Get<T>() for service-locator access, InvalidateCacheAsync / WithCacheInvalidationAsync(...) for writes, and FormatDateIf(...) using UserPreferences.DateFormat.

Service registration, replacement and decoration

ABP's dependency injection conventions register app services automatically. This codebase uses several override patterns:

  • [ExposeServices(IncludeSelf = true)] on HubEntityBaseService (and ShipmentAppService) so the concrete type stays resolvable — the base re-resolves TService from DI inside its cache factory (sp.GetRequiredService<TService>()).
  • Service replacement via [Dependency(ReplaceServices = true)] + [ExposeServices(...)]: IdentityUserAppService replaces ABP's IIdentityUserAppService, and OrganizationUnitAppService replaces IOrganizationUnitAppService.
  • Decoration of an ABP service: ApiKeyPermissionAppService is marked [DisableConventionalRegistration] and registered as a decorator in CargonerdsApplicationModule:
// src/Cargonerds.Application/CargonerdsApplicationModule.cs
context.Services.Decorate<IPermissionAppService, ApiKeyPermissionAppService>();

Common DTO patterns

ABP paging request/result types are used throughout (see DTOs):

public class PagedAndSortedResultRequestDto { /* SkipCount, MaxResultCount, Sorting */ }
public class PagedResultDto<T> { /* TotalCount, Items */ }

Hub list endpoints extend the result envelope with faceting and OData metadata:

// modules/hub/src/Hub.Application.Contracts/Dtos/PagedResultDtoWithFilters.cs
public class PagedResultDtoWithFilters<T> : PagedResultDto<T>, IFacetResponse
{
    public List<FilterGroupDto> Filters { get; set; } = new();   // filter groups for the UI
    public List<AppliedFilterDto> Applied { get; set; } = new(); // active filters as chips
    public string? ODataFilter { get; set; }                     // combined OData filter
    public QueryMetaDto Query { get; set; } = new();             // $orderby/$top/$skip metadata
}

Request DTOs extend the ABP paging DTOs with facet options (PagedAndSortedResultRequestWithFacetOptionsDto, FilterablePagedAndSortedResultRequestDto, ShipmentPageRequestDto). See DTOs for the generated-DTO and Mapperly mapping story.

Authorization

Permissions are declared as constant trees in HubPermissions, CargonerdsPermissions and PricingPermissions (in the matching *.Application.Contracts projects) and enforced two ways, both present in this codebase:

  • Declarative [Authorize(...)] on classes/methods (e.g. ExcelExportAppService, NotificationAppService, the Pricing services).
  • Imperative via the base CheckPermissionAsync / AuthorizationService.IsGrantedAnyAsync inside HubEntityBaseService-derived services.
[Authorize(HubPermissions.Shipment.Edit)]
public Task<TagDto> CreateAsync(CreateUpdateTagDto input) { /* ... */ }

Grants are attribute-driven and hierarchical

Permission constants carry a custom [GrantInRoles(...)] attribute that Cargonerds.Domain/RolesDataSeedContributor.cs reads reflectively to auto-grant permissions to roles at seed time. With Inherit = true (the default) a grant walks up RoleConsts.Hierarchy (DefaultCustomer → Operator → AccountOwner → LocalRealtimeAdmin → SuperUser), so a permission granted to a lower role is also granted to every role above it. Adding a permission constant without [GrantInRoles] means no role receives it by seeding. See Authorization.

Auto-API and remote services

Service interfaces are conventional ABP (ICrudAppService<…>, IReadOnlyAppService<…>) and feed ABP's auto-API controllers and dynamic C# client proxies. Each module names its remote service — e.g. HubRemoteServiceConsts declares RemoteServiceName = "Hub", ModuleName = "hub". FluentValidation validators in *.Application.Contracts/Validators/ are auto-applied to incoming DTOs via AbpFluentValidationModule.

HubEntityBaseService exposes only the two-argument list/get methods

On the base, GetAsync(Guid) and the single-argument GetListAsync(input) are marked [RemoteService(IsEnabled = false)]. The live endpoints are the string-keyed GetAsync(string id) and the two-argument GetListAsync(input, force). Because reads are cached by service-type tag, writes must call InvalidateCacheAsync / WithCacheInvalidationAsync or stale data (up to the configured TTL) is served. See Caching.

Service lifecycle

graph TD
    A[HTTP Request] --> B[Auto-API Controller]
    B --> C[Application Service]
    C --> D[Authorization Check]
    D --> E[DTO Validation]
    E --> F[Begin Unit of Work]
    F --> G{Cache hit?}
    G -- yes --> K[Return cached DTO]
    G -- no --> H[FilterAll org/tenant + OData query]
    H --> I[Repository / Domain Logic]
    I --> J[Map to DTO via ToDto / Mapperly]
    J --> L[Build facets + cache result]
    L --> M[Commit Unit of Work]
    M --> N[Return Response]

Next steps

Per-service deep dives:

Cross-cutting implementation detail: