aw-spec-cli 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/.agents/skills/aw-spec-addcomponent/SKILL.md +129 -0
- package/assets/.agents/skills/aw-spec-agent-context-update/SKILL.md +31 -0
- package/assets/.agents/skills/aw-spec-analyze/SKILL.md +296 -0
- package/assets/.agents/skills/aw-spec-checklist/SKILL.md +374 -0
- package/assets/.agents/skills/aw-spec-clarify/SKILL.md +320 -0
- package/assets/.agents/skills/aw-spec-constitution/SKILL.md +194 -0
- package/assets/.agents/skills/aw-spec-fast-implement/SKILL.md +268 -0
- package/assets/.agents/skills/aw-spec-foundation/SKILL.md +358 -0
- package/assets/.agents/skills/aw-spec-implement/SKILL.md +279 -0
- package/assets/.agents/skills/aw-spec-minimal-implementation/SKILL.md +209 -0
- package/assets/.agents/skills/aw-spec-plan/SKILL.md +221 -0
- package/assets/.agents/skills/aw-spec-select-domain/SKILL.md +73 -0
- package/assets/.agents/skills/aw-spec-specify/SKILL.md +382 -0
- package/assets/.agents/skills/aw-spec-tasks/SKILL.md +259 -0
- package/assets/.agents/skills/aw-spec-taskstoissues/SKILL.md +107 -0
- package/assets/.aw-spec/domain.json +3 -0
- package/assets/.aw-spec/extensions/.registry +19 -0
- package/assets/.aw-spec/extensions/agent-context/README.md +57 -0
- package/assets/.aw-spec/extensions/agent-context/agent-context-config.yml +4 -0
- package/assets/.aw-spec/extensions/agent-context/commands/aw-spec.agent-context.update.md +26 -0
- package/assets/.aw-spec/extensions/agent-context/extension.yml +34 -0
- package/assets/.aw-spec/extensions/agent-context/scripts/bash/update-agent-context.sh +201 -0
- package/assets/.aw-spec/extensions/agent-context/scripts/powershell/update-agent-context.ps1 +237 -0
- package/assets/.aw-spec/extensions.yml +23 -0
- package/assets/.aw-spec/feature.json +3 -0
- package/assets/.aw-spec/init-options.json +9 -0
- package/assets/.aw-spec/integration.json +15 -0
- package/assets/.aw-spec/integrations/aw-spec.manifest.json +17 -0
- package/assets/.aw-spec/integrations/claude.manifest.json +17 -0
- package/assets/.aw-spec/memory/constitution.md +50 -0
- package/assets/.aw-spec/scripts/bash/check-prerequisites.sh +189 -0
- package/assets/.aw-spec/scripts/bash/common.sh +582 -0
- package/assets/.aw-spec/scripts/bash/create-new-feature.sh +299 -0
- package/assets/.aw-spec/scripts/bash/setup-plan.sh +84 -0
- package/assets/.aw-spec/scripts/bash/setup-tasks.sh +91 -0
- package/assets/.aw-spec/templates/checklist-template.md +40 -0
- package/assets/.aw-spec/templates/constitution-template.md +57 -0
- package/assets/.aw-spec/templates/plan-template.md +113 -0
- package/assets/.aw-spec/templates/spec-template.md +131 -0
- package/assets/.aw-spec/templates/tasks-template.md +252 -0
- package/assets/.aw-spec/workflows/aw-spec/workflow.yml +77 -0
- package/assets/.aw-spec/workflows/workflow-registry.json +13 -0
- package/assets/.claude/skills/aw-spec-addcomponent/SKILL.md +129 -0
- package/assets/.claude/skills/aw-spec-agent-context-update/SKILL.md +31 -0
- package/assets/.claude/skills/aw-spec-analyze/SKILL.md +296 -0
- package/assets/.claude/skills/aw-spec-checklist/SKILL.md +374 -0
- package/assets/.claude/skills/aw-spec-clarify/SKILL.md +320 -0
- package/assets/.claude/skills/aw-spec-constitution/SKILL.md +194 -0
- package/assets/.claude/skills/aw-spec-fast-implement/SKILL.md +268 -0
- package/assets/.claude/skills/aw-spec-foundation/SKILL.md +358 -0
- package/assets/.claude/skills/aw-spec-implement/SKILL.md +279 -0
- package/assets/.claude/skills/aw-spec-minimal-implementation/SKILL.md +209 -0
- package/assets/.claude/skills/aw-spec-plan/SKILL.md +221 -0
- package/assets/.claude/skills/aw-spec-select-domain/SKILL.md +73 -0
- package/assets/.claude/skills/aw-spec-specify/SKILL.md +382 -0
- package/assets/.claude/skills/aw-spec-tasks/SKILL.md +259 -0
- package/assets/.claude/skills/aw-spec-taskstoissues/SKILL.md +107 -0
- package/assets/.kilocode/rules/specify-rules.md +4 -0
- package/assets/.kilocode/workflows/aw-spec-addcomponent.md +129 -0
- package/assets/.kilocode/workflows/aw-spec-agent-context.update.md +31 -0
- package/assets/.kilocode/workflows/aw-spec-analyze.md +296 -0
- package/assets/.kilocode/workflows/aw-spec-checklist.md +374 -0
- package/assets/.kilocode/workflows/aw-spec-clarify.md +320 -0
- package/assets/.kilocode/workflows/aw-spec-constitution.md +194 -0
- package/assets/.kilocode/workflows/aw-spec-fast-implement.md +263 -0
- package/assets/.kilocode/workflows/aw-spec-foundation.md +358 -0
- package/assets/.kilocode/workflows/aw-spec-implement.md +279 -0
- package/assets/.kilocode/workflows/aw-spec-minimal-implementation.md +204 -0
- package/assets/.kilocode/workflows/aw-spec-plan.md +221 -0
- package/assets/.kilocode/workflows/aw-spec-select-domain.md +73 -0
- package/assets/.kilocode/workflows/aw-spec-specify.md +382 -0
- package/assets/.kilocode/workflows/aw-spec-tasks.md +259 -0
- package/assets/.kilocode/workflows/aw-spec-taskstoissues.md +107 -0
- package/assets/docs/README.md +106 -0
- package/assets/docs/VerticalSlice/PACKAGING.md +185 -0
- package/assets/docs/VerticalSlice/VerticalSlice.pack.txt +2833 -0
- package/assets/docs/VerticalSlice/pack-foundation.ps1 +145 -0
- package/assets/docs/VerticalSlice/pack-foundation.sh +196 -0
- package/assets/docs/VerticalSlice/unpack-foundation.ps1 +126 -0
- package/assets/docs/VerticalSlice/unpack-foundation.sh +217 -0
- package/assets/docs/architecture/clean-architecture.md +71 -0
- package/assets/docs/architecture/cqrs.md +141 -0
- package/assets/docs/architecture/eventing.md +83 -0
- package/assets/docs/architecture/folder-structure.md +61 -0
- package/assets/docs/architecture/modular-monolith.md +47 -0
- package/assets/docs/architecture/persistence.md +269 -0
- package/assets/docs/architecture/testing.md +55 -0
- package/assets/docs/architecture/vertical-slices.md +201 -0
- package/assets/docs/command.txt +295 -0
- package/assets/docs/components/Email/README.md +112 -0
- package/assets/docs/components/Email/integration.md +260 -0
- package/assets/docs/components/Jobs/README.md +113 -0
- package/assets/docs/components/Jobs/integration.md +245 -0
- package/assets/docs/components/QrGenerator/README.md +78 -0
- package/assets/docs/components/QrGenerator/integration.md +185 -0
- package/assets/docs/components/README.md +57 -0
- package/assets/docs/domain/domain-driven-design.md +212 -0
- package/assets/docs/domainspecs/healthcare/ai-rules.domain.md +34 -0
- package/assets/docs/domainspecs/healthcare/api-versioning.domain.md +17 -0
- package/assets/docs/domainspecs/healthcare/auditing.domain.md +29 -0
- package/assets/docs/domainspecs/healthcare/authorization.domain.md +26 -0
- package/assets/docs/domainspecs/healthcare/clinical-terms.md +20 -0
- package/assets/docs/domainspecs/healthcare/coding-standards.domain.md +26 -0
- package/assets/docs/domainspecs/healthcare/comandrulemap.md +119 -0
- package/assets/docs/domainspecs/healthcare/consent.md +19 -0
- package/assets/docs/domainspecs/healthcare/cqrs.domain.md +12 -0
- package/assets/docs/domainspecs/healthcare/data-classification.domain.md +28 -0
- package/assets/docs/domainspecs/healthcare/dependency-policy.domain.md +11 -0
- package/assets/docs/domainspecs/healthcare/dicom.md +15 -0
- package/assets/docs/domainspecs/healthcare/domain-driven-design.domain.md +28 -0
- package/assets/docs/domainspecs/healthcare/domain-requirements.md +55 -0
- package/assets/docs/domainspecs/healthcare/eventing.domain.md +18 -0
- package/assets/docs/domainspecs/healthcare/fhir.md +36 -0
- package/assets/docs/domainspecs/healthcare/folder-structure.domain.md +23 -0
- package/assets/docs/domainspecs/healthcare/hipaa.md +34 -0
- package/assets/docs/domainspecs/healthcare/hl7.md +15 -0
- package/assets/docs/domainspecs/healthcare/integration-terms.md +21 -0
- package/assets/docs/domainspecs/healthcare/persistence.domain.md +20 -0
- package/assets/docs/domainspecs/healthcare/terminology.md +30 -0
- package/assets/docs/domainspecs/healthcare/testing.domain.md +15 -0
- package/assets/docs/domainspecs/healthcare/vertical-slices.domain.md +18 -0
- package/assets/docs/domainspecs/healthcare/zero-trust.domain.md +12 -0
- package/assets/docs/domainspecs/pharma/ai-rules.domain.md +32 -0
- package/assets/docs/domainspecs/pharma/api-versioning.domain.md +20 -0
- package/assets/docs/domainspecs/pharma/atc.md +22 -0
- package/assets/docs/domainspecs/pharma/auditing.domain.md +26 -0
- package/assets/docs/domainspecs/pharma/authorization.domain.md +25 -0
- package/assets/docs/domainspecs/pharma/coding-standards.domain.md +28 -0
- package/assets/docs/domainspecs/pharma/comandrulemap.md +136 -0
- package/assets/docs/domainspecs/pharma/cqrs.domain.md +16 -0
- package/assets/docs/domainspecs/pharma/data-classification.domain.md +24 -0
- package/assets/docs/domainspecs/pharma/data-provenance.md +21 -0
- package/assets/docs/domainspecs/pharma/dependency-policy.domain.md +11 -0
- package/assets/docs/domainspecs/pharma/domain-driven-design.domain.md +36 -0
- package/assets/docs/domainspecs/pharma/domain-requirements.md +99 -0
- package/assets/docs/domainspecs/pharma/eventing.domain.md +18 -0
- package/assets/docs/domainspecs/pharma/fhir.md +31 -0
- package/assets/docs/domainspecs/pharma/folder-structure.domain.md +24 -0
- package/assets/docs/domainspecs/pharma/hl7.md +17 -0
- package/assets/docs/domainspecs/pharma/idmp.md +26 -0
- package/assets/docs/domainspecs/pharma/integration-terms.md +21 -0
- package/assets/docs/domainspecs/pharma/ndc.md +28 -0
- package/assets/docs/domainspecs/pharma/persistence.domain.md +23 -0
- package/assets/docs/domainspecs/pharma/pharma-terms.md +27 -0
- package/assets/docs/domainspecs/pharma/product-lifecycle.domain.md +30 -0
- package/assets/docs/domainspecs/pharma/regulatory-compliance.md +19 -0
- package/assets/docs/domainspecs/pharma/terminology.md +32 -0
- package/assets/docs/domainspecs/pharma/testing.domain.md +19 -0
- package/assets/docs/domainspecs/pharma/vertical-slices.domain.md +17 -0
- package/assets/docs/domainspecs/pharma/zero-trust.domain.md +12 -0
- package/assets/docs/glossary/ai-rules.md +59 -0
- package/assets/docs/governance/api-versioning.md +99 -0
- package/assets/docs/governance/coding-standards.md +87 -0
- package/assets/docs/governance/dependency-policy.md +40 -0
- package/assets/docs/interop/ihe-conformance.md +31 -0
- package/assets/docs/pharma-constitution.my-ref.md +86 -0
- package/assets/docs/requirement-spec.txt +15 -0
- package/assets/docs/security/auditing.md +117 -0
- package/assets/docs/security/authorization-matrix.md +14 -0
- package/assets/docs/security/authorization.md +87 -0
- package/assets/docs/security/data-classification.md +38 -0
- package/assets/docs/security/zero-trust.md +76 -0
- package/assets/docs/technology-constitution.md +172 -0
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +3 -2
- package/dist/commands/init.js.map +1 -1
- package/dist/utils/copy-assets.d.ts +8 -1
- package/dist/utils/copy-assets.d.ts.map +1 -1
- package/dist/utils/copy-assets.js +1 -0
- package/dist/utils/copy-assets.js.map +1 -1
- package/dist/utils/paths.js +1 -1
- package/dist/utils/paths.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# Clean Architecture Standards
|
|
2
|
+
|
|
3
|
+
> Pure technology (domain-agnostic). NON-NEGOTIABLE. Commands: `/aw-spec-plan`, `/aw-spec-tasks`, `/aw-spec-implement`.
|
|
4
|
+
|
|
5
|
+
## Dependency direction
|
|
6
|
+
|
|
7
|
+
Dependencies flow **inward only**:
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
API -> Application (Features) -> Domain <- Infrastructure
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
The Domain is the innermost ring; everything depends on it and it depends on nothing.
|
|
14
|
+
|
|
15
|
+
## Layer responsibilities
|
|
16
|
+
|
|
17
|
+
- **API** — HTTP surface (FastEndpoints). Translates transport to/from mediator
|
|
18
|
+
messages. MUST NOT contain business logic.
|
|
19
|
+
- **Application (Features)** — Command/Query handlers, validators, orchestration.
|
|
20
|
+
Dispatched via `IMediator`. MUST NOT depend on API frameworks or concrete EF Core types.
|
|
21
|
+
- **Domain** — aggregates, entities, value objects, domain events, specifications.
|
|
22
|
+
Framework-agnostic.
|
|
23
|
+
- **Infrastructure** — EF Core, external service clients, persistence configuration.
|
|
24
|
+
Depends inward on Domain abstractions.
|
|
25
|
+
|
|
26
|
+
## Allowed dependencies / annotations in Domain
|
|
27
|
+
|
|
28
|
+
The Domain layer is framework-agnostic. The **only** allowed annotations are:
|
|
29
|
+
|
|
30
|
+
- `[NotMapped]`
|
|
31
|
+
- `[ValueObject<T>]` (Vogen)
|
|
32
|
+
- `System.ComponentModel.DataAnnotations.Schema`
|
|
33
|
+
|
|
34
|
+
## Infrastructure boundaries
|
|
35
|
+
|
|
36
|
+
- EF Core mapping lives **only** in `{Module}/Persistence/<Aggregate>Configuration.cs` and is
|
|
37
|
+
applied via `ApplyConfigurationsFromAssembly`.
|
|
38
|
+
- **Endpoints MUST NOT** call repositories, `DbContext`, or `IUnitOfWork` directly; they
|
|
39
|
+
**MUST** call `IMediator.Send(...)`.
|
|
40
|
+
- **Application Features MUST NOT** depend on API frameworks or concrete EF Core types.
|
|
41
|
+
- Direct `DbContext` access from Endpoints or Application Features (outside the
|
|
42
|
+
repository / specification surface) is FORBIDDEN.
|
|
43
|
+
|
|
44
|
+
## Canonical example (agent reference — `<SampleProjectName>` sample)
|
|
45
|
+
|
|
46
|
+
The dependency rule in practice — the **API** layer depends inward via `IMediator` only
|
|
47
|
+
(never `IRepository`/`DbContext`); the **Application** layer depends on Domain abstractions
|
|
48
|
+
(`IRepository<Project>`), never on concrete EF Core types:
|
|
49
|
+
|
|
50
|
+
```csharp
|
|
51
|
+
// API layer — <SampleProjectName>.Service/Projects/Features/CreateProject/CreateProjectEndpoint.cs
|
|
52
|
+
public class CreateProjectEndpoint(IMediator _mediator)
|
|
53
|
+
: SecureFastEndpoint<CreateProjectRequest, ProjectDto>
|
|
54
|
+
{
|
|
55
|
+
protected override Task<Result<ProjectDto>> ExecuteAsync(
|
|
56
|
+
CreateProjectRequest req, CancellationToken ct)
|
|
57
|
+
=> _mediator.Send(new CreateProjectCommand(
|
|
58
|
+
req.Code, req.Name, req.Description, req.StartDate, req.EndDate, req.OwnerId), ct);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Application layer — depends on Domain abstractions, not EF Core
|
|
62
|
+
public class CreateProjectCommandHandler(
|
|
63
|
+
IRepository<Project> _repo, IUnitOfWork _uow, IPublisher _eventBus)
|
|
64
|
+
: ICommandHandler<CreateProjectCommand, Result<ProjectDto>> { /* ... */ }
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Related
|
|
68
|
+
|
|
69
|
+
- [[cqrs]] — how the Application layer dispatches work
|
|
70
|
+
- [[persistence]] — repository + specification surface, EF Core conventions
|
|
71
|
+
- [[vertical-slices]] — physical organization of the layers per feature
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# CQRS + Mediator Standards
|
|
2
|
+
|
|
3
|
+
> Pure technology (domain-agnostic) — domain additions in [[cqrs.domain]]. NON-NEGOTIABLE. Commands: `/aw-spec-plan`, `/aw-spec-tasks`, `/aw-spec-implement`.
|
|
4
|
+
|
|
5
|
+
## Command / query patterns
|
|
6
|
+
|
|
7
|
+
- All application logic is dispatched through **`AppWeaver.Mediator`** (NOT MediatR).
|
|
8
|
+
- **Commands:** `record XxxCommand(...) : ICommand<Result<TDto>>` or `ICommand<Result>`.
|
|
9
|
+
- **Queries:** `record XxxQuery(...) : IQuery<Result<TDto>>`.
|
|
10
|
+
- Handlers use **primary-constructor DI** and return `ValueTask<Result<T>>`.
|
|
11
|
+
|
|
12
|
+
## Mediator conventions
|
|
13
|
+
|
|
14
|
+
- Endpoints call `IMediator.Send(...)` — never repositories/`DbContext`/`IUnitOfWork` directly.
|
|
15
|
+
- Raw `Guid` parameters at the Application boundary **MUST be wrapped** into strongly-typed
|
|
16
|
+
IDs at the handler edge.
|
|
17
|
+
|
|
18
|
+
## Pipeline behaviors
|
|
19
|
+
|
|
20
|
+
Enabled at startup, in order:
|
|
21
|
+
|
|
22
|
+
1. Logging behavior
|
|
23
|
+
2. FluentValidation behavior
|
|
24
|
+
|
|
25
|
+
> Additional cross-cutting behaviors (e.g. domain access-audit behaviors) may be appended —
|
|
26
|
+
> see [[cqrs.domain]] for the domain audit-pipeline behavior.
|
|
27
|
+
|
|
28
|
+
## Result pattern
|
|
29
|
+
|
|
30
|
+
- Errors are returned as `Result<T>.Failure(new Error(code, message, status, resource))`.
|
|
31
|
+
- **Exceptions MUST NOT** be used for expected business outcomes (validation, not-found,
|
|
32
|
+
conflict, concurrency). `DomainException` is reserved for invariant violations and is
|
|
33
|
+
caught by the centralized exception handler.
|
|
34
|
+
- **Error code** MUST be a stable dot-namespaced slug — e.g. `resource.not_found`,
|
|
35
|
+
`concurrency.conflict`, `resource.duplicate`.
|
|
36
|
+
- **resource** MUST be the kebab-case resource name.
|
|
37
|
+
|
|
38
|
+
## Handler responsibilities
|
|
39
|
+
|
|
40
|
+
- **Read handlers** inject `IReadRepository<TAggregate>`.
|
|
41
|
+
- **Write handlers** inject `IRepository<TAggregate>` + `IUnitOfWork` (+ `IPublisher`
|
|
42
|
+
when domain events are produced).
|
|
43
|
+
- Validators check **request shape only**; invariants are enforced inside the aggregate.
|
|
44
|
+
- No business logic in handlers (anemic-domain anti-pattern — see [[domain-driven-design]]).
|
|
45
|
+
|
|
46
|
+
## Transaction handling
|
|
47
|
+
|
|
48
|
+
- Write handlers **MUST** wrap mutating work in
|
|
49
|
+
`IUnitOfWork.ExecuteInTransactionAsync(..., IsolationLevel.ReadCommitted, ct)` and call
|
|
50
|
+
`SaveChangesAsync` **inside** the lambda.
|
|
51
|
+
- They **MUST** publish domain events and call `ClearDomainEvents()` before returning.
|
|
52
|
+
- **One command mutates one aggregate per transaction** — cross-aggregate effects flow
|
|
53
|
+
through domain events or follow-up commands (see [[eventing]]).
|
|
54
|
+
|
|
55
|
+
## Canonical example (agent reference — `<SampleProjectName>` sample)
|
|
56
|
+
|
|
57
|
+
Command as a `record`, write handler with transaction + event publication, and a read handler
|
|
58
|
+
returning `Result.Failure(...)` for the expected not-found outcome (no exceptions):
|
|
59
|
+
|
|
60
|
+
```csharp
|
|
61
|
+
// Command — record implementing ICommand<Result<TDto>>
|
|
62
|
+
public record CreateProjectCommand(
|
|
63
|
+
string Code, string Name, string? Description,
|
|
64
|
+
DateOnly StartDate, DateOnly? EndDate, Guid? OwnerId
|
|
65
|
+
) : ICommand<Result<ProjectDto>>;
|
|
66
|
+
|
|
67
|
+
// Write handler — primary-ctor DI, transaction, publish + clear events, Result<T>
|
|
68
|
+
public class CreateProjectCommandHandler(
|
|
69
|
+
IRepository<Project> _repo, IUnitOfWork _uow, IPublisher _eventBus
|
|
70
|
+
) : ICommandHandler<CreateProjectCommand, Result<ProjectDto>>
|
|
71
|
+
{
|
|
72
|
+
public async ValueTask<Result<ProjectDto>> Handle(
|
|
73
|
+
CreateProjectCommand request, CancellationToken ct)
|
|
74
|
+
=> await _uow.ExecuteInTransactionAsync(async () =>
|
|
75
|
+
{
|
|
76
|
+
var ownerId = request.OwnerId.HasValue
|
|
77
|
+
? TaskUserId.From(request.OwnerId.Value) : (TaskUserId?)null;
|
|
78
|
+
var entity = new Project(request.Code, request.Name, request.StartDate, ownerId);
|
|
79
|
+
|
|
80
|
+
await _repo.AddAsync(entity, ct);
|
|
81
|
+
await _uow.SaveChangesAsync(ct);
|
|
82
|
+
|
|
83
|
+
foreach (var e in entity.DomainEvents) await _eventBus.Publish(e, ct);
|
|
84
|
+
entity.ClearDomainEvents();
|
|
85
|
+
|
|
86
|
+
return Result<ProjectDto>.Ok(entity.ToDto());
|
|
87
|
+
}, IsolationLevel.ReadCommitted, ct);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Read handler — IReadRepository + Specification; not-found is a Result, not an exception
|
|
91
|
+
public class GetProjectByIdQueryHandler(IReadRepository<Project> _repo)
|
|
92
|
+
: IQueryHandler<GetProjectByIdQuery, Result<ProjectDto>>
|
|
93
|
+
{
|
|
94
|
+
public async ValueTask<Result<ProjectDto>> Handle(
|
|
95
|
+
GetProjectByIdQuery request, CancellationToken ct)
|
|
96
|
+
{
|
|
97
|
+
Guard.NotDefault(request.Id, nameof(request.Id));
|
|
98
|
+
var entity = await _repo.FirstOrDefaultAsync(
|
|
99
|
+
new ProjectWithTasksSpec(ProjectId.From(request.Id)), ct);
|
|
100
|
+
|
|
101
|
+
return entity is null
|
|
102
|
+
? Result<ProjectDto>.Failure(new Error(
|
|
103
|
+
"project.not_found", $"Project {request.Id} not found", 404, "projects"))
|
|
104
|
+
: Result<ProjectDto>.Ok(entity.ToDto());
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Commands with no return payload use `ICommand<Result>` (e.g. delete) and return
|
|
110
|
+
`Result.Success()` / `Result.Failure(...)`:
|
|
111
|
+
|
|
112
|
+
```csharp
|
|
113
|
+
public record DeleteProjectCommand(Guid Id, string RowVersionBase64) : ICommand<Result>;
|
|
114
|
+
|
|
115
|
+
public class DeleteProjectCommandHandler(IRepository<Project> _repo, IUnitOfWork _uow, IPublisher _bus)
|
|
116
|
+
: ICommandHandler<DeleteProjectCommand, Result> { /* ... returns Result.Success() */ }
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Composition — register the mediator with the logging + validation pipeline behaviors,
|
|
120
|
+
the validators, and the in-process event bus (one-time startup wiring):
|
|
121
|
+
|
|
122
|
+
```csharp
|
|
123
|
+
// <SampleProjectName>.Service/Extensions/ApplicationServiceRegistration.cs
|
|
124
|
+
services.AddMediator(builder =>
|
|
125
|
+
{
|
|
126
|
+
builder.UseDefault()
|
|
127
|
+
.UseBehaviors()
|
|
128
|
+
.AddLoggingBehavior()
|
|
129
|
+
.AddFluentValidationBehavior(); // domain projects also add PHI-audit — see [[cqrs.domain]]
|
|
130
|
+
}, options => options.Provider = "Default");
|
|
131
|
+
|
|
132
|
+
services.AddAppWeaverMediatorHandlersFrom(typeof(ApplicationServiceRegistration));
|
|
133
|
+
services.AddValidatorsFromAssembly(typeof(ApplicationServiceRegistration).Assembly);
|
|
134
|
+
|
|
135
|
+
services.AddEventBus(b => b.UseDefaultEventBus(o =>
|
|
136
|
+
o.RegisterServicesFromAssembly(typeof(ApplicationServiceRegistration).Assembly)));
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
## Related
|
|
140
|
+
|
|
141
|
+
- [[clean-architecture]] · [[persistence]] · [[eventing]] · [[domain-driven-design]] · [[cqrs.domain]]
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# Eventing Standards
|
|
2
|
+
|
|
3
|
+
> Pure technology (domain-agnostic) — domain additions in [[eventing.domain]]. Commands: `/aw-spec-plan`, `/aw-spec-tasks`, `/aw-spec-implement`.
|
|
4
|
+
|
|
5
|
+
## Domain events
|
|
6
|
+
|
|
7
|
+
- Domain Events derive from `DomainEventBase`, are **immutable**, and are added **inside**
|
|
8
|
+
aggregate methods via `AddDomainEvent(...)`.
|
|
9
|
+
- They are published **explicitly** by command handlers: `foreach` over `entity.DomainEvents`,
|
|
10
|
+
then `ClearDomainEvents()` before returning. See [[cqrs]].
|
|
11
|
+
- Cross-aggregate effects flow through domain events or follow-up commands — never a
|
|
12
|
+
multi-aggregate transaction.
|
|
13
|
+
|
|
14
|
+
## CloudEvents conventions
|
|
15
|
+
|
|
16
|
+
- Asynchronous propagation of domain events to downstream consumers uses a **durable event
|
|
17
|
+
stream**: **CloudEvents** over the configured broker (e.g. Azure Service Bus, AWS SQS/SNS,
|
|
18
|
+
or RabbitMQ).
|
|
19
|
+
- Each module owns its **public events**; downstream consumers subscribe to them rather than
|
|
20
|
+
reading the OLTP database.
|
|
21
|
+
|
|
22
|
+
## Event immutability
|
|
23
|
+
|
|
24
|
+
- Published events are immutable facts. Consumers build their own read models; they
|
|
25
|
+
**MUST NOT** query the source OLTP database directly.
|
|
26
|
+
- Analytics / reporting is built from emitted events — **never** by querying the OLTP DB
|
|
27
|
+
directly.
|
|
28
|
+
|
|
29
|
+
## Event versioning
|
|
30
|
+
|
|
31
|
+
- The service publishes a versioned, machine-readable **AsyncAPI** document for event
|
|
32
|
+
contracts.
|
|
33
|
+
- Breaking changes to a domain-event contract are **MAJOR** version bumps and require the
|
|
34
|
+
AsyncAPI document to regenerate cleanly (CI gate). See [[api-versioning]].
|
|
35
|
+
|
|
36
|
+
## Broker integration patterns
|
|
37
|
+
|
|
38
|
+
- The message broker is declared as an Aspire resource and referenced via `.WithReference(...)`;
|
|
39
|
+
connection details are discovered through Aspire, never hard-coded.
|
|
40
|
+
- Trace context **MUST** propagate across `IMediator.Send`, `HttpClient`, and EF Core; bypassing
|
|
41
|
+
the framework's `HttpClientFactory` is forbidden.
|
|
42
|
+
(Outbound domain-protocol propagation is covered in [[eventing.domain]].)
|
|
43
|
+
|
|
44
|
+
## Inbound integration rule (generic)
|
|
45
|
+
|
|
46
|
+
- Inbound integrations flow through **Interop adapters** that translate to internal commands;
|
|
47
|
+
they **MUST NOT** write directly to aggregates. (Domain feed types in [[eventing.domain]].)
|
|
48
|
+
|
|
49
|
+
## Canonical example (agent reference — `<SampleProjectName>` sample)
|
|
50
|
+
|
|
51
|
+
Immutable domain event, raised inside the aggregate, published explicitly by the handler:
|
|
52
|
+
|
|
53
|
+
```csharp
|
|
54
|
+
// <SampleProjectName>.Service/Projects/Domain/Events/ProjectCreatedEvent.cs
|
|
55
|
+
public class ProjectCreatedEvent(Guid projectId, string code, string name) : DomainEventBase
|
|
56
|
+
{
|
|
57
|
+
public Guid ProjectId { get; } = projectId; // get-only — immutable
|
|
58
|
+
public string Code { get; } = code;
|
|
59
|
+
public string Name { get; } = name;
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
```csharp
|
|
64
|
+
// Raised inside the aggregate method (never in the handler):
|
|
65
|
+
AddDomainEvent(new ProjectCreatedEvent(Id.Value, Code.Value, Name));
|
|
66
|
+
|
|
67
|
+
// Published by the command handler after SaveChanges, then cleared:
|
|
68
|
+
foreach (var domainEvent in entity.DomainEvents)
|
|
69
|
+
await _eventBus.Publish(domainEvent, ct); // IPublisher — in-process today, broker tomorrow
|
|
70
|
+
entity.ClearDomainEvents();
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Composition — register the event bus (handlers discovered from the assembly):
|
|
74
|
+
|
|
75
|
+
```csharp
|
|
76
|
+
// <SampleProjectName>.Service/Extensions/ApplicationServiceRegistration.cs
|
|
77
|
+
services.AddEventBus(b => b.UseDefaultEventBus(o =>
|
|
78
|
+
o.RegisterServicesFromAssembly(typeof(ApplicationServiceRegistration).Assembly)));
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Related
|
|
82
|
+
|
|
83
|
+
- [[cqrs]] · [[modular-monolith]] · [[api-versioning]] · [[eventing.domain]]
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# Mandatory Folder Structure
|
|
2
|
+
|
|
3
|
+
> Pure technology (domain-agnostic) — domain additions (Interop adapters, conformance tests, fixtures) in [[folder-structure.domain]]. NON-NEGOTIABLE. Commands: `/aw-spec-plan`, `/aw-spec-tasks`, `/aw-spec-implement`.
|
|
4
|
+
>
|
|
5
|
+
> Replace `<SampleProjectName>` with the project name (e.g. `Acme.Billing`).
|
|
6
|
+
|
|
7
|
+
## Layout
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
src/
|
|
11
|
+
<SampleProjectName>.AppHost/ Aspire composition root (Program.cs)
|
|
12
|
+
<SampleProjectName>.ServiceDefaults/ OTel + Serilog + resilience + service discovery
|
|
13
|
+
<SampleProjectName>.Service/ headless API runtime (a.k.a. <SampleProjectName>.Api)
|
|
14
|
+
Program.cs composition only
|
|
15
|
+
GlobalUsings.cs
|
|
16
|
+
appsettings.json / appsettings.{Env}.json
|
|
17
|
+
Common/ PagedResult<T> + shared value objects
|
|
18
|
+
Extensions/ ApplicationServiceRegistration (mediator/eventbus),
|
|
19
|
+
InfrastructureServiceRegistration (DbContext/repos),
|
|
20
|
+
Aspire/Host extensions, SpecificationExtensions
|
|
21
|
+
Infrastructure/
|
|
22
|
+
ApplicationDbContext.cs ContextAwareDbContext<T>
|
|
23
|
+
*DbContextFactory.cs design-time factories
|
|
24
|
+
Migrations/
|
|
25
|
+
Utilities/ ETag/HTTP helpers (HttpRequest/Response ETag ext)
|
|
26
|
+
<Module>/ one folder per bounded context
|
|
27
|
+
Domain/
|
|
28
|
+
<Aggregate>.cs
|
|
29
|
+
<ChildEntity>.cs
|
|
30
|
+
<ValueObject>.cs
|
|
31
|
+
<Aggregate>Id.cs [ValueObject<Guid>] partial struct
|
|
32
|
+
Enums/ (optional) status/priority enums
|
|
33
|
+
Events/ DomainEventBase descendants
|
|
34
|
+
Specifications/ Specification<TAggregate>
|
|
35
|
+
Persistence/
|
|
36
|
+
<Aggregate>Configuration.cs (+ <ChildEntity>Configuration.cs)
|
|
37
|
+
Features/<FeatureName>/ Command/Query + Handler + Endpoint + Validator
|
|
38
|
+
Shared/ module-public DTOs + ToDto mapping extensions
|
|
39
|
+
tests/
|
|
40
|
+
<SampleProjectName>.UnitTests/
|
|
41
|
+
<SampleProjectName>.IntegrationTests/ Testcontainers-driven
|
|
42
|
+
<SampleProjectName>.TestFixtures/ shared test fixtures
|
|
43
|
+
helm/
|
|
44
|
+
<SampleProjectName>.Api/
|
|
45
|
+
docs/ engineering standards (see [[README]] + context map)
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Rules
|
|
49
|
+
|
|
50
|
+
- Each **module** is a top-level folder with `Domain / Features / Persistence / Shared`
|
|
51
|
+
(see [[modular-monolith]], [[vertical-slices]]).
|
|
52
|
+
- Each **feature** is a self-contained vertical slice (Command/Query + Handler + Endpoint +
|
|
53
|
+
Validator co-located).
|
|
54
|
+
- Cross-cutting files (`Program.cs`, `GlobalUsings.cs`, `Directory.Build.props`,
|
|
55
|
+
`Directory.Packages.props`, `global.json`) MUST stay at the level they occupy in the
|
|
56
|
+
AppWeaver template.
|
|
57
|
+
- New folders outside this structure require a constitution amendment.
|
|
58
|
+
|
|
59
|
+
## Related
|
|
60
|
+
|
|
61
|
+
- [[modular-monolith]] · [[vertical-slices]] · [[clean-architecture]] · [[folder-structure.domain]]
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# Modular Monolith Standards
|
|
2
|
+
|
|
3
|
+
> Pure technology (domain-agnostic). NON-NEGOTIABLE where marked. Commands: `/aw-spec-plan`, `/aw-spec-tasks`, `/aw-spec-implement`.
|
|
4
|
+
|
|
5
|
+
## Module boundaries
|
|
6
|
+
|
|
7
|
+
- The runtime is a **single deployable service** partitioned into bounded contexts (modules).
|
|
8
|
+
- Each module is a **top-level folder** containing four mandatory child folders:
|
|
9
|
+
`Domain`, `Features`, `Persistence`, `Shared` (plus `Interop` where applicable).
|
|
10
|
+
- A module **owns** its aggregates, persistence configurations, DTOs, integration
|
|
11
|
+
adapters/projectors, and public events. Nothing outside the module may reach past these.
|
|
12
|
+
|
|
13
|
+
## Cross-module communication
|
|
14
|
+
|
|
15
|
+
- Modules **MUST NOT** reference another module's `Domain` types except through
|
|
16
|
+
**strongly-typed IDs** or the other module's **`Shared` DTOs**.
|
|
17
|
+
- Modules **MUST NOT** take EF Core `DbSet` dependencies on entities they do not own.
|
|
18
|
+
- **Cross-module reads** go through the other module's `Shared` DTOs or its `IMediator`
|
|
19
|
+
queries — never through its `DbContext` entities.
|
|
20
|
+
- **Cross-module writes** go through commands published to `IMediator` (in-process today,
|
|
21
|
+
on the event bus tomorrow). A module **MUST NOT** acquire a write lock on another
|
|
22
|
+
module's aggregate.
|
|
23
|
+
- No horizontal "Services/Managers" layers across slices.
|
|
24
|
+
|
|
25
|
+
## Schema-per-module guidance
|
|
26
|
+
|
|
27
|
+
- Per-module schemas in PostgreSQL (**schema-per-module**) are RECOMMENDED so a future
|
|
28
|
+
service extraction is a clean cutover.
|
|
29
|
+
|
|
30
|
+
## Module isolation / testability
|
|
31
|
+
|
|
32
|
+
- Every module **MUST be testable in isolation**: its unit tests and handler tests
|
|
33
|
+
**MUST NOT** require any other module to be loaded.
|
|
34
|
+
|
|
35
|
+
## Future microservice extraction strategy
|
|
36
|
+
|
|
37
|
+
- Today's modules are tomorrow's services. Splitting a module into its own service
|
|
38
|
+
**MUST require zero changes to the domain code of other modules**.
|
|
39
|
+
- Migration to microservices is **approved by amendment** when a module's deployment
|
|
40
|
+
cadence, scaling profile, team ownership, or regulatory boundary materially diverges
|
|
41
|
+
from the rest of the monolith.
|
|
42
|
+
|
|
43
|
+
## Related
|
|
44
|
+
|
|
45
|
+
- [[vertical-slices]] — feature organization within a module
|
|
46
|
+
- [[clean-architecture]] — dependency rule across layers
|
|
47
|
+
- [[eventing]] — async cross-module/event-bus propagation
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
# Persistence Standards
|
|
2
|
+
|
|
3
|
+
> Pure technology (domain-agnostic) — domain additions in [[persistence.domain]]. NON-NEGOTIABLE where marked. Commands: `/aw-spec-plan`, `/aw-spec-tasks`, `/aw-spec-implement`.
|
|
4
|
+
|
|
5
|
+
## EF Core conventions
|
|
6
|
+
|
|
7
|
+
- The `DbContext` is a `ContextAwareDbContext<TContext>` and is **provider-aware**.
|
|
8
|
+
- EF Core mapping lives **only** in `{Module}/Persistence/<Aggregate>Configuration.cs`,
|
|
9
|
+
applied via `ApplyConfigurationsFromAssembly`.
|
|
10
|
+
- `DateTimeOffset` columns **MUST** be re-mapped to `timestamptz` when the provider is Npgsql.
|
|
11
|
+
- Cross-cutting behaviors are applied via `modelBuilder.ApplyCrossCuttingBehaviors(this)`:
|
|
12
|
+
soft-delete global filters, tenant filters, base concurrency-token configuration.
|
|
13
|
+
|
|
14
|
+
## Repository rules
|
|
15
|
+
|
|
16
|
+
- **Read handlers** inject `IReadRepository<TAggregate>`; **write handlers** inject
|
|
17
|
+
`IRepository<TAggregate>` + `IUnitOfWork`.
|
|
18
|
+
- No "repository as data-access god object" — **no custom methods** beyond
|
|
19
|
+
`IRepository<T>` / `IReadRepository<T>` + `Specification<T>`.
|
|
20
|
+
- Direct `DbContext.Set<T>()` LINQ inside Application Features is FORBIDDEN — use
|
|
21
|
+
`IReadRepository<T>` + a `Specification<T>` only.
|
|
22
|
+
|
|
23
|
+
## Specification pattern
|
|
24
|
+
|
|
25
|
+
- Every non-trivial query is encapsulated in a `Specification<TAggregate>` under
|
|
26
|
+
`{Module}/Domain/Specifications/`.
|
|
27
|
+
- Specifications **MUST be composable**; paging via `WithPaging(pageNumber, pageSize)`.
|
|
28
|
+
- Ordering uses shared generic helpers (`OrderByIdAsc`, `OrderByIdDesc`, `OrderByNameAsc`);
|
|
29
|
+
ad-hoc `OrderBy` in handlers is forbidden when a typed helper exists.
|
|
30
|
+
- **Cross-aggregate joins via EF navigation properties are FORBIDDEN** — reference parent
|
|
31
|
+
aggregates by strongly-typed ID and project client-side.
|
|
32
|
+
|
|
33
|
+
## Concurrency handling
|
|
34
|
+
|
|
35
|
+
- Every mutable aggregate **MUST** implement `IConcurrencyTracked`.
|
|
36
|
+
- Concurrency tokens are **client-managed**: `byte[] RowVersion`,
|
|
37
|
+
`IsConcurrencyToken().ValueGeneratedNever()`, refreshed by a `SaveChanges` interceptor.
|
|
38
|
+
- SQL Server `IsRowVersion()` semantics **MUST NOT** leak into entity configurations.
|
|
39
|
+
- ETag / `If-Match` at the edge: see [[api-versioning]].
|
|
40
|
+
- Server sets the ETag header on every create/read/update via `SetETagFromBase64(...)`.
|
|
41
|
+
- Update/Delete endpoints extract `If-Match` via `GetIfMatchBase64Token()` and pass it
|
|
42
|
+
to the command; mismatch returns `409 concurrency.conflict`.
|
|
43
|
+
- `DbUpdateConcurrencyException` is translated to the same `409 concurrency.conflict`.
|
|
44
|
+
|
|
45
|
+
## Soft-delete rules
|
|
46
|
+
|
|
47
|
+
- Soft delete is enabled via `EnableSoftDelete()`.
|
|
48
|
+
- **Hard deletes are FORBIDDEN** for any aggregate flagged as retention-protected / regulated;
|
|
49
|
+
deletion is logical via `MarkAsDeleted(...)`. (See [[persistence.domain]] for which data
|
|
50
|
+
classes are retention-protected.)
|
|
51
|
+
- Append-only aggregates have no delete path at all.
|
|
52
|
+
|
|
53
|
+
## Tenancy patterns
|
|
54
|
+
|
|
55
|
+
- Tenancy is enabled via `EnableTenancy()` and `RegisterDefaultContexts(...)`.
|
|
56
|
+
- Tenant resolution **MUST be transparent** to handlers.
|
|
57
|
+
- Default model: **tenant-per-row** with tenant-aware global filters; deployments MAY opt
|
|
58
|
+
into tenant-per-database via configuration.
|
|
59
|
+
|
|
60
|
+
## Migrations policy
|
|
61
|
+
|
|
62
|
+
- Migrations are authored against **PostgreSQL** (production provider). Development MAY run
|
|
63
|
+
on SQLite (non-sensitive development data only).
|
|
64
|
+
- Multi-provider warnings (`PendingModelChangesWarning`) **MUST** be suppressed only at the
|
|
65
|
+
runtime registration site, **never** in migration code.
|
|
66
|
+
- Migrations **MUST** apply cleanly to a fresh PostgreSQL instance (CI gate).
|
|
67
|
+
|
|
68
|
+
## Provider profile
|
|
69
|
+
|
|
70
|
+
- Production: PostgreSQL 16+ (encryption-at-rest profile in [[persistence.domain]]);
|
|
71
|
+
per-module schemas where reasonable.
|
|
72
|
+
- Development: PostgreSQL via Aspire containers; SQLite only for in-memory unit-test fixtures.
|
|
73
|
+
|
|
74
|
+
## Canonical example (agent reference — `<SampleProjectName>` sample)
|
|
75
|
+
|
|
76
|
+
EF Core configuration (Vogen ID + value object conversions, unique business key,
|
|
77
|
+
cross-aggregate FK stored as a raw `Guid` with no navigation):
|
|
78
|
+
|
|
79
|
+
```csharp
|
|
80
|
+
// <SampleProjectName>.Service/Projects/Persistence/ProjectConfiguration.cs
|
|
81
|
+
public class ProjectConfiguration : IEntityTypeConfiguration<Project>
|
|
82
|
+
{
|
|
83
|
+
public void Configure(EntityTypeBuilder<Project> builder)
|
|
84
|
+
{
|
|
85
|
+
builder.ToTable("Projects");
|
|
86
|
+
builder.HasKey(e => e.Id);
|
|
87
|
+
|
|
88
|
+
// Strongly-typed Vogen ID -> Guid, never DB-generated
|
|
89
|
+
builder.Property(e => e.Id)
|
|
90
|
+
.HasConversion(id => id.Value, v => ProjectId.From(v))
|
|
91
|
+
.ValueGeneratedNever();
|
|
92
|
+
|
|
93
|
+
// Value object -> column + unique business key
|
|
94
|
+
builder.Property(e => e.Code)
|
|
95
|
+
.HasConversion(c => c.Value, v => ProjectCode.From(v))
|
|
96
|
+
.HasColumnName("Code").HasMaxLength(20).IsRequired();
|
|
97
|
+
builder.HasIndex(e => e.Code).IsUnique();
|
|
98
|
+
|
|
99
|
+
builder.Property(e => e.Status).HasConversion<string>().HasMaxLength(30).IsRequired();
|
|
100
|
+
|
|
101
|
+
// Cross-aggregate FK as nullable Guid — NO EF navigation to TaskUser
|
|
102
|
+
builder.Property(e => e.OwnerId)
|
|
103
|
+
.HasConversion(
|
|
104
|
+
id => id.HasValue ? id.Value.Value : (Guid?)null,
|
|
105
|
+
v => v.HasValue ? TaskUserId.From(v.Value) : (TaskUserId?)null)
|
|
106
|
+
.HasColumnName("OwnerId");
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Composable specification (filter + include + paging):
|
|
112
|
+
|
|
113
|
+
```csharp
|
|
114
|
+
// {Module}/Domain/Specifications/ProjectSearchSpec.cs
|
|
115
|
+
public sealed class ProjectSearchSpec : Specification<Project>
|
|
116
|
+
{
|
|
117
|
+
public ProjectSearchSpec(string? search, ProjectStatus? status = null)
|
|
118
|
+
{
|
|
119
|
+
Query.Include(p => p.Tasks);
|
|
120
|
+
if (!string.IsNullOrWhiteSpace(search))
|
|
121
|
+
{
|
|
122
|
+
var s = search.Trim().ToLowerInvariant();
|
|
123
|
+
Query.Where(p => p.Code.Value.ToLower().Contains(s) || p.Name.ToLower().Contains(s));
|
|
124
|
+
}
|
|
125
|
+
if (status.HasValue) Query.Where(p => p.Status == status.Value);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
public ProjectSearchSpec WithPaging(int pageNumber, int pageSize)
|
|
129
|
+
{
|
|
130
|
+
Query.Skip(Math.Max(0, (pageNumber - 1) * pageSize)).Take(Math.Max(1, pageSize));
|
|
131
|
+
return this;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Read handler using the spec + typed ordering helper, returning `PagedResult<T>`:
|
|
137
|
+
|
|
138
|
+
```csharp
|
|
139
|
+
var spec = new ProjectSearchSpec(request.Search, request.Status)
|
|
140
|
+
.OrderByIdDesc<ProjectSearchSpec, Project, ProjectId>(); // typed helper, not ad-hoc OrderBy
|
|
141
|
+
|
|
142
|
+
var total = await _repo.CountAsync(spec, ct);
|
|
143
|
+
var items = await _repo.ListAsync(spec.WithPaging(request.PageNumber, request.PageSize), ct);
|
|
144
|
+
|
|
145
|
+
return Result<PagedResult<ProjectDto>>.Ok(new PagedResult<ProjectDto>
|
|
146
|
+
{
|
|
147
|
+
Items = items.ToDtoList(), TotalCount = total,
|
|
148
|
+
PageNumber = request.PageNumber, PageSize = request.PageSize
|
|
149
|
+
});
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Owned child-entity configuration (separate table, FK + cascade, soft-delete mirror filter,
|
|
153
|
+
no EF navigation to other aggregates):
|
|
154
|
+
|
|
155
|
+
```csharp
|
|
156
|
+
// {Module}/Persistence/ProjectTaskConfiguration.cs
|
|
157
|
+
public class ProjectTaskConfiguration : IEntityTypeConfiguration<ProjectTask>
|
|
158
|
+
{
|
|
159
|
+
public void Configure(EntityTypeBuilder<ProjectTask> builder)
|
|
160
|
+
{
|
|
161
|
+
builder.ToTable("ProjectTasks");
|
|
162
|
+
builder.HasKey(e => e.Id);
|
|
163
|
+
builder.Property(e => e.Id)
|
|
164
|
+
.HasConversion(id => id.Value, v => ProjectTaskId.From(v)).ValueGeneratedNever();
|
|
165
|
+
|
|
166
|
+
builder.Property(e => e.ProjectId) // FK back to the root
|
|
167
|
+
.HasConversion(id => id.Value, v => ProjectId.From(v)).IsRequired();
|
|
168
|
+
|
|
169
|
+
builder.Property(e => e.Title) // Vogen value object
|
|
170
|
+
.HasConversion(t => t.Value, v => TaskTitle.From(v)).HasMaxLength(300).IsRequired();
|
|
171
|
+
builder.Property(e => e.Status).HasConversion<string>().HasMaxLength(30).IsRequired();
|
|
172
|
+
|
|
173
|
+
// Cross-aggregate ref stored as raw Guid — NO navigation to TaskUser
|
|
174
|
+
builder.Property(e => e.AssigneeId)
|
|
175
|
+
.HasConversion(
|
|
176
|
+
id => id.HasValue ? id.Value.Value : (Guid?)null,
|
|
177
|
+
v => v.HasValue ? TaskUserId.From(v.Value) : (TaskUserId?)null);
|
|
178
|
+
|
|
179
|
+
builder.HasOne(e => e.Project).WithMany(p => p.Tasks) // owned collection
|
|
180
|
+
.HasForeignKey(e => e.ProjectId).OnDelete(DeleteBehavior.Cascade);
|
|
181
|
+
|
|
182
|
+
// Mirror the parent's ISoftDeletable filter onto the required dependent
|
|
183
|
+
builder.HasQueryFilter(t => !t.Project.IsDeleted);
|
|
184
|
+
builder.HasIndex(e => e.ProjectId);
|
|
185
|
+
builder.HasIndex(e => e.AssigneeId);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
DbContext — `ContextAwareDbContext`, assembly-scanned configs, provider-aware `timestamptz`,
|
|
191
|
+
cross-cutting behaviors, and the client-managed concurrency token override:
|
|
192
|
+
|
|
193
|
+
```csharp
|
|
194
|
+
// <SampleProjectName>.Service/Infrastructure/ApplicationDbContext.cs
|
|
195
|
+
public class ApplicationDbContext(
|
|
196
|
+
DbContextOptions<ApplicationDbContext> options, ICurrentTenant? tenant = null)
|
|
197
|
+
: ContextAwareDbContext<ApplicationDbContext>(options, tenant)
|
|
198
|
+
{
|
|
199
|
+
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
|
200
|
+
{
|
|
201
|
+
modelBuilder.ApplyConfigurationsFromAssembly(typeof(ApplicationDbContext).Assembly);
|
|
202
|
+
|
|
203
|
+
if (Database.ProviderName == "Npgsql.EntityFrameworkCore.PostgreSQL")
|
|
204
|
+
foreach (var et in modelBuilder.Model.GetEntityTypes())
|
|
205
|
+
foreach (var p in et.GetProperties()
|
|
206
|
+
.Where(p => p.ClrType == typeof(DateTimeOffset) || p.ClrType == typeof(DateTimeOffset?)))
|
|
207
|
+
p.SetColumnType("timestamptz");
|
|
208
|
+
|
|
209
|
+
modelBuilder.ApplyCrossCuttingBehaviors(this); // soft-delete + tenant filters + audit
|
|
210
|
+
|
|
211
|
+
// Client-managed concurrency token (works on SQLite + PostgreSQL; not SQL Server IsRowVersion)
|
|
212
|
+
foreach (var et in modelBuilder.Model.GetEntityTypes())
|
|
213
|
+
if (typeof(IConcurrencyTracked).IsAssignableFrom(et.ClrType))
|
|
214
|
+
modelBuilder.Entity(et.ClrType).Property<byte[]>("RowVersion")
|
|
215
|
+
.IsConcurrencyToken().ValueGeneratedNever();
|
|
216
|
+
|
|
217
|
+
base.OnModelCreating(modelBuilder);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
public DbSet<Project> Projects => Set<Project>();
|
|
221
|
+
public DbSet<ProjectTask> ProjectTasks => Set<ProjectTask>();
|
|
222
|
+
}
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
Soft-delete: logical delete via `MarkAsDeleted(...)` + `DeleteAsync` (never a hard delete),
|
|
226
|
+
and a separate `IgnoreQueryFilters()` spec for admin/audit reads:
|
|
227
|
+
|
|
228
|
+
```csharp
|
|
229
|
+
// Delete handler — soft delete + concurrency guard (returns Result, no DTO)
|
|
230
|
+
var entity = await _repo.GetByIdAsync(ProjectId.From(request.Id), ct);
|
|
231
|
+
if (entity is null)
|
|
232
|
+
return Result.Failure(new Error("project.not_found", $"Project {request.Id} not found", 404, "projects"));
|
|
233
|
+
// ... concurrency token check (see api-versioning) ...
|
|
234
|
+
entity.MarkAsDeleted(); // sets IsDeleted/DeletedOn + raises ProjectDeletedEvent
|
|
235
|
+
await _repo.DeleteAsync(entity, ct); // framework translates to UPDATE, not DELETE
|
|
236
|
+
|
|
237
|
+
// Admin/audit spec that bypasses the global soft-delete filter
|
|
238
|
+
public sealed class ProjectSearchIncludeDeletedSpec : Specification<Project>
|
|
239
|
+
{
|
|
240
|
+
public ProjectSearchIncludeDeletedSpec(string? search, ProjectStatus? status = null)
|
|
241
|
+
{
|
|
242
|
+
Query.Include(p => p.Tasks).IgnoreQueryFilters(); // include soft-deleted rows
|
|
243
|
+
// ... same Where filters as ProjectSearchSpec ...
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
Composition — repository + cross-cutting behaviors registered once at startup:
|
|
249
|
+
|
|
250
|
+
```csharp
|
|
251
|
+
// <SampleProjectName>.Service/Extensions/InfrastructureServiceRegistration.cs
|
|
252
|
+
services.AddRepositories(builder =>
|
|
253
|
+
{
|
|
254
|
+
builder
|
|
255
|
+
.UseSingleDbContextEFRepositories<ApplicationDbContext>(db => db.UseSqlite(/* or UseNpgsql */)
|
|
256
|
+
.ConfigureWarnings(w => w.Ignore(RelationalEventId.PendingModelChangesWarning)))
|
|
257
|
+
.EnableTenancy()
|
|
258
|
+
.EnableSoftDelete()
|
|
259
|
+
.EnableClientManagedConcurrency()
|
|
260
|
+
.EnableAuditing().EnableAdvancedAuditing() // see [[auditing]]
|
|
261
|
+
.Done();
|
|
262
|
+
}, options => options.Provider = "EfCore");
|
|
263
|
+
|
|
264
|
+
services.TryAddScoped(typeof(IUnitOfWork), typeof(EfUnitOfWork<ApplicationDbContext>));
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
## Related
|
|
268
|
+
|
|
269
|
+
- [[clean-architecture]] · [[cqrs]] · [[domain-driven-design]] · [[api-versioning]] · [[persistence.domain]]
|