"Create or modify a CoreEx Application-layer service. USE FOR: new service class (exception-based or Result<T>), adding CRUD/business operations, CQRS read service (XxxReadService), adapter interface in Application/Adapters/, policy class in Application/Policies/, application-level mapper (Domain → Contract). DO NOT USE FOR: Infrastructure repositories (use coreex-repository), validators (use coreex-validator), controller endpoints (use coreex-api)."
Resources
1Install
npx skillscat add avanade/coreex/coreex-app-service Install via the SkillsCat registry.
CoreEx: Application Service
Guides you through creating or modifying a CoreEx Application-layer service in Application/. Covers CRUD operations, business actions, unit-of-work + events, CQRS read services, adapters, and policies.
When to Use
- New service class for an entity (scaffold interface + implementation)
- Adding a mutation operation (create / update / delete / custom business action)
- Adding a CQRS read service (
{Name}ReadService) for queries and read-model shapes - Adding an adapter interface (
Application/Adapters/) for a cross-domain or external-service call - Adding a policy class (
Application/Policies/) for guard logic that requires I/O - Switching a method between exception-based and
Result<T>pipeline styles
When Not to Use
- Infrastructure repositories — use the
coreex-repositoryskill - Validators — use the
coreex-validatorskill - Controller endpoints — use
coreex-api - Domain aggregates, entities, value objects — those belong in the Domain layer
Quick Reference
Clarifying questions before writing any code:
0. Resolve rop-enabled (exception vs Result<T>), whether a *.Domain project exists (aggregate mapping vs direct-CRUD), and outbox-enabled (event publishing) from the solution-root AGENTS.md Feature Configuration and project structure before asking the rest; only prompt for what is unresolved and re-state resolved values for confirmation.
- Exception-based or
Result<T>pipeline style? (→ Path A or B — per-project choice) - Which operations? Get / Create / Update / Delete / custom business action? (never assume Query)
- Any cross-domain or external-service calls? (→ adapter interface, Path D)
- Any policy guard checks requiring I/O? (→ policy class, Path D)
- Domain layer present? (→
Application/Mapping/mapper; used in Path B) - Read-only queries or collection results needed? (→ CQRS read service, Path C)
Key rules at a glance:
[ScopedService<IInterface>]on every service — auto-registers viaAddDynamicServicesUsing<T>()- Only inject: repository, unit of work, adapter interfaces, logger — never validators, mappers, policies
ValidateAndThrowAsync(exception style) /ValidateWithResultAsync(Result<T>) — never bareValidateAsync- Service assigns
Idafter validation:value.Id = Runtime.NewId()(string key) /Runtime.NewGuid()(Guid) - All mutations in
_unitOfWork.TransactionAsync(...)— event added inside, never outside WhereMutated(v => ...)for Create/Update (DataResult<T>carries value);WhereMutated(() => ...)for Delete- Delete event:
EventData.CreateEvent<T>(EventAction.Deleted).WithKey(id)— no value body - Check
EventActionbefore reaching forCreateEventWith(v, "string")— most business actions (Confirmed,Cancelled,Started,Completed,Suspended,Closed,Expired, etc.) already have an enum member; the raw-string overload is only for genuinely novel actions NotFoundException.ThrowIfDefault(entity)after any Get that must find the entity- Validators / Mappers / Policies: not DI-registered — call or instantiate at point of use
Validator<T, TSelf>: call via singleton —{Name}Validator.Default.ValidateAndThrowAsync(...)Validator<T>(with injection): instantiate at call site —new {Name}Validator(_dep).ValidateAndThrowAsync(...)- All interface methods include
CancellationToken cancellationToken = defaultas the last parameter; pass through to every async call andTransactionAsync(async ct => ..., cancellationToken) - CQRS: mutations +
GetAsync→{Name}Service; queries +GetAsync→{Name}ReadService(both haveGetAsync) - Before building Path C: confirm
I{Name}Repositoryalready hasQueryAsync/QuerySchemaAsyncbacked by a{Name}QueryArgsConfig. If not, stop and invokecoreex-repositoryfirst — never add filtering/ordering logic or a hand-rolled query in the service to work around a missing repository method - Always
.ConfigureAwait(false)on everyawait Result<T>failure propagation: prefer aThen*chain; for a guard clausereturn r;(same type, orResult→Result<U>) orreturn r.AsResult();(Result<T>→Result<U>) — neverResult<X>.Fail(r.Error!)/return r.Error!;, and don't touch.Errorin service code- A Domain value object persisted via a JSON column (e.g.
Basket.ShippingAddress) is mapped with aBiDirectionMapper<TDomain, TContract, TSelf>inApplication/Mapping/(not the uni-directionalMapper<TSource,TDest,TSelf>used for the root aggregate) — see `coreex-application-services.instructions.md#json-backed-value-object-mapping`
For full workflow and code examples see `references/workflow.md`.
Key References
- `/.github/instructions/coreex-application-services.instructions.md` — full conventions: guard clauses, events, CQRS, adapters, policies, Result<T> operators
- Related skills: `coreex-repository` (persistence the service injects), `coreex-validator` (invoked before mutations), `coreex-policy` (I/O guard logic), `coreex-adapter` (cross-domain calls), `coreex-contract` (the entity/request types), `coreex-aggregate` (Domain layer + mapping), `coreex-api` (consumes the service), `coreex-test-api` (tests the service through the host)
- Application layer deep-dive — optional (after
/coreex-docs-sync) - Illustrative examples (CoreEx sample — not present in your project):
- `ProductService` + `ProductReadService` — exception-based CRUD + business actions, CQRS read
- `BasketService` + `BasketReadService` — Result<T> + adapter + policy
- `ProductPolicy` — policy example
- `IProductAdapter` — adapter interface example