Skip to main content

build-crud-application-services

Builds or reviews a complete ABP CRUD application-service contract and implementation with the CrudAppService APIs. USE FOR: choosing ICrudAppService/CrudAppService generic overloads, separating get/list/create/update DTOs, wiring IRepository, object mappings and five policy properties, adding list filters without breaking count/sort/page order, and validating all five CRUD operations. DO NOT USE FOR: a non-CRUD use case or general ApplicationService orchestration (use application-services); composite or non-standard keys with AbstractKeyCrudAppService (use application-services); mapping-provider configuration details (use map-objects-and-dtos); permission definitions (use permissions-and-authorization); optimistic concurrency (use handle-optimistic-concurrency); HTTP endpoint conventions (use expose-http-apis); integration-test infrastructure (use test-abp-applications).

Jump to install

Source facts

Repository
abpframework/abp-skills
Last source activity
July 20, 2026 at 03:24
Detected SKILL.md language
English
Stars
18
Forks
0

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
name
build-crud-application-services
description
Builds or reviews a complete ABP CRUD application-service contract and implementation with the CrudAppService APIs. USE FOR: choosing ICrudAppService/CrudAppService generic overloads, separating get/list/create/update DTOs, wiring IRepository, object mappings and five policy properties, adding list filters without breaking count/sort/page order, and validating all five CRUD operations. DO NOT USE FOR: a non-CRUD use case or general ApplicationService orchestration (use application-services); composite or non-standard keys with AbstractKeyCrudAppService (use application-services); mapping-provider configuration details (use map-objects-and-dtos); permission definitions (use permissions-and-authorization); optimistic concurrency (use handle-optimistic-concurrency); HTTP endpoint conventions (use expose-http-apis); integration-test infrastructure (use test-abp-applications).
license
MIT
# Build ABP CRUD Application Services Use the framework CRUD base only when the use case really has the standard get, list, create, update, and delete shape. ## When to Use - Implement a standard keyed entity CRUD contract in `*.Application.Contracts` and its service in `*.Application`. - Select the shortest generic overload that still preserves distinct public DTO contracts. - Add filtering while retaining the framework's count, sorting, paging, mapping, authorization, and tenant behavior. - Review an existing `CrudAppService` for missing policies, mappings, pagination, or operation coverage. ## When Not to Use - **A use case with commands or workflows beyond CRUD** — use **application-services** and explicit methods. - **An entity that cannot use `IEntity<TKey>` key lookup** — use **application-services** for `AbstractKeyCrudAppService` and implement key resolution. - **Mapping-provider setup or profile troubleshooting** — use **map-objects-and-dtos**. - **Permission definition and grant configuration** — use **permissions-and-authorization**; this skill only connects policy names to CRUD operations. - **Concurrency-token design** — use **handle-optimistic-concurrency**. - **Conventional HTTP controllers or routes** — use **expose-http-apis**. - **Test host, database, current user, or current tenant setup** — use **test-abp-applications**. ## How it works ### Contract shape The widest contract is: ```csharp ICrudAppService< TGetOutputDto, TGetListOutputDto, TKey, TGetListInput, TCreateInput, TUpdateInput> ``` It combines these five methods: ```csharp Task<TGetOutputDto> GetAsync(TKey id); Task<PagedResultDto<TGetListOutputDto>> GetListAsync(TGetListInput input); Task<TGetOutputDto> CreateAsync(TCreateInput input); Task<TGetOutputDto> UpdateAsync(TKey id, TUpdateInput input); Task DeleteAsync(TKey id); ``` The shorter overloads progressively reuse one DTO type for get, list, create, and update, and the shortest overload uses `PagedAndSortedResultRequestDto`. Prefer the widest overload when the public fields differ between detail, list, create, and update contracts. ABP's test application contains this shortest-overload implementation. The excerpt omits unrelated test endpoints: ```csharp public interface IPeopleAppService : ICrudAppService<PersonDto, Guid> { // Other test endpoints omitted. } public class PeopleAppService : CrudAppService<Person, PersonDto, Guid>, IPeopleAppService { private readonly IHttpContextAccessor _httpContextAccessor; public PeopleAppService( IRepository<Person, Guid> repository, IHttpContextAccessor httpContextAccessor) : base(repository) { _httpContextAccessor = httpContextAccessor; } // Other test endpoints omitted. } ``` The extra HTTP-context dependency belongs to that test application, not to `CrudAppService`; the base constructor accepts `IRepository<TEntity, TKey>`. ### Implementation shape For separate DTO contracts, derive from the widest base: ```csharp CrudAppService< TEntity, TGetOutputDto, TGetListOutputDto, TKey, TGetListInput, TCreateInput, TUpdateInput> ``` `TEntity` must be a class implementing `IEntity<TKey>`. The base receives `IRepository<TEntity, TKey>` and supplies standard key lookup and deletion. Register mappings for every default mapping path used by the selected generic arguments: - `TEntity` to `TGetOutputDto`. - `TEntity` to `TGetListOutputDto`. - `TCreateInput` to `TEntity`. - `TUpdateInput` to the existing `TEntity` instance. Use **map-objects-and-dtos** to configure the selected object-mapping provider. Override the async mapping methods when entity creation or mutation must go through constructors, aggregate methods, or a domain service. Both are `protected virtual` with default (object-mapper-backed) bodies, so override the one(s) you need: - `Task<TEntity> MapToEntityAsync(TCreateInput createInput)` — build the new entity (e.g. via its constructor or a factory). A custom create override therefore owns key assignment. - `Task MapToEntityAsync(TUpdateInput updateInput, TEntity entity)` — apply the changes onto the loaded entity (e.g. through aggregate methods). A custom update override owns any input-ID normalization. The async overrides take precedence over the default synchronous mapping methods. ### Operation pipeline - `GetAsync` checks `GetPolicyName`, loads by key, then maps to `TGetOutputDto`. - `GetListAsync` checks `GetListPolicyName`, creates the filtered query, counts it, applies sorting, applies paging, executes it, then maps the page to `TGetListOutputDto`. - `CreateAsync` checks `CreatePolicyName`, maps a new entity, sets framework-managed values described below, inserts with `autoSave: true`, then maps the result. - `UpdateAsync` checks `UpdatePolicyName`, loads the entity, maps onto that existing instance, updates with `autoSave: true`, then maps the result. - `DeleteAsync` checks `DeletePolicyName`, then deletes by key. Set all five protected policy-name properties in the derived service. `CheckPolicyAsync` returns without checking authorization when a property is null or empty; leaving a policy property unset does not deny access. ### Framework-managed create and update values - On the default create-mapping path, an empty (`Guid.Empty`) key on an `IEntity<Guid>` is assigned through `GuidGenerator` — but only when the `Id` property is settable and not marked `[DisableIdGeneration]`. A non-empty client-supplied ID is left as-is (only `Guid.Empty` is replaced). - After create mapping, an `IMultiTenant` entity gets `CurrentTenant.Id` when the current tenant has a value and the entity exposes a settable `TenantId` property. - Before the default update mapping, if `TUpdateInput` implements `IEntityDto<TKey>`, its `Id` is replaced with the loaded entity's ID. Do not depend on a client-supplied ID or tenant ID when these framework paths apply. ### Filtering, sorting, and paging Override `CreateFilteredQueryAsync` to apply filters only: ```csharp protected override async Task<IQueryable<TEntity>> CreateFilteredQueryAsync( TGetListInput input) { var query = await base.CreateFilteredQueryAsync(input); // Apply filters only. Sorting and paging run after the total count query. return query; } ``` Do not sort or page in this method. The base counts the filtered query before calling `ApplySorting` and `ApplyPaging`. Sorting is applied only when the input implements `ISortedResultRequest`. A non-empty `Sorting` value is passed to dynamic LINQ `OrderBy`. When a limited result is requested without explicit sorting, `CrudAppService` orders descending by `CreationTime` for `IHasCreationTime` entities and otherwise by `Id`. Paging is applied when the input implements `IPagedResultRequest`; otherwise a plain `ILimitedResultRequest` receives `Take(MaxResultCount)`. `PagedAndSortedResultRequestDto` supplies both paging and sorting. `LimitedResultRequestDto.MaxResultCount` defaults to 10 and validates against a maximum of 1,000. ## Workflow 1. Define the detail output, list output, list input, create input, and update input in `*.Application.Contracts`. 2. Select the matching `ICrudAppService` overload; use the widest overload unless two contract roles intentionally share the same shape. 3. Derive the implementation from the matching `CrudAppService` overload and pass `IRepository<TEntity, TKey>` to the base constructor. 4. Register all default mapping pairs, or override the async mapping methods for domain-controlled construction and mutation. 5. Assign `GetPolicyName`, `GetListPolicyName`, `CreatePolicyName`, `UpdatePolicyName`, and `DeletePolicyName` to defined permission names. 6. Override `CreateFilteredQueryAsync` for filters; leave sorting and paging to their dedicated base methods. 7. Validate get, filtered/paged list, create, update, delete, permission denial, mapping, and tenant behavior in integration tests. ## Validation - Build the contracts and application projects so every selected generic argument and mapping override compiles. - Resolve the interface from DI and execute all five methods against the integration-test database. - Verify list `TotalCount` represents the filtered query before paging, while `Items` contains only the requested page. - Verify each operation is denied when its assigned policy is not granted; an unset policy property is a configuration failure, not a denial test. - For a tenant entity, create under a non-null `CurrentTenant.Id` and verify the persisted tenant ID comes from the current tenant. - When using the default create mapping for a `Guid` entity with an empty ID (and a settable `Id` without `[DisableIdGeneration]`), verify the returned DTO has a non-empty ID; with a non-empty input ID, verify it is kept unchanged. - If update input implements `IEntityDto<TKey>`, test with a mismatched input ID and verify the method ID remains authoritative. - Use **test-abp-applications** for the test host and **handle-optimistic-concurrency** for stale-update and stale-delete cases. ## Common Pitfalls - **Leaving policy properties null.** The base treats null or empty as "no policy check," not "deny by default." - **Using one DTO for every operation by habit.** Short overloads deliberately reuse types; choose the widest overload when writable fields differ from returned fields. - **Missing one mapping direction.** Get/list map from the entity, create maps to a new entity, and update maps onto the loaded entity. - **Paging inside `CreateFilteredQueryAsync`.** It makes `TotalCount` describe the page instead of the full filtered set and is then paged again by the base. - **Using a custom list input without paging interfaces.** Sorting and paging are interface-driven; unrelated properties with similar names do not activate them. - **Trusting client IDs or tenant IDs during create/update.** The base can generate an empty `Guid`, replace an update DTO ID, and assign the current tenant ID. - **Overriding async mapping and expecting default key handling to continue.** The async override has priority; custom creation must assign the key, and custom update mapping must preserve the loaded entity's key. - **Using `CrudAppService` for a non-standard key.** It requires `IEntity<TKey>` and standard repository key lookup; use the `AbstractKeyCrudAppService` path in **application-services** instead. - **Treating CRUD as a substitute for domain behavior.** Override mapping or explicit methods when construction and mutation must enforce aggregate invariants.
View on GitHub