Skip to content

Application Services

Application services are the entry points to use cases. They orchestrate repositories and domain logic, accept and return DTOs (never raw entities), enforce authorization and validation, run inside an ABP unit of work, and are automatically published as REST endpoints by ABP's auto-API controllers.

This page covers the base-class hierarchy, the CRUD / read-only service contracts, how those classes are wired and exposed over HTTP, and the project-specific conventions you must know before adding a service. Cross-cutting topics live on sibling pages — recurring code patterns in Service Patterns, the DTO/paging contracts in DTOs, and permission definitions in Authorization.

TL;DR for this codebase

  • Three application layers stacked by module dependency: Hub (the bulk of the domain), Pricing (depends on Hub), and the Cargonerds host (thin glue + ABP overrides).
  • Most entity reads do not use ABP's CrudAppService. They use a bespoke base, HubEntityBaseService, that layers caching, OData-style filtering, faceting, and org-unit/permission checks on top of IReadOnlyAppService.
  • The closest thing to a plain ABP CRUD service is TagAppService — it implements ICrudAppService (via HubEntityBaseService) with real Create/Update/Delete and [Authorize] per method.
  • Auto-API is enabled for exactly three assemblies; verb and route are derived by convention (see Auto-API exposure).

Where they live

Project Namespace root Examples
src/Cargonerds.Application Cargonerds.* IdentityUserAppService, ApiKeyPermissionAppService, OrganizationUnitAppService, ClientSettingsAppService
modules/hub/src/Hub.Application/Services Hub.Services ShipmentAppService, HubOrganizationAppService, OrderAppService, PurchaseOrderAppService, TagAppService, ExcelExportAppService
modules/pricing/src/Pricing.Application Pricing.* QuotationAppService, OfferAppService

Service interfaces live in the matching *.Application.Contracts project — e.g. modules/hub/src/Hub.Application.Contracts/Services/IShipmentAppService.cs and src/Cargonerds.Application.Contracts/ApiKeys/IApiKeyAppService.cs. There are roughly 35 *AppService classes across the three *.Application projects.

Module dependency direction

PricingApplicationModule [DependsOn] HubApplicationModule, and CargonerdsApplicationModule (src/Cargonerds.Application/CargonerdsApplicationModule.cs) [DependsOn] both Pricing and Hub (plus a long list of ABP Pro modules). That is why Pricing can reuse Hub's base service and mappers, and why the host can override services from any module. See the module system docs.

Base-class hierarchy

classDiagram
    class ApplicationService {
        +ObjectMapper
        +CurrentUser
        +CurrentTenant
        +Logger
        +L (localization)
    }
    class HubAppService {
        LocalizationResource = HubResource
        ObjectMapperContext = HubApplicationModule
    }
    class CargonerdsAppService {
        LocalizationResource = CargonerdsResource
    }
    class HubEntityBaseService~TEntity,TDto,TListParamDto,TService~ {
        +GetAsync(string id)
        +GetListAsync(input, force)
        #ToDto(entity)*
        #ReadPermission
        #WithDetailsAsync()
    }
    class ShipmentAppService
    class HubOrganizationAppService
    class QuotationAppService
    class TagAppService

    ApplicationService <|-- HubAppService
    ApplicationService <|-- CargonerdsAppService
    HubAppService <|-- HubEntityBaseService~TEntity,TDto,TListParamDto,TService~
    HubAppService <|-- HubOrganizationAppService
    HubEntityBaseService~TEntity,TDto,TListParamDto,TService~ <|-- ShipmentAppService
    HubEntityBaseService~TEntity,TDto,TListParamDto,TService~ <|-- QuotationAppService
    HubEntityBaseService~TEntity,TDto,TListParamDto,TService~ <|-- TagAppService

ApplicationService (ABP)

All services ultimately derive from ABP's Volo.Abp.Application.Services.ApplicationService, which provides ObjectMapper, CurrentUser, CurrentTenant, Logger, LazyServiceProvider, AuthorizationService, UnitOfWorkManager, and localization (L).

HubAppService and CargonerdsAppService

Each layer has a thin abstract base that pins the localization resource (and, for Hub, the AutoMapper context). Use these for services that are not entity-CRUD services.

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

Why ObjectMapperContext is set on Hub but not the core

ObjectMapperContext = typeof(HubApplicationModule) makes ObjectMapper resolve the module's AutoMapper maps. In practice the Hub layer maps almost everything with Mapperly ToDto() extension methods, so ObjectMapper is rarely used there (see the warning in the "Object mapping is split" warning under Gotchas).

