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 ofIReadOnlyAppService. - The closest thing to a plain ABP CRUD service is
TagAppService— it implementsICrudAppService(viaHubEntityBaseService) with realCreate/Update/Deleteand[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<TDto>"]
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
- Permission check —
await CheckPermissionAsync(ReadPermission).ReadPermissionis aprotected virtual string(empty by default, so the check no-ops); override it per service. For exampleShipmentAppService.ReadPermission => HubPermissions.Shipment.View. A non-empty value runsIAuthorizationService.CheckAsync(permission). - Caching — the result is wrapped in
Cache.GetOrAddAsync(...)(IAppCache), keyed on the input (AddRequestQueryAsKey = true) and tagged withtypeof(TService).Name. Caching is bypassed whenforceistrueor theHubFeatures.Cache.Enabledfeature is off. Cache options (AbsoluteExpirationRelativeToNow,RefreshInBackground,RefreshAfter) are read fromHubFeatures.Cache.*(see Features). - Core query (
GetListCoreAsync→FilterAllQueryFiltersAsync→PaginationAsync):WithListDetailsAsync()produces the base queryable,FilterAll(serviceProvider)applies org/tenant and soft-delete filters and addsOrderBy, thenApplyAllQueryApplier(serviceProvider)applies the OData query options from the current request, counts, appliesSkip/Take, maps viaToDto, and finallyReadFacetsForAsyncbuilds the facet groups. The result is aPagedResultDtoWithFilters<TDto>(see DTOs → Result envelopes). ToDto— each concrete service implementsprotected 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
Includes —WithDetailsAsync()conditionally includes documents and invoices based onAuthorizationService.IsGrantedAnyAsync(HubPermissions.Document.View / Invoice.View), so a user only loads data they may see. - Org-unit visibility —
IsAvailableInOrgUnit(...)filters a single shipment againstCurrentFilterContextProvider.ActiveOrgIds/ActiveOrgUnitIds. - Custom request DTO —
ShipmentPageRequestDtoextends the faceted paging DTO withQuickFilter,SubscribedOnly, andTagIds, andGetListCoreAsyncis overridden to honor them. - Domain operations beyond CRUD —
CreateBookingAsync,SendBookingToCw1Async,DeactivateBookingAsync,GetTracking,GetUpcomingShipments,GetRecentStatusChanges, etc., each cached and most explicitly[Authorize(...)]-attributed (ShipmentAppServicecarries ~11[Authorize]attributes). - Stubbed generic CRUD — because the base is read-only, the
ICrudAppServicewrite members are explicitlythrow 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, PricingQuotationAppService). - Imperative — the base-class
CheckPermissionAsync(ReadPermission)andAuthorizationService.IsGrantedAnyAsync(...)insideHubEntityBaseService-derived services (used for the permission-gatedIncludes 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 usesPricingRemoteServiceConsts, 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.SendBookingToCw1Asyncis[HttpPost]). - First
Guidparameter 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
GetAsyncis the live one.GetAsync(Guid)and the single-argumentGetListAsync(input)are[RemoteService(IsEnabled = false)]; the exposed endpoints areGetAsync(string id)andGetListAsync(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.
HubEntityBaseServiceprovides no mutating operations; services that implementICrudAppServicestubCreateAsync/UpdateAsync/DeleteAsyncwithNotImplementedExceptionand 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
*.Contractsproject.
See also¶
- Service Patterns — primary-constructor DI,
ToDto()mapping, tracking, org isolation - DTOs —
PagedAndSortedResultRequestWithFacetOptionsDto,ShipmentPageRequestDto,PagedResultDtoWithFilters<T> - Authorization — permission constants,
[GrantInRoles]seeding, cross-module groups - Services Overview · Shipment Service · Organization Service
- Architecture Overview · Layered Architecture · ABP Patterns