Avanade

coreex-app-service

"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)."

Avanade 28 8 Updated 1w ago

Resources

1
GitHub

Install

npx skillscat add avanade/coreex/coreex-app-service

Install via the SkillsCat registry.

SKILL.md

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-repository skill
  • Validators — use the coreex-validator skill
  • 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.

  1. Exception-based or Result<T> pipeline style? (→ Path A or B — per-project choice)
  2. Which operations? Get / Create / Update / Delete / custom business action? (never assume Query)
  3. Any cross-domain or external-service calls? (→ adapter interface, Path D)
  4. Any policy guard checks requiring I/O? (→ policy class, Path D)
  5. Domain layer present? (→ Application/Mapping/ mapper; used in Path B)
  6. Read-only queries or collection results needed? (→ CQRS read service, Path C)

Key rules at a glance:

  • [ScopedService<IInterface>] on every service — auto-registers via AddDynamicServicesUsing<T>()
  • Only inject: repository, unit of work, adapter interfaces, logger — never validators, mappers, policies
  • ValidateAndThrowAsync (exception style) / ValidateWithResultAsync (Result<T>) — never bare ValidateAsync
  • Service assigns Id after 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 EventAction before reaching for CreateEventWith(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 = default as the last parameter; pass through to every async call and TransactionAsync(async ct => ..., cancellationToken)
  • CQRS: mutations + GetAsync → {Name}Service; queries + GetAsync → {Name}ReadService (both have GetAsync)
  • Before building Path C: confirm I{Name}Repository already has QueryAsync/QuerySchemaAsync backed by a {Name}QueryArgsConfig. If not, stop and invoke coreex-repository first — never add filtering/ordering logic or a hand-rolled query in the service to work around a missing repository method
  • Always .ConfigureAwait(false) on every await
  • Result<T> failure propagation: prefer a Then* chain; for a guard clause return r; (same type, or Result → Result<U>) or return r.AsResult(); (Result<T> → Result<U>) — never Result<X>.Fail(r.Error!) / return r.Error!;, and don't touch .Error in service code
  • A Domain value object persisted via a JSON column (e.g. Basket.ShippingAddress) is mapped with a BiDirectionMapper<TDomain, TContract, TSelf> in Application/Mapping/ (not the uni-directional Mapper<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

Categories