HubOrganizationAppService is a representative non-CRUD Hub service — it derives from HubAppService, takes its repositories via a primary constructor, and maps with Mapperly extensions:

// modules/hub/src/Hub.Application/Services/HubOrganizationAppService.cs
[Authorize]
public class HubOrganizationAppService(
    IOrganizationRepository repository,
    IRepository<ExternalOrganizationIdentifier> externalIdRepository
) : HubAppService, IHubOrganizationAppService
{
    public async Task<OrganizationDto?> GetAsync(Guid id)
    {
        var res = await repository.FindByIdAsync(id);
        return res?.ToDto();
    }
    // GetByIdsAsync, GetSearchAsync, GetListAsync(...) ...
}

HubEntityBaseService<TEntity, TDto, TListParamDto, TService>

This is the central custom read/list engine for Hub entities — and the most important class on this page. It lives in modules/hub/src/Hub.Application/Services/HubEntityBaseService.cs, derives from HubAppService, and implements ABP's IReadOnlyAppService<TDto, Guid, TListParamDto>. It centralizes paging, OData-style filtering, faceting, second-level caching, org-unit filtering, and read-permission checks so that concrete services stay tiny.

// 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);          // the only required override

    protected virtual string ReadPermission => string.Empty;  // override to gate reads
    protected virtual Task<IQueryable<TEntity>> WithDetailsAsync() => Repository.WithDetailsAsync();
    protected virtual Task<IQueryable<TEntity>> WithListDetailsAsync() => Repository.GetQueryableAsync();
    protected virtual string DefaultOrder() => $"{nameof(HubBaseGuidEntity.LastModificationTime)} DESC";
    // ...
}

A three-generic convenience overload defaults TListParamDto to PagedAndSortedResultRequestWithFacetOptionsDto, so services that don't need a custom request DTO can write HubEntityBaseService<TEntity, TDto, TService>:

// same file — the 3-generic shorthand
public abstract class HubEntityBaseService<TEntity, TDto, TService>(
    IRepository<TEntity, Guid> repository,
    IServiceProvider serviceProvider
) : HubEntityBaseService<TEntity, TDto, PagedAndSortedResultRequestWithFacetOptionsDto, TService>(
        repository, serviceProvider) { ... }

The self-referential TService type parameter is what lets the base resolve its own concrete type from DI (sp.GetRequiredService<TService>()) inside cache factories — which is exactly why the class is annotated [ExposeServices(IncludeSelf = true)].

The override surface for a new entity service

To add a read/list service for a Hub entity you typically only implement ToDto, set ReadPermission, and optionally override WithDetailsAsync (for Includes) and GetListCoreAsync (for custom query shaping). Caching, paging, facets, and org filtering come for free. See Service Patterns → the read/list engine.

How HubEntityBaseService reads and lists

GetAsync(string id) and the 2-argument GetListAsync(input, force) are the public entry points. Both follow the same shape: check the read permission, then serve through the cache, whose factory re-resolves the concrete service and runs the core query.

flowchart TD
    A["GetListAsync(input, force)"] --> B["CheckPermissionAsync(ReadPermission)"]
    B --> C{"cache hit?<br/>(force or feature off bypasses)"}
    C -->|hit| Z["return PagedResultDtoWithFilters&lt;TDto&gt;"]
    C -->|miss| D["GetListCoreAsync"]
    D --> E["WithListDetailsAsync()"]
    E --> F["FilterAllQueryFiltersAsync<br/>.FilterAll(sp) org/tenant + OrderBy"]
    F --> G["PaginationAsync"]
    G --> H["ApplyAllQueryApplier(sp)<br/>OData $filter/$orderby/$top/$skip"]
    H --> I["Count + Skip/Take + ToDto"]
    I --> J["ReadFacetsForAsync<br/>build facet groups + applied chips"]
    J --> Z
  1. Permission checkawait CheckPermissionAsync(ReadPermission). ReadPermission is a protected virtual string (empty by default, so the check no-ops); override it per service. For example ShipmentAppService.ReadPermission => HubPermissions.Shipment.View. A non-empty value runs IAuthorizationService.CheckAsync(permission).
  2. Caching — the result is wrapped in Cache.GetOrAddAsync(...) (IAppCache), keyed on the input (AddRequestQueryAsKey = true) and tagged with typeof(TService).Name. Caching is bypassed when force is true or the HubFeatures.Cache.Enabled feature is off. Cache options (AbsoluteExpirationRelativeToNow, RefreshInBackground, RefreshAfter) are read from HubFeatures.Cache.* (see Features).
  3. Core query (GetListCoreAsyncFilterAllQueryFiltersAsyncPaginationAsync): WithListDetailsAsync() produces the base queryable, FilterAll(serviceProvider) applies org/tenant and soft-delete filters and adds OrderBy, then ApplyAllQueryApplier(serviceProvider) applies the OData query options from the current request, counts, applies Skip/Take, maps via ToDto, and finally ReadFacetsForAsync builds the facet groups. The result is a PagedResultDtoWithFilters<TDto> (see DTOs → Result envelopes).
  4. ToDto — each concrete service implements protected abstract TDto? ToDto(TEntity?), typically delegating to a Mapperly extension (entity?.ToCombinedShipmentDto()).

