aw-spec-cli 0.1.5 → 0.1.6
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/agent.json +1 -1
- package/assets/.agents/skills/{aw-spec-orch → appweaver}/SKILL.md +10 -10
- package/assets/.agents/skills/{aw-spec-orch-demo → appweaver-demo}/SKILL.md +9 -9
- package/assets/.claude/agent.json +1 -1
- package/assets/.claude/skills/{aw-spec-orch → appweaver}/SKILL.md +10 -10
- package/assets/.claude/skills/{aw-spec-orch-demo → appweaver-demo}/SKILL.md +9 -9
- package/assets/.kilocode/agent.json +1 -1
- package/assets/.kilocode/workflows/{aw-spec-orch-demo.md → appweaver-demo.md} +9 -9
- package/assets/.kilocode/workflows/{aw-spec-orch.md → appweaver.md} +10 -10
- package/assets/docs/pharma-constitution.my-ref.md +86 -0
- package/dist/utils/paths.js +2 -2
- package/dist/utils/paths.js.map +1 -1
- package/package.json +1 -1
- package/assets/.aw-spec/DESIGN-FORMAT.md +0 -151
- package/assets/.aw-spec/ORCH-DIALECT.md +0 -156
- package/assets/.aw-spec/RULE-PARTITION.md +0 -126
- package/assets/.aw-spec/domain.json +0 -3
- package/assets/.aw-spec/extensions/.registry +0 -19
- package/assets/.aw-spec/extensions/agent-context/README.md +0 -57
- package/assets/.aw-spec/extensions/agent-context/agent-context-config.yml +0 -4
- package/assets/.aw-spec/extensions/agent-context/commands/aw-spec.agent-context.update.md +0 -26
- package/assets/.aw-spec/extensions/agent-context/extension.yml +0 -37
- package/assets/.aw-spec/extensions/agent-context/scripts/bash/update-agent-context.sh +0 -201
- package/assets/.aw-spec/extensions/agent-context/scripts/powershell/update-agent-context.ps1 +0 -237
- package/assets/.aw-spec/extensions.yml +0 -16
- package/assets/.aw-spec/feature.json +0 -3
- package/assets/.aw-spec/init-options.json +0 -9
- package/assets/.aw-spec/integration.json +0 -15
- package/assets/.aw-spec/integrations/aw-spec.manifest.json +0 -9
- package/assets/.aw-spec/interpreter/run-skill.py +0 -1158
- package/assets/.aw-spec/memory/constitution.md +0 -50
- package/assets/.aw-spec/orchestrator/AGENT-PROFILE.md +0 -94
- package/assets/.aw-spec/orchestrator/README.md +0 -300
- package/assets/.aw-spec/orchestrator/adapters/claude_agent_sdk.py +0 -74
- package/assets/.aw-spec/orchestrator/adapters/google_antigravity.py +0 -74
- package/assets/.aw-spec/orchestrator/modules/delta-design.orch.yaml +0 -105
- package/assets/.aw-spec/orchestrator/modules/delta-spec.orch.yaml +0 -79
- package/assets/.aw-spec/orchestrator/modules/domain-select.orch.yaml +0 -81
- package/assets/.aw-spec/orchestrator/modules/tech-select.orch.yaml +0 -75
- package/assets/.aw-spec/orchestrator/orchestrator.py +0 -1721
- package/assets/.aw-spec/orchestrator/rev-templates/ai-rules.reference.md +0 -40
- package/assets/.aw-spec/orchestrator/rev-templates/comandrulemap.skeleton.md +0 -59
- package/assets/.aw-spec/orchestrator/rev-templates/commandtechrulemap.skeleton.md +0 -97
- package/assets/.aw-spec/orchestrator/rev-templates/domain-constitution.skeleton.md +0 -61
- package/assets/.aw-spec/orchestrator/rev-templates/domain-requirements.skeleton.md +0 -48
- package/assets/.aw-spec/orchestrator/rev-templates/domain-rule-file.skeleton.md +0 -39
- package/assets/.aw-spec/orchestrator/rev-templates/technical-constitution.skeleton.md +0 -106
- package/assets/.aw-spec/orchestrator/rev-templates/technical-rule-file.skeleton.md +0 -54
- package/assets/.aw-spec/orchestrator/workflows/_smoke.orch.yaml +0 -160
- package/assets/.aw-spec/orchestrator/workflows/aw-spec-add-component.orch.yaml +0 -244
- package/assets/.aw-spec/orchestrator/workflows/aw-spec-adopt.orch.yaml +0 -775
- package/assets/.aw-spec/orchestrator/workflows/aw-spec-analyze-delta-design.orch.yaml +0 -248
- package/assets/.aw-spec/orchestrator/workflows/aw-spec-analyze-design.orch.yaml +0 -261
- package/assets/.aw-spec/orchestrator/workflows/aw-spec-analyze-slice.orch.yaml +0 -425
- package/assets/.aw-spec/orchestrator/workflows/aw-spec-build.orch.yaml +0 -188
- package/assets/.aw-spec/orchestrator/workflows/aw-spec-characterize.orch.yaml +0 -474
- package/assets/.aw-spec/orchestrator/workflows/aw-spec-close-delta.orch.yaml +0 -181
- package/assets/.aw-spec/orchestrator/workflows/aw-spec-constitution.orch.yaml +0 -284
- package/assets/.aw-spec/orchestrator/workflows/aw-spec-demo.orch.yaml +0 -103
- package/assets/.aw-spec/orchestrator/workflows/aw-spec-dependency-tree.orch.yaml +0 -69
- package/assets/.aw-spec/orchestrator/workflows/aw-spec-deploy.orch.yaml +0 -85
- package/assets/.aw-spec/orchestrator/workflows/aw-spec-design.orch.yaml +0 -382
- package/assets/.aw-spec/orchestrator/workflows/aw-spec-finalize-delta.orch.yaml +0 -382
- package/assets/.aw-spec/orchestrator/workflows/aw-spec-foundation.orch.yaml +0 -246
- package/assets/.aw-spec/orchestrator/workflows/aw-spec-implement-delta.orch.yaml +0 -231
- package/assets/.aw-spec/orchestrator/workflows/aw-spec-implement.orch.yaml +0 -258
- package/assets/.aw-spec/orchestrator/workflows/aw-spec-inventory.orch.yaml +0 -236
- package/assets/.aw-spec/orchestrator/workflows/aw-spec-onboard.orch.yaml +0 -949
- package/assets/.aw-spec/orchestrator/workflows/aw-spec-parity.orch.yaml +0 -293
- package/assets/.aw-spec/orchestrator/workflows/aw-spec-run.orch.yaml +0 -177
- package/assets/.aw-spec/orchestrator/workflows/aw-spec-select-domain.orch.yaml +0 -79
- package/assets/.aw-spec/orchestrator/workflows/aw-spec-select-feature.orch.yaml +0 -128
- package/assets/.aw-spec/orchestrator/workflows/aw-spec-specify.orch.yaml +0 -412
- package/assets/.aw-spec/scripts/bash/aw-inventory.sh +0 -14
- package/assets/.aw-spec/scripts/bash/aw-parity.sh +0 -13
- package/assets/.aw-spec/scripts/bash/aw-rev-constitution.sh +0 -13
- package/assets/.aw-spec/scripts/bash/aw-spec-actions.sh +0 -1899
- package/assets/.aw-spec/scripts/bash/aw-xref.sh +0 -15
- package/assets/.aw-spec/scripts/bash/common.sh +0 -533
- package/assets/.aw-spec/scripts/powershell/aw-inventory.ps1 +0 -14
- package/assets/.aw-spec/scripts/powershell/aw-parity.ps1 +0 -13
- package/assets/.aw-spec/scripts/powershell/aw-rev-constitution.ps1 +0 -13
- package/assets/.aw-spec/scripts/powershell/aw-spec-actions.ps1 +0 -1563
- package/assets/.aw-spec/scripts/powershell/aw-xref.ps1 +0 -15
- package/assets/.aw-spec/scripts/python/aw-inventory.py +0 -683
- package/assets/.aw-spec/scripts/python/aw-parity.py +0 -380
- package/assets/.aw-spec/scripts/python/aw-rev-constitution.py +0 -312
- package/assets/.aw-spec/scripts/python/aw-xref.py +0 -1330
- package/assets/.aw-spec/scripts/python/validate_okf.py +0 -349
- package/assets/.aw-spec/templates/constitution-domain-template.md +0 -75
- package/assets/.aw-spec/templates/constitution-technical-template.md +0 -70
- package/assets/.aw-spec/templates/delta-design-template.md +0 -69
- package/assets/.aw-spec/templates/delta-spec-template.md +0 -60
- package/assets/.aw-spec/templates/design-template.md +0 -383
- package/assets/.aw-spec/templates/okf-concept-template.md +0 -103
- package/assets/.aw-spec/templates/parity-manifest-template.json +0 -49
- package/assets/.aw-spec/templates/source-map-template.md +0 -85
- package/assets/.aw-spec/templates/spec-template.md +0 -252
- package/assets/docs/ModularMonolith/ModularMonolith.Reference.pack.txt +0 -25248
- package/assets/docs/ModularMonolith/ModularMonolith.pack.txt +0 -5726
- package/assets/docs/ModularMonolith/pack-foundation.ps1 +0 -145
- package/assets/docs/ModularMonolith/pack-foundation.sh +0 -196
- package/assets/docs/ModularMonolith/pack-reference-foundation.ps1 +0 -157
- package/assets/docs/ModularMonolith/pack-reference-foundation.sh +0 -209
- package/assets/docs/ModularMonolith/unpack-foundation.ps1 +0 -126
- package/assets/docs/ModularMonolith/unpack-foundation.sh +0 -217
- package/assets/docs/ModularMonolith/unpack-reference-foundation.ps1 +0 -129
- package/assets/docs/ModularMonolith/unpack-reference-foundation.sh +0 -218
- package/assets/docs/README.md +0 -106
- package/assets/docs/VerticalSlice/PACKAGING.md +0 -185
- package/assets/docs/VerticalSlice/VerticalSlice.Reference.pack.txt +0 -19318
- package/assets/docs/VerticalSlice/VerticalSlice.pack.txt +0 -2833
- package/assets/docs/VerticalSlice/pack-foundation.ps1 +0 -145
- package/assets/docs/VerticalSlice/pack-foundation.sh +0 -196
- package/assets/docs/VerticalSlice/pack-reference-foundation.ps1 +0 -157
- package/assets/docs/VerticalSlice/pack-reference-foundation.sh +0 -209
- package/assets/docs/VerticalSlice/unpack-foundation.ps1 +0 -126
- package/assets/docs/VerticalSlice/unpack-foundation.sh +0 -217
- package/assets/docs/VerticalSlice/unpack-reference-foundation.ps1 +0 -129
- package/assets/docs/VerticalSlice/unpack-reference-foundation.sh +0 -218
- package/assets/docs/architecture/clean-architecture.md +0 -71
- package/assets/docs/architecture/cqrs.md +0 -141
- package/assets/docs/architecture/eventing.md +0 -83
- package/assets/docs/architecture/folder-structure.md +0 -61
- package/assets/docs/architecture/modular-monolith.md +0 -47
- package/assets/docs/architecture/persistence.md +0 -269
- package/assets/docs/architecture/testing.md +0 -55
- package/assets/docs/architecture/vertical-slices.md +0 -201
- package/assets/docs/components/Email/README.md +0 -119
- package/assets/docs/components/Email/ReferenceEndpoints/SendEmailEndpoint.cs +0 -84
- package/assets/docs/components/Email/integration.md +0 -300
- package/assets/docs/components/Jobs/README.md +0 -113
- package/assets/docs/components/Jobs/integration.md +0 -259
- package/assets/docs/components/QrGenerator/README.md +0 -85
- package/assets/docs/components/QrGenerator/ReferenceEndpoints/GenerateQrEndpoint.cs +0 -57
- package/assets/docs/components/QrGenerator/integration.md +0 -230
- package/assets/docs/components/README.md +0 -74
- package/assets/docs/domain/domain-driven-design.md +0 -212
- package/assets/docs/domainspecs/healthcare/ai-rules.domain.md +0 -34
- package/assets/docs/domainspecs/healthcare/api-versioning.domain.md +0 -17
- package/assets/docs/domainspecs/healthcare/auditing.domain.md +0 -97
- package/assets/docs/domainspecs/healthcare/authorization.domain.md +0 -26
- package/assets/docs/domainspecs/healthcare/clinical-terms.md +0 -20
- package/assets/docs/domainspecs/healthcare/coding-standards.domain.md +0 -26
- package/assets/docs/domainspecs/healthcare/comandrulemap.md +0 -100
- package/assets/docs/domainspecs/healthcare/commandtechrulemap.md +0 -119
- package/assets/docs/domainspecs/healthcare/consent.md +0 -19
- package/assets/docs/domainspecs/healthcare/cqrs.domain.md +0 -12
- package/assets/docs/domainspecs/healthcare/data-classification.domain.md +0 -28
- package/assets/docs/domainspecs/healthcare/dependency-policy.domain.md +0 -11
- package/assets/docs/domainspecs/healthcare/dicom.md +0 -15
- package/assets/docs/domainspecs/healthcare/domain-driven-design.domain.md +0 -28
- package/assets/docs/domainspecs/healthcare/domain-requirements.md +0 -55
- package/assets/docs/domainspecs/healthcare/eventing.domain.md +0 -18
- package/assets/docs/domainspecs/healthcare/fhir.md +0 -36
- package/assets/docs/domainspecs/healthcare/folder-structure.domain.md +0 -23
- package/assets/docs/domainspecs/healthcare/hipaa.md +0 -34
- package/assets/docs/domainspecs/healthcare/hl7.md +0 -15
- package/assets/docs/domainspecs/healthcare/integration-terms.md +0 -21
- package/assets/docs/domainspecs/healthcare/persistence.domain.md +0 -20
- package/assets/docs/domainspecs/healthcare/terminology.md +0 -30
- package/assets/docs/domainspecs/healthcare/testing.domain.md +0 -15
- package/assets/docs/domainspecs/healthcare/vertical-slices.domain.md +0 -18
- package/assets/docs/domainspecs/healthcare/zero-trust.domain.md +0 -12
- package/assets/docs/domainspecs/pharma/ai-rules.domain.md +0 -32
- package/assets/docs/domainspecs/pharma/api-versioning.domain.md +0 -20
- package/assets/docs/domainspecs/pharma/atc.md +0 -22
- package/assets/docs/domainspecs/pharma/auditing.domain.md +0 -90
- package/assets/docs/domainspecs/pharma/authorization.domain.md +0 -25
- package/assets/docs/domainspecs/pharma/coding-standards.domain.md +0 -28
- package/assets/docs/domainspecs/pharma/comandrulemap.md +0 -108
- package/assets/docs/domainspecs/pharma/commandtechrulemap.md +0 -119
- package/assets/docs/domainspecs/pharma/cqrs.domain.md +0 -16
- package/assets/docs/domainspecs/pharma/data-classification.domain.md +0 -24
- package/assets/docs/domainspecs/pharma/data-provenance.md +0 -21
- package/assets/docs/domainspecs/pharma/dependency-policy.domain.md +0 -11
- package/assets/docs/domainspecs/pharma/domain-driven-design.domain.md +0 -36
- package/assets/docs/domainspecs/pharma/domain-requirements.md +0 -99
- package/assets/docs/domainspecs/pharma/eventing.domain.md +0 -18
- package/assets/docs/domainspecs/pharma/fhir.md +0 -31
- package/assets/docs/domainspecs/pharma/folder-structure.domain.md +0 -24
- package/assets/docs/domainspecs/pharma/hl7.md +0 -17
- package/assets/docs/domainspecs/pharma/idmp.md +0 -26
- package/assets/docs/domainspecs/pharma/integration-terms.md +0 -21
- package/assets/docs/domainspecs/pharma/ndc.md +0 -28
- package/assets/docs/domainspecs/pharma/persistence.domain.md +0 -23
- package/assets/docs/domainspecs/pharma/pharma-terms.md +0 -27
- package/assets/docs/domainspecs/pharma/product-lifecycle.domain.md +0 -30
- package/assets/docs/domainspecs/pharma/regulatory-compliance.md +0 -19
- package/assets/docs/domainspecs/pharma/terminology.md +0 -32
- package/assets/docs/domainspecs/pharma/testing.domain.md +0 -19
- package/assets/docs/domainspecs/pharma/vertical-slices.domain.md +0 -17
- package/assets/docs/domainspecs/pharma/zero-trust.domain.md +0 -12
- package/assets/docs/glossary/ai-rules.md +0 -59
- package/assets/docs/governance/api-versioning.md +0 -99
- package/assets/docs/governance/coding-standards.md +0 -87
- package/assets/docs/governance/dependency-policy.md +0 -40
- package/assets/docs/interop/ihe-conformance.md +0 -31
- package/assets/docs/requirement-spec.txt +0 -27
- package/assets/docs/security/auditing.md +0 -160
- package/assets/docs/security/authorization-matrix.md +0 -14
- package/assets/docs/security/authorization.md +0 -87
- package/assets/docs/security/data-classification.md +0 -38
- package/assets/docs/security/zero-trust.md +0 -76
- package/assets/docs/technology-constitution.md +0 -172
|
@@ -1,269 +0,0 @@
|
|
|
1
|
-
# Persistence Standards
|
|
2
|
-
|
|
3
|
-
> Pure technology (domain-agnostic) — domain additions in [[persistence.domain]]. NON-NEGOTIABLE where marked. Commands: `/aw-spec-design`, `/aw-spec-run`.
|
|
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]]
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
# Testing Standards
|
|
2
|
-
|
|
3
|
-
> Pure technology (domain-agnostic) — domain additions in [[testing.domain]]. Commands: `/aw-spec-design`, `/aw-spec-implement`, `/aw-spec-implement-delta`, `/aw-spec-analyze-slice`.
|
|
4
|
-
|
|
5
|
-
## Test-first engineering
|
|
6
|
-
|
|
7
|
-
- Generated tests follow Test-First Engineering and **MUST fail before** the production
|
|
8
|
-
change is in place.
|
|
9
|
-
- Any change to an aggregate invariant, endpoint contract, or error code **MUST** include
|
|
10
|
-
the corresponding test update **in the same commit**.
|
|
11
|
-
|
|
12
|
-
## Unit test conventions
|
|
13
|
-
|
|
14
|
-
- Unit tests are required for **every** aggregate behavior, value object, and specification.
|
|
15
|
-
- Tests are **deterministic** — time, randomness, and IDs are injected
|
|
16
|
-
(use the framework-provided `IClock` / `IGuidGenerator`). Never `DateTime.UtcNow` /
|
|
17
|
-
`Guid.NewGuid()` in the system under test.
|
|
18
|
-
- Tests **MUST NOT** reach into module internals; they exercise the public endpoint or
|
|
19
|
-
`IMediator` surface.
|
|
20
|
-
|
|
21
|
-
## Handler tests
|
|
22
|
-
|
|
23
|
-
Required for **every** Command/Query handler and MUST cover:
|
|
24
|
-
|
|
25
|
-
- happy path
|
|
26
|
-
- validation failure
|
|
27
|
-
- not-found
|
|
28
|
-
- (for writes) concurrency conflict
|
|
29
|
-
|
|
30
|
-
## Integration testing
|
|
31
|
-
|
|
32
|
-
- Required for **every** endpoint; run against **PostgreSQL via Testcontainers** or Aspire.
|
|
33
|
-
- Test database engine: Testcontainers PostgreSQL for integration; in-memory fixtures for
|
|
34
|
-
unit tests.
|
|
35
|
-
|
|
36
|
-
## Testcontainers usage
|
|
37
|
-
|
|
38
|
-
- Integration and contract tests spin up real PostgreSQL (and other backing services)
|
|
39
|
-
via Testcontainers / Aspire — no faked persistence in integration tests.
|
|
40
|
-
|
|
41
|
-
## Test data hygiene
|
|
42
|
-
|
|
43
|
-
- Tests use a **shared fixture library** rather than inlining fabricated data.
|
|
44
|
-
- Production data MUST NOT be used in development, test, demo, or shared CI.
|
|
45
|
-
(Domain specifics — synthetic fixtures + terminology sentinels — in [[testing.domain]].)
|
|
46
|
-
|
|
47
|
-
## Coverage rules
|
|
48
|
-
|
|
49
|
-
- Minimum **85% line coverage** on Application + Domain.
|
|
50
|
-
- **100%** on aggregate behaviors and value objects.
|
|
51
|
-
- Code coverage on changed files must meet the threshold (CI gate).
|
|
52
|
-
|
|
53
|
-
## Related
|
|
54
|
-
|
|
55
|
-
- [[domain-driven-design]] · [[persistence]] · [[testing.domain]]
|
|
@@ -1,201 +0,0 @@
|
|
|
1
|
-
# Vertical Slice Standards
|
|
2
|
-
|
|
3
|
-
> Pure technology (domain-agnostic) — domain additions in [[vertical-slices.domain]]. Commands: `/aw-spec-design`.
|
|
4
|
-
|
|
5
|
-
## Feature organization
|
|
6
|
-
|
|
7
|
-
- Each feature is a **self-contained vertical slice**: Command/Query, Handler, Endpoint,
|
|
8
|
-
Validator co-located in **one folder**.
|
|
9
|
-
- No horizontal "Services/Managers" layers spanning slices.
|
|
10
|
-
|
|
11
|
-
## Folder structure (per module)
|
|
12
|
-
|
|
13
|
-
```
|
|
14
|
-
<Module>/
|
|
15
|
-
Domain/
|
|
16
|
-
<Aggregate>.cs
|
|
17
|
-
<ChildEntity>.cs
|
|
18
|
-
<ValueObject>.cs
|
|
19
|
-
<Aggregate>Id.cs [ValueObject<Guid>] partial struct
|
|
20
|
-
Events/ DomainEventBase descendants
|
|
21
|
-
Specifications/
|
|
22
|
-
Persistence/<Aggregate>Configuration.cs
|
|
23
|
-
Features/<FeatureName>/ Command/Query + Handler + Endpoint + Validator
|
|
24
|
-
Interop/ external-protocol adapters (see [[vertical-slices.domain]])
|
|
25
|
-
Shared/ module-public DTOs + ToDto extensions
|
|
26
|
-
```
|
|
27
|
-
|
|
28
|
-
## Endpoint conventions
|
|
29
|
-
|
|
30
|
-
- One endpoint **per file**. Endpoints inherit `SecureFastEndpoint<TRequest, TResponse>`.
|
|
31
|
-
- Kebab-case routes; resources are plural nouns; verbs map to intent
|
|
32
|
-
(POST create, GET list/fetch, PUT full update, PATCH partial update, DELETE logical delete).
|
|
33
|
-
- GUID route ids **MUST** use the `:guid` constraint.
|
|
34
|
-
- Tagged via `Options(o => o.WithTags("NN - <Module> - (FastEndpoints)"))` and **MUST**
|
|
35
|
-
provide a `Summary`.
|
|
36
|
-
- HTTP request classes are declared **inside the same file as the Endpoint** and contain
|
|
37
|
-
**primitive types only**.
|
|
38
|
-
- Endpoints **MUST NOT** swallow exceptions — the central exception handler owns the contract.
|
|
39
|
-
- See [[authorization]] for security cache policy on endpoints.
|
|
40
|
-
|
|
41
|
-
## Validator placement
|
|
42
|
-
|
|
43
|
-
- Validators live in the **feature folder** alongside the Command/Query they validate.
|
|
44
|
-
- Validators check **request shape only**; aggregate invariants are enforced in the Domain.
|
|
45
|
-
|
|
46
|
-
## DTO placement (immutable contracts)
|
|
47
|
-
|
|
48
|
-
- DTOs live in `{Module}/Shared/` and are mapped with **hand-written** `ToDto()` /
|
|
49
|
-
`ToDtoList()` extension methods. AutoMapper-style reflection mappers are FORBIDDEN.
|
|
50
|
-
- `record EntityDto { public required Guid Id { get; init; } ... }` — no setters;
|
|
51
|
-
required init for non-nullable members.
|
|
52
|
-
- Every DTO **MUST** include a base64 `RowVersionBase64` when the aggregate implements
|
|
53
|
-
`IConcurrencyTracked`.
|
|
54
|
-
- Paged results **MUST** use the standard `PagedResult<T>` shape
|
|
55
|
-
(`Items, TotalCount, PageNumber, PageSize`, computed `TotalPages`) with required init props.
|
|
56
|
-
|
|
57
|
-
## Specification placement
|
|
58
|
-
|
|
59
|
-
- Query composition lives in the **Domain**, under `{Module}/Domain/Specifications/`.
|
|
60
|
-
See [[persistence]] for specification rules.
|
|
61
|
-
|
|
62
|
-
## Canonical example (agent reference — `<SampleProjectName>` sample)
|
|
63
|
-
|
|
64
|
-
A complete slice: Endpoint (+ request class in the same file), Validator, DTO, and mapping.
|
|
65
|
-
|
|
66
|
-
```csharp
|
|
67
|
-
// <SampleProjectName>.Service/Projects/Features/CreateProject/CreateProjectEndpoint.cs
|
|
68
|
-
public class CreateProjectEndpoint(IMediator _mediator)
|
|
69
|
-
: SecureFastEndpoint<CreateProjectRequest, ProjectDto>
|
|
70
|
-
{
|
|
71
|
-
public override void Configure()
|
|
72
|
-
{
|
|
73
|
-
Post("/projects"); // kebab-case, plural noun
|
|
74
|
-
Summary(s => s.Summary = "Create a new project");
|
|
75
|
-
Options(o => o.WithTags("02 - Project - (FastEndpoints)"));
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
protected override SecurityCachePolicy GetSecurityCachePolicy() => new()
|
|
79
|
-
{
|
|
80
|
-
SecurityLevel = SecurityLevel.Internal,
|
|
81
|
-
CachePolicy = CachePolicy.NoStore // mutating endpoint
|
|
82
|
-
};
|
|
83
|
-
|
|
84
|
-
protected override async Task<Result<ProjectDto>> ExecuteAsync(
|
|
85
|
-
CreateProjectRequest req, CancellationToken ct)
|
|
86
|
-
{
|
|
87
|
-
var result = await _mediator.Send(new CreateProjectCommand( // IMediator only
|
|
88
|
-
req.Code, req.Name, req.Description, req.StartDate, req.EndDate, req.OwnerId), ct);
|
|
89
|
-
|
|
90
|
-
if (result.IsSuccess && !string.IsNullOrWhiteSpace(result.Value?.RowVersionBase64))
|
|
91
|
-
HttpContext.Response.SetETagFromBase64(result.Value!.RowVersionBase64);
|
|
92
|
-
return result;
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// Request lives in the SAME file — primitive types only
|
|
97
|
-
public class CreateProjectRequest
|
|
98
|
-
{
|
|
99
|
-
public string Code { get; set; } = string.Empty;
|
|
100
|
-
public string Name { get; set; } = string.Empty;
|
|
101
|
-
public string? Description { get; set; }
|
|
102
|
-
public DateOnly StartDate { get; set; }
|
|
103
|
-
public DateOnly? EndDate { get; set; }
|
|
104
|
-
public Guid? OwnerId { get; set; }
|
|
105
|
-
}
|
|
106
|
-
```
|
|
107
|
-
|
|
108
|
-
```csharp
|
|
109
|
-
// .../CreateProject/CreateProjectValidator.cs — request shape only
|
|
110
|
-
public class CreateProjectValidator : AbstractValidator<CreateProjectCommand>
|
|
111
|
-
{
|
|
112
|
-
public CreateProjectValidator()
|
|
113
|
-
{
|
|
114
|
-
RuleFor(x => x.Code).NotEmpty().MaximumLength(20).Matches(@"^[A-Z0-9\-]+$");
|
|
115
|
-
RuleFor(x => x.Name).NotEmpty().MaximumLength(200);
|
|
116
|
-
RuleFor(x => x.EndDate)
|
|
117
|
-
.GreaterThanOrEqualTo(x => x.StartDate).When(x => x.EndDate.HasValue);
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
```
|
|
121
|
-
|
|
122
|
-
```csharp
|
|
123
|
-
// {Module}/Shared/ProjectDto.cs — includes RowVersionBase64 (IConcurrencyTracked)
|
|
124
|
-
public record ProjectDto
|
|
125
|
-
{
|
|
126
|
-
public Guid Id { get; set; }
|
|
127
|
-
public string Code { get; set; } = string.Empty;
|
|
128
|
-
public string Name { get; set; } = string.Empty;
|
|
129
|
-
public string Status { get; set; } = string.Empty;
|
|
130
|
-
public Guid? OwnerId { get; set; }
|
|
131
|
-
public List<ProjectTaskDto> Tasks { get; set; } = new();
|
|
132
|
-
public string RowVersionBase64 { get; set; } = string.Empty;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
// {Module}/Shared/ProjectMappingExtensions.cs — hand-written, zero reflection
|
|
136
|
-
public static class ProjectMappingExtensions
|
|
137
|
-
{
|
|
138
|
-
public static ProjectDto ToDto(this Project e) => new()
|
|
139
|
-
{
|
|
140
|
-
Id = e.Id.Value, Code = e.Code.Value, Name = e.Name,
|
|
141
|
-
Status = e.Status.ToString(), OwnerId = e.OwnerId?.Value,
|
|
142
|
-
Tasks = e.Tasks.Select(t => t.ToDto()).ToList(),
|
|
143
|
-
RowVersionBase64 = e.RowVersion is { Length: > 0 } rv
|
|
144
|
-
? Convert.ToBase64String(rv) : string.Empty
|
|
145
|
-
};
|
|
146
|
-
|
|
147
|
-
public static List<ProjectDto> ToDtoList(this IEnumerable<Project> es)
|
|
148
|
-
=> es.Select(e => e.ToDto()).ToList();
|
|
149
|
-
}
|
|
150
|
-
```
|
|
151
|
-
|
|
152
|
-
Sub-resource endpoint for a distinct operation on a child — nested route, multiple route
|
|
153
|
-
params, `EmptyResponse` when there is no body to return:
|
|
154
|
-
|
|
155
|
-
```csharp
|
|
156
|
-
// .../Features/AssignTask/AssignTaskEndpoint.cs
|
|
157
|
-
public class AssignTaskEndpoint(IMediator _mediator)
|
|
158
|
-
: SecureFastEndpoint<AssignTaskRequest, EmptyResponse>
|
|
159
|
-
{
|
|
160
|
-
public override void Configure()
|
|
161
|
-
{
|
|
162
|
-
Put("/projects/{projectId:guid}/tasks/{taskId:guid}/assignee"); // sub-resource
|
|
163
|
-
Summary(s => s.Summary = "Assign a task to a TaskUser");
|
|
164
|
-
Options(o => o.WithTags("02 - Project - (FastEndpoints)"));
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
protected override SecurityCachePolicy GetSecurityCachePolicy() => new()
|
|
168
|
-
{ SecurityLevel = SecurityLevel.Internal, CachePolicy = CachePolicy.NoStore };
|
|
169
|
-
|
|
170
|
-
protected override async Task<Result<EmptyResponse>> ExecuteAsync(
|
|
171
|
-
AssignTaskRequest req, CancellationToken ct)
|
|
172
|
-
{
|
|
173
|
-
var projectId = Route<Guid>("projectId");
|
|
174
|
-
var taskId = Route<Guid>("taskId");
|
|
175
|
-
var result = await _mediator.Send(new AssignTaskCommand(projectId, taskId, req.AssigneeId), ct);
|
|
176
|
-
return result.IsSuccess
|
|
177
|
-
? Result<EmptyResponse>.Ok(new EmptyResponse())
|
|
178
|
-
: Result<EmptyResponse>.Failure([.. result.Errors]);
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
public class AssignTaskRequest { public Guid AssigneeId { get; set; } }
|
|
183
|
-
```
|
|
184
|
-
|
|
185
|
-
Standard `PagedResult<T>` contract (required init props, computed `TotalPages`):
|
|
186
|
-
|
|
187
|
-
```csharp
|
|
188
|
-
// {Module}/Common/PagedResult.cs
|
|
189
|
-
public sealed class PagedResult<T>
|
|
190
|
-
{
|
|
191
|
-
public required IReadOnlyList<T> Items { get; init; }
|
|
192
|
-
public required int TotalCount { get; init; }
|
|
193
|
-
public required int PageNumber { get; init; }
|
|
194
|
-
public required int PageSize { get; init; }
|
|
195
|
-
public int TotalPages => (int)Math.Ceiling((double)TotalCount / PageSize);
|
|
196
|
-
}
|
|
197
|
-
```
|
|
198
|
-
|
|
199
|
-
## Related
|
|
200
|
-
|
|
201
|
-
- [[clean-architecture]] · [[cqrs]] · [[persistence]] · [[authorization]] · [[vertical-slices.domain]]
|
|
@@ -1,119 +0,0 @@
|
|
|
1
|
-
# Email Component (`AppWeaver.Messaging.Email`)
|
|
2
|
-
|
|
3
|
-
> Pure technology (domain-agnostic). Reference sheet for the Email messaging component.
|
|
4
|
-
> To wire it into a project, follow the agent runbook in [integration.md](./integration.md).
|
|
5
|
-
> Commands: `/aw-spec-add-component`.
|
|
6
|
-
|
|
7
|
-
## What it is
|
|
8
|
-
|
|
9
|
-
A provider-agnostic email-sending component. Application code depends on a single abstraction,
|
|
10
|
-
`IEmailService`; the transport (Microsoft Graph, SMTP, SendGrid, OAuth2-SMTP, or a local File
|
|
11
|
-
sink) is chosen once during DI registration via `AddEmailMessaging(...)`.
|
|
12
|
-
|
|
13
|
-
**Use when:** a feature must send transactional or notification email (user-created alerts,
|
|
14
|
-
confirmations, admin notices, password flows, attachments).
|
|
15
|
-
|
|
16
|
-
## Public contract (`AppWeaver.Messaging.Email`)
|
|
17
|
-
|
|
18
|
-
```csharp
|
|
19
|
-
namespace AppWeaver.Messaging.Email;
|
|
20
|
-
|
|
21
|
-
public interface IEmailService
|
|
22
|
-
{
|
|
23
|
-
// Convenience overload — single recipient.
|
|
24
|
-
Task SendAsync(string from, string to, string subject, string body,
|
|
25
|
-
bool isHtml = true, CancellationToken cancellationToken = default);
|
|
26
|
-
|
|
27
|
-
// Full message — multi-recipient, Cc/Bcc, attachments.
|
|
28
|
-
Task SendAsync(EmailMessage message, CancellationToken cancellationToken = default);
|
|
29
|
-
}
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
```csharp
|
|
33
|
-
namespace AppWeaver.Messaging.Email.Entities;
|
|
34
|
-
|
|
35
|
-
public class EmailMessage
|
|
36
|
-
{
|
|
37
|
-
public string From { get; set; } // falls back to provider DefaultFrom if empty
|
|
38
|
-
public List<string> To { get; set; } = new();
|
|
39
|
-
public List<string> Cc { get; set; } = new();
|
|
40
|
-
public List<string> Bcc { get; set; } = new();
|
|
41
|
-
public string Subject { get; set; }
|
|
42
|
-
public string Body { get; set; }
|
|
43
|
-
public bool IsHtml { get; set; } = true;
|
|
44
|
-
public List<EmailAttachment> Attachments { get; set; } = new();
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
public class EmailAttachment
|
|
48
|
-
{
|
|
49
|
-
public string FileName { get; set; }
|
|
50
|
-
public byte[] Content { get; set; }
|
|
51
|
-
public string ContentType { get; set; } = "application/octet-stream";
|
|
52
|
-
}
|
|
53
|
-
```
|
|
54
|
-
|
|
55
|
-
## Packages
|
|
56
|
-
|
|
57
|
-
| Package | Version | Role | Builder method | Provider key |
|
|
58
|
-
|---|---|---|---|---|
|
|
59
|
-
| `AppWeaver.Messaging.Email` | `0.1.3` | **Required.** Abstraction + `AddEmailMessaging` + facade. | — | — |
|
|
60
|
-
| `AppWeaver.Messaging.Email.MsGraph` | `0.1.2` | Microsoft 365 / Graph API (recommended for prod). | `UseMsGraph` | `MsGraph` |
|
|
61
|
-
| `AppWeaver.Messaging.Email.Smtp` | `0.1.3` | Classic SMTP (host/port/user/password). | `UseSmtp` | `Smtp` |
|
|
62
|
-
| `AppWeaver.Messaging.Email.SendGrid` | `0.1.2` | SendGrid API key. | `UseSendGrid` | `SendGrid` |
|
|
63
|
-
| `AppWeaver.Messaging.Email.OAuth2` | `0.1.3` | OAuth2-authenticated SMTP (O365). | `UseOAuth2` | `OAuth2` |
|
|
64
|
-
| `AppWeaver.Messaging.Email.File` | `0.1.2` | Writes email to a local `.txt` file — local dev / tests. | `UseFile` | `File` |
|
|
65
|
-
|
|
66
|
-
Install the abstraction **plus exactly one provider** for the environment you are wiring.
|
|
67
|
-
|
|
68
|
-
## Provider option shapes
|
|
69
|
-
|
|
70
|
-
| Provider | Options type | Key fields | Bound config section |
|
|
71
|
-
|---|---|---|---|
|
|
72
|
-
| MsGraph | `MsGraphEmailOptions` | `TenantId`, `ClientId`, `ClientSecret`, `DefaultFrom`, `Scopes` | `Messaging.Email.MsGraph` |
|
|
73
|
-
| Smtp | `SmtpEmailOptions` | `Host`, `Port`, `Username`, `Password`, `EnableSsl`, `DefaultFrom` | `Messaging.Email.Smtp` |
|
|
74
|
-
| SendGrid | `SendGridEmailOptions` | `ApiKey` *(required)*, `DefaultFrom` | `Messaging.Email.SendGrid` |
|
|
75
|
-
| OAuth2 | `OAuth2EmailOptions` | `TenantId`, `ClientId`, `ClientSecret`, `Host`, `Port`, `Scopes`, `DefaultFrom` | `Messaging.Email.OAuth2` |
|
|
76
|
-
| File | `FileEmailOptions` | `DefaultFrom` | `Messaging.Email.Smtp` *(quirk — pass options inline)* |
|
|
77
|
-
|
|
78
|
-
## Registration entry point
|
|
79
|
-
|
|
80
|
-
```csharp
|
|
81
|
-
// In AppWeaver.Messaging.Email.Extensions
|
|
82
|
-
IComponentBuilder<IEmailService> AddEmailMessaging(
|
|
83
|
-
this IServiceCollection services,
|
|
84
|
-
Action<IComponentBuilder<IEmailService>> configureBuilder, // required
|
|
85
|
-
Action<MessagingEmailConfiguration>? configureOptions = null,// set Provider here
|
|
86
|
-
IConfiguration? configuration = null); // OR bind from config
|
|
87
|
-
```
|
|
88
|
-
|
|
89
|
-
`MessagingEmailConfiguration` exposes a single `[Required] string Provider`. The facade
|
|
90
|
-
(`DefaultImplementation`) resolves the keyed provider whose key equals `Provider`.
|
|
91
|
-
|
|
92
|
-
## Source map (framework)
|
|
93
|
-
|
|
94
|
-
```
|
|
95
|
-
framework/Messaging.Email/
|
|
96
|
-
Integration.md ← original MsGraph tutorial (reference)
|
|
97
|
-
src/
|
|
98
|
-
AppWeaver.Messaging.Email/ ← IEmailService, EmailMessage, AddEmailMessaging, facade
|
|
99
|
-
AppWeaver.Messaging.Email.MsGraph/ ← UseMsGraph + MsGraphEmailService
|
|
100
|
-
AppWeaver.Messaging.Email.Smtp/ ← UseSmtp + SmtpEmailService
|
|
101
|
-
AppWeaver.Messaging.Email.SendGrid/ ← UseSendGrid + SendGridEmailService
|
|
102
|
-
AppWeaver.Messaging.Email.OAuth2/ ← UseOAuth2 + OAuth2EmailService
|
|
103
|
-
AppWeaver.Messaging.Email.File/ ← UseFile + FileEmailService
|
|
104
|
-
test/
|
|
105
|
-
MsGraphTest/Program.cs ← MsGraph end-to-end sample
|
|
106
|
-
TestApp/Program.cs ← OAuth2 + File multi-provider sample
|
|
107
|
-
```
|
|
108
|
-
|
|
109
|
-
## Sample reference endpoint
|
|
110
|
-
|
|
111
|
-
A ready-to-run FastEndpoint that sends mail inline (no command/handler) is bundled at
|
|
112
|
-
[`ReferenceEndpoints/SendEmailEndpoint.cs`](./ReferenceEndpoints/SendEmailEndpoint.cs). It is offered
|
|
113
|
-
(opt-in) at the end of the [integration.md](./integration.md) runbook and, if accepted, copied into
|
|
114
|
-
the host project's `ReferenceEndpoints/` folder. Endpoint: `POST /reference/email/send`.
|
|
115
|
-
|
|
116
|
-
## Related
|
|
117
|
-
|
|
118
|
-
- [integration.md](./integration.md) · [[components-readme]] · [[cqrs]] ·
|
|
119
|
-
[[clean-architecture]] · [[dependency-policy]]
|