"Add or modify a DDD domain object in a CoreEx Domain layer. USE FOR: new aggregate root (Aggregate<TId,TSelf>), new child entity (Entity<TId,TSelf>) owned by an aggregate, new value object (sealed record), adding mutation methods, PersistenceState-aware factory methods, mutation guards (OnCheckCanMutate/OnMutate). Interviews the developer to determine which of the three (aggregate root / entity / value object) applies, then follows the appropriate pattern. DO NOT USE FOR: CRUD-oriented domains with no Domain layer (use coreex-app-service directly against repositories), Application-layer mappers between aggregate and contract (see coreex-application-services.instructions.md), Infrastructure persistence of aggregates (use coreex-repository)."
Resources
1Install
npx skillscat add avanade/coreex/coreex-aggregate Install via the SkillsCat registry.
CoreEx: Aggregate (DDD Domain Object)
Guides you through adding or modifying a domain object in a CoreEx Domain layer — aggregate roots,
child entities, and value objects. This skill is scoped to solutions where the *.Domain project exists
(added via dotnet new coreex-domain). It interviews you to determine which kind of domain object you need,
then follows the corresponding pattern from CoreEx.DomainDriven.
When to Use
- Add a new aggregate root — a consistency boundary with mutation guards and (optionally) integration events
- Add a new child entity — owned exclusively by an aggregate, with identity but no independent lifecycle
- Add a new value object — a concept with no identity, defined entirely by its values
- Add mutation methods, factory methods, or invariant guards to an existing domain object
- Decide whether a domain concept needs a Domain layer at all, or should stay CRUD-oriented
Mix and match within a domain. Once a
*.Domainproject exists, adopting it is not all-or-nothing across
every entity in that domain. Only give an entity the full aggregate/entity/value-object ceremony (mutation
guards,PersistenceState, factory methods) when it genuinely benefits — meaningful invariants, state
transitions, or integration events. A simple reference/lookup-style entity in the same domain can remain
CRUD-oriented (Application service → repository directly), even while a sibling aggregate in the same*.Domainproject uses the full pattern.
When Not to Use
- The domain is CRUD-oriented with no meaningful invariants to protect at the model level — let the
Application service orchestrate directly against repository interfaces; skip the Domain layer entirely - Application-layer mapping between aggregate and contract — see
.github/instructions/coreex-application-services.instructions.md - Infrastructure persistence of the aggregate (EF mapping,
PersistenceState-driven insert/update/delete) — usecoreex-repository - Validating primitive request DTOs before they reach the domain — use
coreex-validator
Confirm this skill applies before writing any domain object. It is gated on the presence of the
*.Domainproject (src/*.Domain/). If it is absent, the domain is CRUD-oriented — stop and usecoreex-app-serviceagainst repository interfaces instead. To add the Domain layer, rundotnet new coreex-domain -n Company.Product.Books.Domainand wire it into the solution first.
Quick Reference
- Aggregate root →
Aggregate<TId, TSelf>(extendsEntity<TId, TSelf>) — addsEvents/AddEvent/ClearEventsfor integration-event accumulation - Child entity →
Entity<TId, TSelf>— identity-based equality (Idonly), no independent construction path from outside the aggregate - Value object →
sealed record— structural equality,init-only properties, invariants enforced in the initialiser - Two factory methods only:
CreateNew(...)(.AsNew()) andCreateFrom(...)(.AsNotModified()) — constructor isprivate Modify(...)/Remove(...)are the only mutation paths — they invokeOnCheckCanMutate(), apply the change, transitionPersistenceState, then callOnMutate()OnCheckCanMutate()returnsResultbut is invoked via.ThrowOnError()internally — a failing guard throws the exception carried by theResult(typicallyBusinessExceptionforResult.BusinessError(...)) fromModify/Remove, regardless of theResultreturn type declared here- Public mutation methods on the aggregate should return
Result/Result<T>for consistency with Application-layerResult<T>pipelines — even though the internal guard throws - Child entity mutation methods are
internal— only the owning aggregate may invoke them - No async I/O in domain classes — ever. Async belongs in Application services or Policies.
- No native domain-event dispatch (MediatR-style) — only integration events via
Aggregate<TId,TSelf>.Events, forwarded throughIUnitOfWork.Eventsin the Application layer - Aggregates are the best unit-test target in the codebase (no injected dependencies) — cover every mutation method's happy path and rejection path with
WithGenericTester<EntryPoint>+Test.Scoped(...), calling the aggregate directly (no repository, no Application service); assertOnCheckCanMutate()guard failures as thrown exceptions, and failed-Resultreturns asResultassertions - A value object backed by a JSON database column (e.g.
Addressstored asbasket.ShippingAddressJson) is authored exactly like any other value object — a plainsealed record/record classwith invariant-enforcinginitsetters. It sits as the middle type in a three-type chain (Contracts.Xxx↔Domain.ValueObjects.Xxx↔Persistence.Xxx) bridged by two separate mappers (Application: Domain↔Contract; Infrastructure: Domain↔Persistence) — see `coreex-domain.instructions.md#value-objects-backed-by-a-json-column` and the `coreex-db-migration` skill for the column/mapper setup
For full workflow and code examples see `references/workflow.md`.
Key References
- `/.github/instructions/coreex-domain.instructions.md` — full Domain layer conventions (aggregates, entities, value objects,
PersistenceState) - `/.github/instructions/coreex-application-services.instructions.md` — how Application services construct, mutate, and map aggregates (
Domain.Xxx.CreateNew(...),Application/Mapping/Mapper<TSource,TDest,TSelf>) - `/.github/instructions/coreex-tests.instructions.md` —
*.Test.Unitconventions (WithGenericTester<EntryPoint>,Test.Scoped(...)) reused for aggregate unit tests - Related skills: `coreex-contract` (maps aggregate ↔ contract), `coreex-repository` (persists aggregates via
PersistenceState), `coreex-app-service` (constructs/mutates aggregates, forwardsEvents), `coreex-db-migration` (JSON column setup for JSON-backed value objects) - Deep-dive (after
/coreex-docs-sync) — the primary consumer-resolvable pointers intoCoreEx.DomainDriven:- `/.github/docs/coreex/agents/CoreEx.DomainDriven.md` — package AI usage guide (key types, the "Domain Events — Intentionally Not Supported" rationale)
- `/.github/docs/coreex/domain-layer.md` — Domain layer sample architecture doc
- Illustrative examples (CoreEx sample — not present in your project):
- CoreEx.DomainDriven README — package overview and rationale
- Aggregate.cs, Entity.cs, EntityBase.cs — base-class implementation (
Modify,Remove,OnCheckCanMutate,OnMutate,PersistenceStatetransitions) - PersistenceState.cs, DomainDrivenExtensions.cs — state enum and filter helpers (
IsNew,IsModified,IsNewOrModified,IsNotRemoved) - CoreEx.Template README —
coreex-domainaddon template and generated*.Domainproject shape