Cache invalidation helpers are built in: InvalidateCacheAsync() / InvalidateCacheAsync<T>() (invalidate by tag = service type name) and WithCacheInvalidationAsync(...) (run a write then invalidate). Org-unit guards are also provided: ValidateUserAccessToOrgUnitAsync, ValidateUserAccessToShipmentDataAsync, ValidateIsCurrentUserAsync.

HubEntityBaseService is not ABP CrudAppService

Reads/lists go through the bespoke pipeline above, and the framework Guid/single-arg overloads are hidden from the HTTP surface:

Member Exposed over HTTP? Notes
GetAsync(string id) the live read endpoint
GetAsync(Guid id) [RemoteService(IsEnabled = false)] delegates to the string overload
GetListAsync(input, force) the live list endpoint (returns PagedResultDtoWithFilters<T>)
GetListAsync(input) [RemoteService(IsEnabled = false)] calls GetListAsync(input, false)

Because reads are cached (tag = service type name), writes must call InvalidateCacheAsync / WithCacheInvalidationAsync or callers will see stale data.

The closest thing to a CRUD service: TagAppService

The solution no longer ships a vanilla ABP CRUD sample. The closest live example is TagAppService (modules/hub/src/Hub.Application/Services/TagAppService.cs): its interface derives from ICrudAppService, so it exposes the full Create/Update/Delete shape — but even here the class extends HubEntityBaseService (for caching and visibility filtering) rather than ABP's CrudAppService, mapping runs through Mapperly extension methods (.ToDto(), .ToNet()) instead of ObjectMapper, and every write carries its own [Authorize] and invalidates the read cache.

// modules/hub/src/Hub.Application/Services/TagAppService.cs
[ExposeServices(IncludeSelf = true)]
public class TagAppService(
    IRepository<Tag, Guid> tagRepository,
    /* ... org-unit/validation collaborators ... */
    IServiceProvider serviceProvider
) : HubEntityBaseService<Tag, TagDto, TagAppService>(tagRepository, serviceProvider), ITagAppService
{
    [Authorize(HubPermissions.Shipment.Edit)]
    public Task<TagDto> CreateAsync(CreateUpdateTagDto input) =>
        WithCacheInvalidationAsync(async () =>
        {
            var dto = await EnsureOrganizationAsync(input);
            var entity = dto.ToNet();
            AssignOwnership(entity, dto.OrganizationUnitId);
            return (await Repository.InsertAsync(entity)).ToDto();
        });

    [Authorize(HubPermissions.Shipment.Edit)]
    public Task<TagDto> UpdateAsync(Guid id, CreateUpdateTagDto input) { /* ... */ }

    [Authorize(HubPermissions.Shipment.Edit)]
    public Task DeleteAsync(Guid id) { /* ... */ }
}

The interface mixes ABP's read-only and CRUD contracts and adds domain-specific methods on top:

// modules/hub/src/Hub.Application.Contracts/Services/ITagAppService.cs
public interface ITagAppService
    : IApplicationService,
        IReadOnlyAppService<TagDto, Guid, PagedAndSortedResultRequestWithFacetOptionsDto>,
        ICrudAppService<TagDto, Guid, PagedAndSortedResultRequestWithFacetOptionsDto, CreateUpdateTagDto>
{
    Task<List<TagDto>> GetTagsByIdAsync(Guid shipmentId);
    // ... AddTagByIdAsync / RemoveTagByIdAsync / GetListAsync(input, force) ...
}

A rich entity service: ShipmentAppService

ShipmentAppService (modules/hub/src/Hub.Application/Services/ShipmentAppService.cs) is the richest example of the entity-base pattern. It extends HubEntityBaseService<ShipmentBase, CombinedShipmentDto, ShipmentPageRequestDto, ShipmentAppService> and implements IShipmentAppService.

// modules/hub/src/Hub.Application/Services/ShipmentAppService.cs
[UsedImplicitly]
[ExposeServices(IncludeSelf = true)]
public class ShipmentAppService(
    IShipmentBaseRepository repository,
    IGoogleService googleService,
    IServiceProvider serviceProvider,
    ICw1BookingService cw1BookingService,
    ICalculatedDataService calculatedDataService
)
    : HubEntityBaseService<ShipmentBase, CombinedShipmentDto, ShipmentPageRequestDto, ShipmentAppService>(
        repository, serviceProvider),
      IShipmentAppService
{
    protected override string ReadPermission => HubPermissions.Shipment.View;

    [return: NotNullIfNotNull("entity")]
    protected override CombinedShipmentDto? ToDto(ShipmentBase? entity) => entity?.ToCombinedShipmentDto();
}

It demonstrates several conventions worth copying:

  • Permission-gated IncludesWithDetailsAsync() conditionally includes documents and invoices based on AuthorizationService.IsGrantedAnyAsync(HubPermissions.Document.View / Invoice.View), so a user only loads data they may see.
  • Org-unit visibilityIsAvailableInOrgUnit(...) filters a single shipment against CurrentFilterContextProvider.ActiveOrgIds / ActiveOrgUnitIds.
  • Custom request DTOShipmentPageRequestDto extends the faceted paging DTO with QuickFilter, SubscribedOnly, and TagIds, and GetListCoreAsync is overridden to honor them.
  • Domain operations beyond CRUDCreateBookingAsync, SendBookingToCw1Async, DeactivateBookingAsync, GetTracking, GetUpcomingShipments, GetRecentStatusChanges, etc., each cached and most explicitly [Authorize(...)]-attributed (ShipmentAppService carries ~11 [Authorize] attributes).
  • Stubbed generic CRUD — because the base is read-only, the ICrudAppService write members are explicitly throw new NotImplementedException(); real writes go through the booking methods.

The interface keeps the framework CRUD shape while adding the bespoke list overload and domain methods:

// modules/hub/src/Hub.Application.Contracts/Services/IShipmentAppService.cs
public interface IShipmentAppService : ICrudAppService<CombinedShipmentDto, Guid, ShipmentPageRequestDto>
{
    Task<PagedResultDtoWithFilters<CombinedShipmentDto>> GetListAsync(ShipmentPageRequestDto input, bool force);
    Task<ShipmentActivityDto[]> GetLastActivityAsync(Guid shipmentId);
    Task<CombinedShipmentDto> SendBookingToCw1Async(Guid shipmentId);
    // ...
}

See the Shipment Service page for the full API.

Cross-module reuse: QuotationAppService

Pricing reuses the same base via the 3-generic shorthand — QuotationAppService : HubEntityBaseService<Quotation, QuotationDto, QuotationAppService> (modules/pricing/src/Pricing.Application/Quotation/QuotationAppService.cs) — which is only possible because PricingApplicationModule depends on HubApplicationModule.

Authorization styles

Both authorization styles are used and are equivalent in effect — pick whichever fits the method:

  • Declarative[Authorize(...)] / [Authorize(Policy = ...)] at class or method level (e.g. ShipmentAppService, ExcelExportAppService, Pricing QuotationAppService).
  • Imperative — the base-class CheckPermissionAsync(ReadPermission) and AuthorizationService.IsGrantedAnyAsync(...) inside HubEntityBaseService-derived services (used for the permission-gated Includes above).

Permission definitions (the constant trees, the custom [GrantInRoles] seeding convention, and cross-module group augmentation) are documented separately on the Authorization page. ABP's model is described in Authorization & permission management.

Service replacement and decoration

Because the host depends on every module, the core layer frequently replaces or decorates framework services using ABP's dependency injection features:

Technique Where Effect
[Dependency(ReplaceServices = true)] + [ExposeServices(...)] src/Cargonerds.Application/Services/IdentityUserAppService.cs Replaces ABP's IIdentityUserAppService with a Cargonerds implementation
[DisableConventionalRegistration] + context.Services.Decorate<...>() src/Cargonerds.Application/ApiKeys/ApiKeyPermissionAppService.cs, registered in CargonerdsApplicationModule Wraps IPermissionAppService to resolve permissions for API keys
[ExposeServices(IncludeSelf = true)] HubEntityBaseService and derived services Lets the concrete type be resolved from DI (needed for cache-factory re-entrancy via Get<TService>())
// src/Cargonerds.Application/CargonerdsApplicationModule.cs (ConfigureServices)
context.Services.Decorate<IPermissionAppService, ApiKeyPermissionAppService>();

Auto-API exposure

ABP turns application services into MVC controllers at startup for exactly three assemblies, configured in the HTTP host module:

// src/Cargonerds.HttpApi.Host/CargonerdsHttpApiHostModule.cs (ConfigureConventionalControllers)
Configure<AbpAspNetCoreMvcOptions>(options =>
{
    options.ConventionalControllers.Create(typeof(CargonerdsApplicationModule).Assembly);
    options.ConventionalControllers.Create(typeof(HubApplicationModule).Assembly);
    options.ConventionalControllers.Create(typeof(PricingApplicationModule).Assembly);
});

Every public application-service method becomes an endpoint. Routing and verbs are derived by convention (see the ABP auto-controller docs):

  • Route prefix comes from the module's remote-service constants. Hub uses HubRemoteServiceConsts (RemoteServiceName = "Hub", ModuleName = "hub") and Pricing uses PricingRemoteServiceConsts, so Hub endpoints land under /api/hub/..., Pricing under /api/pricing/..., and the host under ABP's default /api/app/....
  • HTTP verb is inferred from the method-name prefix: Get→GET, Create/Add/Insert/Post→POST, Update/Put→PUT, Delete/Remove→DELETE, anything else→POST. Methods can override this with an explicit [HttpGet]/[HttpPost] attribute (e.g. ShipmentAppService.SendBookingToCw1Async is [HttpPost]).
  • First Guid parameter becomes a path segment; otherwise simple parameters become query-string parameters.
  • Enums serialize as integers by default — nothing registers a global JsonStringEnumConverter; opt-in per property with [JsonConverter(typeof(JsonStringEnumConverter))] where needed.

The same interfaces also drive ABP's dynamic C# HTTP client proxies for the generated HttpApi.Client. The full HTTP surface — the OData v8 layer, API-key auth, Swagger, and the request pipeline — is documented on the architecture pages; this section only covers how application services become endpoints.

Validators don't fire automatically on auto-API controllers

The *.Application.Contracts modules pull in AbpFluentValidationModule, but ABP does not run FluentValidation interceptors on conventional (auto-API) controllers. The host registers a global FluentValidationActionFilter that reflectively resolves IValidator<T> per action argument and throws AbpValidationException. A validator only runs if it is registered in DI for the exact argument type. See DTOs for the validator inventory.

Gotchas

Object mapping is split

Two mapping engines coexist. Mapperly (DtoMapper, QuoteMapper, ApiKeyDtoMapper) is the real path — most domain↔DTO conversions are extension methods (entity.ToDto(), dto.ToNet()), not ObjectMapper. AutoMapper profiles still exist, but the only active CreateMap left is IdentityUser → IdentityUserDto (HubApplicationAutoMapperProfile). Don't assume ObjectMapper.Map<> works for an arbitrary domain type. ABP's mapping abstraction is documented under object-to-object mapping; the Mapperly conventions (reference handling, [MapDerivedType], ApplyUpdate) are covered in Service Patterns.

  • The string-keyed GetAsync is the live one. GetAsync(Guid) and the single-argument GetListAsync(input) are [RemoteService(IsEnabled = false)]; the exposed endpoints are GetAsync(string id) and GetListAsync(input, force).
  • Writes must invalidate the cache. Reads are cached per service (tag = service type name). After a write, call InvalidateCacheAsync / WithCacheInvalidationAsync, or callers get stale results.
  • Read-only base + stubbed CRUD. HubEntityBaseService provides no mutating operations; services that implement ICrudAppService stub CreateAsync/UpdateAsync/DeleteAsync with NotImplementedException and expose real writes as dedicated methods.
  • The contracts module is required for auto-API. Service interfaces feed both the auto-API controllers and the dynamic HTTP client proxies; keep the interface in the matching *.Contracts project.

See also