aw-spec-cli 0.1.4 → 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 +28 -0
- package/assets/.agents/skills/appweaver/SKILL.md +56 -0
- package/assets/.agents/skills/appweaver-demo/SKILL.md +59 -0
- package/assets/.claude/agent.json +27 -0
- package/assets/.claude/skills/appweaver/SKILL.md +55 -0
- package/assets/.claude/skills/appweaver-demo/SKILL.md +59 -0
- package/assets/.kilocode/agent.json +28 -0
- package/assets/.kilocode/workflows/appweaver-demo.md +59 -0
- package/assets/.kilocode/workflows/appweaver.md +55 -0
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +7 -54
- package/dist/commands/init.js.map +1 -1
- package/dist/index.js +0 -2
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/assets/.agents/skills/aw-spec-addcomponent/SKILL.md +0 -129
- package/assets/.agents/skills/aw-spec-agent-context-update/SKILL.md +0 -31
- package/assets/.agents/skills/aw-spec-analyze/SKILL.md +0 -296
- package/assets/.agents/skills/aw-spec-checklist/SKILL.md +0 -374
- package/assets/.agents/skills/aw-spec-clarify/SKILL.md +0 -320
- package/assets/.agents/skills/aw-spec-constitution/SKILL.md +0 -194
- package/assets/.agents/skills/aw-spec-fast-implement/SKILL.md +0 -268
- package/assets/.agents/skills/aw-spec-foundation/SKILL.md +0 -358
- package/assets/.agents/skills/aw-spec-implement/SKILL.md +0 -279
- package/assets/.agents/skills/aw-spec-minimal-implementation/SKILL.md +0 -209
- package/assets/.agents/skills/aw-spec-plan/SKILL.md +0 -221
- package/assets/.agents/skills/aw-spec-select-domain/SKILL.md +0 -73
- package/assets/.agents/skills/aw-spec-specify/SKILL.md +0 -382
- package/assets/.agents/skills/aw-spec-tasks/SKILL.md +0 -259
- package/assets/.agents/skills/aw-spec-taskstoissues/SKILL.md +0 -107
- 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 -34
- 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 -23
- 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 -17
- package/assets/.aw-spec/integrations/claude.manifest.json +0 -17
- package/assets/.aw-spec/memory/constitution.md +0 -50
- package/assets/.aw-spec/scripts/bash/check-prerequisites.sh +0 -189
- package/assets/.aw-spec/scripts/bash/common.sh +0 -582
- package/assets/.aw-spec/scripts/bash/create-new-feature.sh +0 -299
- package/assets/.aw-spec/scripts/bash/setup-plan.sh +0 -84
- package/assets/.aw-spec/scripts/bash/setup-tasks.sh +0 -91
- package/assets/.aw-spec/templates/checklist-template.md +0 -40
- package/assets/.aw-spec/templates/constitution-template.md +0 -57
- package/assets/.aw-spec/templates/plan-template.md +0 -113
- package/assets/.aw-spec/templates/spec-template.md +0 -131
- package/assets/.aw-spec/templates/tasks-template.md +0 -252
- package/assets/.aw-spec/workflows/aw-spec/workflow.yml +0 -77
- package/assets/.aw-spec/workflows/workflow-registry.json +0 -13
- package/assets/.claude/skills/aw-spec-addcomponent/SKILL.md +0 -129
- package/assets/.claude/skills/aw-spec-agent-context-update/SKILL.md +0 -31
- package/assets/.claude/skills/aw-spec-analyze/SKILL.md +0 -296
- package/assets/.claude/skills/aw-spec-checklist/SKILL.md +0 -374
- package/assets/.claude/skills/aw-spec-clarify/SKILL.md +0 -320
- package/assets/.claude/skills/aw-spec-constitution/SKILL.md +0 -194
- package/assets/.claude/skills/aw-spec-fast-implement/SKILL.md +0 -268
- package/assets/.claude/skills/aw-spec-foundation/SKILL.md +0 -358
- package/assets/.claude/skills/aw-spec-implement/SKILL.md +0 -279
- package/assets/.claude/skills/aw-spec-minimal-implementation/SKILL.md +0 -209
- package/assets/.claude/skills/aw-spec-plan/SKILL.md +0 -221
- package/assets/.claude/skills/aw-spec-select-domain/SKILL.md +0 -73
- package/assets/.claude/skills/aw-spec-specify/SKILL.md +0 -382
- package/assets/.claude/skills/aw-spec-tasks/SKILL.md +0 -259
- package/assets/.claude/skills/aw-spec-taskstoissues/SKILL.md +0 -107
- package/assets/.kilocode/rules/specify-rules.md +0 -4
- package/assets/.kilocode/workflows/aw-spec-addcomponent.md +0 -129
- package/assets/.kilocode/workflows/aw-spec-agent-context.update.md +0 -31
- package/assets/.kilocode/workflows/aw-spec-analyze.md +0 -296
- package/assets/.kilocode/workflows/aw-spec-checklist.md +0 -374
- package/assets/.kilocode/workflows/aw-spec-clarify.md +0 -320
- package/assets/.kilocode/workflows/aw-spec-constitution.md +0 -194
- package/assets/.kilocode/workflows/aw-spec-fast-implement.md +0 -263
- package/assets/.kilocode/workflows/aw-spec-foundation.md +0 -358
- package/assets/.kilocode/workflows/aw-spec-implement.md +0 -279
- package/assets/.kilocode/workflows/aw-spec-minimal-implementation.md +0 -204
- package/assets/.kilocode/workflows/aw-spec-plan.md +0 -221
- package/assets/.kilocode/workflows/aw-spec-select-domain.md +0 -73
- package/assets/.kilocode/workflows/aw-spec-specify.md +0 -382
- package/assets/.kilocode/workflows/aw-spec-tasks.md +0 -259
- package/assets/.kilocode/workflows/aw-spec-taskstoissues.md +0 -107
- 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/UI.Visualization/PACKAGING.md +0 -191
- package/assets/docs/UI.Visualization/UI.Visualization.Reference.pack.txt +0 -11768
- package/assets/docs/UI.Visualization/UI.Visualization.pack.txt +0 -11214
- package/assets/docs/UI.Visualization/pack-foundation.ps1 +0 -165
- package/assets/docs/UI.Visualization/pack-foundation.sh +0 -206
- package/assets/docs/UI.Visualization/pack-reference-foundation.ps1 +0 -159
- package/assets/docs/UI.Visualization/pack-reference-foundation.sh +0 -205
- package/assets/docs/UI.Visualization/unpack-foundation.ps1 +0 -126
- package/assets/docs/UI.Visualization/unpack-foundation.sh +0 -217
- package/assets/docs/UI.Visualization/unpack-reference-foundation.ps1 +0 -129
- package/assets/docs/UI.Visualization/unpack-reference-foundation.sh +0 -218
- 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/command.txt +0 -295
- 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/UIVisualization/chart-bubble.md +0 -70
- package/assets/docs/domainspecs/UIVisualization/chart-dimred.md +0 -58
- package/assets/docs/domainspecs/UIVisualization/chart-heatmap.md +0 -62
- package/assets/docs/domainspecs/UIVisualization/chart-network.md +0 -69
- package/assets/docs/domainspecs/UIVisualization/chart-parallel.md +0 -75
- package/assets/docs/domainspecs/UIVisualization/chart-scurve.md +0 -66
- package/assets/docs/domainspecs/UIVisualization/chart-survival.md +0 -69
- package/assets/docs/domainspecs/UIVisualization/chart-trellis.md +0 -68
- package/assets/docs/domainspecs/UIVisualization/chart-violin.md +0 -61
- package/assets/docs/domainspecs/UIVisualization/chart-volcano.md +0 -63
- package/assets/docs/domainspecs/UIVisualization/charting-standards.domain.md +0 -80
- package/assets/docs/domainspecs/UIVisualization/clean-architecture.domain.md +0 -87
- package/assets/docs/domainspecs/UIVisualization/coding-standards.domain.md +0 -47
- package/assets/docs/domainspecs/UIVisualization/comandrulemap.md +0 -130
- package/assets/docs/domainspecs/UIVisualization/component-contracts.domain.md +0 -67
- package/assets/docs/domainspecs/UIVisualization/data-source.domain.md +0 -68
- package/assets/docs/domainspecs/UIVisualization/domain-requirements.md +0 -110
- package/assets/docs/domainspecs/UIVisualization/folder-structure.domain.md +0 -55
- package/assets/docs/domainspecs/UIVisualization/state-management.domain.md +0 -67
- package/assets/docs/domainspecs/UIVisualization/testing.domain.md +0 -52
- package/assets/docs/domainspecs/UIVisualization/ui-viz-terms.md +0 -39
- 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 -29
- 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 -166
- 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 -26
- 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 -186
- 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/security/auditing.md +0 -117
- 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,85 +0,0 @@
|
|
|
1
|
-
# QrGenerator Component (`AppWeaver.QrGenerator`)
|
|
2
|
-
|
|
3
|
-
> Pure technology (domain-agnostic). Reference sheet for the QR-code generation component.
|
|
4
|
-
> To wire it into a project, follow the agent runbook in [integration.md](./integration.md).
|
|
5
|
-
> Commands: `/aw-spec-plan`, `/aw-spec-tasks`, `/aw-spec-implement`.
|
|
6
|
-
|
|
7
|
-
## What it is
|
|
8
|
-
|
|
9
|
-
A small provider-agnostic component that turns a string into a QR-code **PNG byte array**.
|
|
10
|
-
Application code depends on the `IQRGenerator` abstraction; the implementation
|
|
11
|
-
(`QRCoder` library) is selected once during DI registration.
|
|
12
|
-
|
|
13
|
-
**Use when:** a feature must produce a scannable QR code — patient/asset cards, share links,
|
|
14
|
-
check-in tokens, downloadable QR images.
|
|
15
|
-
|
|
16
|
-
## Public contract (`AppWeaver.QrGenerator`)
|
|
17
|
-
|
|
18
|
-
```csharp
|
|
19
|
-
namespace AppWeaver.QRGenerator; // note: namespace is "QRGenerator" (capital R)
|
|
20
|
-
|
|
21
|
-
public interface IQRGenerator
|
|
22
|
-
{
|
|
23
|
-
/// <summary>Encodes the content and returns a PNG image as a byte[].</summary>
|
|
24
|
-
byte[] GenerateQR(string QRContent);
|
|
25
|
-
}
|
|
26
|
-
```
|
|
27
|
-
|
|
28
|
-
Implementation detail (`QRCoderService`): ECC level **Q**, **20px** per module, output **PNG**.
|
|
29
|
-
`null` content throws `ArgumentNullException`; an **empty string is allowed** (returns a valid
|
|
30
|
-
PNG).
|
|
31
|
-
|
|
32
|
-
## Packages
|
|
33
|
-
|
|
34
|
-
| Package | Version | Role | Builder method | Provider key |
|
|
35
|
-
|---|---|---|---|---|
|
|
36
|
-
| `AppWeaver.QrGenerator` | `0.1.2` | **Required.** Abstraction + `AddQRGenerator` + facade. | — | — |
|
|
37
|
-
| `AppWeaver.QrGenerator.QRCoder` | `0.1.2` | QRCoder-backed implementation. | `UseQRCoder` | `QRCoder` |
|
|
38
|
-
|
|
39
|
-
> **Casing trap:** the **package IDs** use `QrGenerator` (lower `r`) but the **namespaces/types**
|
|
40
|
-
> use `QRGenerator` (upper `R`). `using AppWeaver.QRGenerator;` is correct.
|
|
41
|
-
|
|
42
|
-
## Registration entry point
|
|
43
|
-
|
|
44
|
-
```csharp
|
|
45
|
-
// In AppWeaver.QRGenerator.Extensions
|
|
46
|
-
IComponentBuilder<IQRGenerator> AddQRGenerator(
|
|
47
|
-
this IServiceCollection services,
|
|
48
|
-
Action<IComponentBuilder<IQRGenerator>> configureBuilder, // required
|
|
49
|
-
Action<QRConfiguration>? configureOptions = null, // set Provider here
|
|
50
|
-
IConfiguration? configuration = null); // OR bind from config
|
|
51
|
-
|
|
52
|
-
// In AppWeaver.QRGenerator.QRCoder.Extensions
|
|
53
|
-
IComponentBuilder<IQRGenerator> UseQRCoder(
|
|
54
|
-
this IComponentBuilder<IQRGenerator> builder,
|
|
55
|
-
Action<QRGeneratorOptions>? options = null, // QRGeneratorOptions is currently empty
|
|
56
|
-
IConfiguration? configuration = null); // binds section "QRGenerator.QRCoder"
|
|
57
|
-
```
|
|
58
|
-
|
|
59
|
-
`QRConfiguration` exposes a single `[Required] string Provider`. The facade resolves the keyed
|
|
60
|
-
provider whose key equals `Provider`.
|
|
61
|
-
|
|
62
|
-
## Source map (framework)
|
|
63
|
-
|
|
64
|
-
```
|
|
65
|
-
framework/QrGenerator/
|
|
66
|
-
Integration.md ← original endpoint tutorial (reference)
|
|
67
|
-
README.md ← component overview
|
|
68
|
-
src/
|
|
69
|
-
AppWeaver.QrGenerator/ ← IQRGenerator, AddQRGenerator, DefaultImplementation
|
|
70
|
-
AppWeaver.QrGenerator.QRCoder/ ← UseQRCoder + QRCoderService
|
|
71
|
-
test/
|
|
72
|
-
QrGenerator.QrCoder.Tests/ ← QRCoderServiceTests (xUnit)
|
|
73
|
-
```
|
|
74
|
-
|
|
75
|
-
## Sample reference endpoint
|
|
76
|
-
|
|
77
|
-
A ready-to-run FastEndpoint that generates a QR PNG inline (no command/handler) is bundled at
|
|
78
|
-
[`ReferenceEndpoints/GenerateQrEndpoint.cs`](./ReferenceEndpoints/GenerateQrEndpoint.cs). It is
|
|
79
|
-
offered (opt-in) at the end of the [integration.md](./integration.md) runbook and, if accepted,
|
|
80
|
-
copied into the host project's `ReferenceEndpoints/` folder. Endpoint: `POST /reference/qr-codes`.
|
|
81
|
-
|
|
82
|
-
## Related
|
|
83
|
-
|
|
84
|
-
- [integration.md](./integration.md) · [[components-readme]] · [[cqrs]] ·
|
|
85
|
-
[[vertical-slices]] · [[dependency-policy]]
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
using FastEndpoints;
|
|
2
|
-
using AppWeaver.QRGenerator; // namespace uses QRGenerator (capital R); package id is QrGenerator
|
|
3
|
-
|
|
4
|
-
// NOTE: This is a bundled SAMPLE endpoint for the AppWeaver QrGenerator component.
|
|
5
|
-
// It lives under docs/ and is NOT compiled here. /aw-spec-addcomponent copies it into the
|
|
6
|
-
// host project's ReferenceEndpoints/ folder and replaces the __HOSTPROJECT__ namespace token
|
|
7
|
-
// with the host project's root namespace (e.g. AppWeaver.Health.Service).
|
|
8
|
-
namespace __HOSTPROJECT__.ReferenceEndpoints;
|
|
9
|
-
|
|
10
|
-
/// <summary>
|
|
11
|
-
/// Sample reference endpoint for the AppWeaver QrGenerator component.
|
|
12
|
-
///
|
|
13
|
-
/// Demonstrates the full sequence inline — it injects the <see cref="IQRGenerator"/> abstraction
|
|
14
|
-
/// directly, calls <c>GenerateQR</c>, and streams the PNG back as <c>image/png</c>. There is
|
|
15
|
-
/// intentionally no command / command-handler / mediator: this is a copy-paste sample only.
|
|
16
|
-
///
|
|
17
|
-
/// Behaviour note: <c>GenerateQR(null)</c> throws <see cref="System.ArgumentNullException"/>, but an
|
|
18
|
-
/// empty string is valid and returns a (small) PNG, so only <c>null</c> content is rejected here.
|
|
19
|
-
/// </summary>
|
|
20
|
-
public class GenerateQrEndpoint(IQRGenerator _qr) : Endpoint<GenerateQrRequest>
|
|
21
|
-
{
|
|
22
|
-
public override void Configure()
|
|
23
|
-
{
|
|
24
|
-
Post("/reference/qr-codes");
|
|
25
|
-
AllowAnonymous();
|
|
26
|
-
Summary(s =>
|
|
27
|
-
{
|
|
28
|
-
s.Summary = "Generate a QR-code PNG from text content";
|
|
29
|
-
s.Description = "Reference endpoint for the AppWeaver QrGenerator component. Injects "
|
|
30
|
-
+ "IQRGenerator directly (no command/handler) and returns an image/png body.";
|
|
31
|
-
});
|
|
32
|
-
Options(o => o.WithTags("Reference - QrGenerator"));
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
public override async Task HandleAsync(GenerateQrRequest req, CancellationToken ct)
|
|
36
|
-
{
|
|
37
|
-
if (req.Content is null) // null throws inside the provider; empty string is allowed
|
|
38
|
-
{
|
|
39
|
-
HttpContext.Response.StatusCode = 400;
|
|
40
|
-
await HttpContext.Response.WriteAsJsonAsync(
|
|
41
|
-
new { error = "'content' is required (an empty string is allowed)." }, ct);
|
|
42
|
-
return;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
byte[] png = _qr.GenerateQR(req.Content);
|
|
46
|
-
|
|
47
|
-
HttpContext.Response.ContentType = "image/png";
|
|
48
|
-
HttpContext.Response.Headers["Content-Disposition"] = "inline; filename=qr-code.png";
|
|
49
|
-
await HttpContext.Response.Body.WriteAsync(png, ct);
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
public class GenerateQrRequest
|
|
54
|
-
{
|
|
55
|
-
/// <summary>Text to encode into the QR code. Empty string is valid; null is rejected.</summary>
|
|
56
|
-
public string? Content { get; set; }
|
|
57
|
-
}
|
|
@@ -1,230 +0,0 @@
|
|
|
1
|
-
# QrGenerator Component — Integration Workflow (agent playbook)
|
|
2
|
-
|
|
3
|
-
> Pure technology (domain-agnostic). This file is an **agent runbook**: read it top-to-bottom and
|
|
4
|
-
> execute the workflow in order. It drives an interactive integration of `IQRGenerator` into a
|
|
5
|
-
> project. Read [README.md](./README.md) first for the contract. Replace `<SampleProjectName>`
|
|
6
|
-
> with the project name. Commands: `/aw-spec-implement`.
|
|
7
|
-
|
|
8
|
-
---
|
|
9
|
-
|
|
10
|
-
## Agent workflow — execute in order
|
|
11
|
-
|
|
12
|
-
1. **Load context.** Read [README.md](./README.md) and this file.
|
|
13
|
-
2. **Resolve the host project** `<HostProject>` (see [[folder-structure]], [[components-readme]] R0.2):
|
|
14
|
-
the single startup project that owns **`Program.cs`** + the ASP.NET Core web SDK —
|
|
15
|
-
`<SampleProjectName>.Service` (Vertical Slice foundation), `<SampleProjectName>.API` (Modular
|
|
16
|
-
Monolith foundation), or another name in a future template. **Find it by locating `Program.cs`, not
|
|
17
|
-
by assuming the name.** There is **no** separate `…Application` / `…Infrastructure` project in these
|
|
18
|
-
foundations; `<HostProject>` is the target for C1–C3 below and holds the API endpoints.
|
|
19
|
-
3. **Present the implementation catalog (below) as a selection list and ASK the user which
|
|
20
|
-
implementation(s) to integrate.** Use a multi-select prompt (`AskUserQuestion`,
|
|
21
|
-
`multiSelect: true`) for consistency with the other components.
|
|
22
|
-
- This component currently ships **one** implementation (`QRCoder`). If the catalog has a single
|
|
23
|
-
row, tell the user it is the only option and proceed once they confirm (do not invent others).
|
|
24
|
-
4. **Apply “Common steps”** (§Common steps).
|
|
25
|
-
5. **For each selected implementation, apply its block** (§Implementation blocks).
|
|
26
|
-
6. **Set the provider:** `configureOptions: options => options.Provider = "<selected key>"`.
|
|
27
|
-
7. **(Optional) Build the feature slice** (§Usage) — DTO + Command + Handler + Endpoint.
|
|
28
|
-
8. **Offer the bundled sample endpoint (ASK).** After the wiring above, ask the user (single-select
|
|
29
|
-
yes/no via `AskUserQuestion`) whether to add the ready-made sample endpoint. If **yes**, copy it
|
|
30
|
-
into the host project — see §Sample reference endpoint. If **no**, skip silently.
|
|
31
|
-
9. **Verify** (§Verification) and report what was added.
|
|
32
|
-
|
|
33
|
-
> **Guardrail — namespace casing.** Package IDs use `QrGenerator` (lower `r`); **namespaces/types
|
|
34
|
-
> use `QRGenerator` (upper `R`)**. `using AppWeaver.QRGenerator;` is correct — getting this wrong is
|
|
35
|
-
> the most common build break.
|
|
36
|
-
|
|
37
|
-
---
|
|
38
|
-
|
|
39
|
-
## Implementation catalog (selection list)
|
|
40
|
-
|
|
41
|
-
| Key | Implementation | Best for | Package (v) | Settings |
|
|
42
|
-
|---|---|---|---|---|
|
|
43
|
-
| `QRCoder` | QRCoder library (PNG, ECC level Q, 20px/module) | All QR-code generation | `AppWeaver.QrGenerator.QRCoder` (0.1.2) | none (`QRGeneratorOptions` is empty) |
|
|
44
|
-
|
|
45
|
-
---
|
|
46
|
-
|
|
47
|
-
## Common steps (apply once)
|
|
48
|
-
|
|
49
|
-
### C1 — Abstraction package version
|
|
50
|
-
|
|
51
|
-
**File:** `Directory.Packages.props`
|
|
52
|
-
|
|
53
|
-
```xml
|
|
54
|
-
<ItemGroup>
|
|
55
|
-
<!-- AppWeaver — QrGenerator (abstraction) -->
|
|
56
|
-
<PackageVersion Include="AppWeaver.QrGenerator" Version="0.1.2" />
|
|
57
|
-
<!-- + one <PackageVersion> per selected implementation — see block below -->
|
|
58
|
-
</ItemGroup>
|
|
59
|
-
```
|
|
60
|
-
|
|
61
|
-
### C2 — Project references (no `Version=`)
|
|
62
|
-
|
|
63
|
-
**File:** `src/<HostProject>/<HostProject>.csproj` (the host project resolved in step 2 — single
|
|
64
|
-
project, no separate Application/Infrastructure).
|
|
65
|
-
|
|
66
|
-
```xml
|
|
67
|
-
<ItemGroup>
|
|
68
|
-
<PackageReference Include="AppWeaver.QrGenerator" />
|
|
69
|
-
<!-- + each selected implementation’s package — see block below -->
|
|
70
|
-
</ItemGroup>
|
|
71
|
-
```
|
|
72
|
-
|
|
73
|
-
### C3 — Registration scaffold
|
|
74
|
-
|
|
75
|
-
**File:** the host project `<HostProject>` (step 2) — register in its
|
|
76
|
-
`Extensions/InfrastructureServiceRegistration.cs` (`AddInfrastructure`) when that file exists (it does
|
|
77
|
-
in the Vertical Slice / Modular Monolith foundations), otherwise directly in `Program.cs` **before**
|
|
78
|
-
`builder.Build()`.
|
|
79
|
-
|
|
80
|
-
```csharp
|
|
81
|
-
using AppWeaver.QRGenerator.Extensions; // AddQRGenerator (capital R)
|
|
82
|
-
|
|
83
|
-
services.AddQRGenerator(
|
|
84
|
-
configureBuilder: builder =>
|
|
85
|
-
{
|
|
86
|
-
// ⬇ one Use…() call per selected implementation
|
|
87
|
-
},
|
|
88
|
-
configureOptions: options => options.Provider = "<selected key>"); // step 6
|
|
89
|
-
```
|
|
90
|
-
|
|
91
|
-
> **Dual local/Aspire startup ([[components-readme]] R0.2a).** If the host project also has an
|
|
92
|
-
> `AddAspireInfrastructure` method (`Extensions/AspireInfrastructureExtensions.cs`) selected by
|
|
93
|
-
> `if (isAspire) … else …` in `Program.cs`, add this **same** `AddQRGenerator(...)` block to **both**
|
|
94
|
-
> `AddInfrastructure` and `AddAspireInfrastructure` (it is DB-agnostic — identical in both), or
|
|
95
|
-
> `IQRGenerator` is unresolvable when the app runs under Aspire.
|
|
96
|
-
|
|
97
|
-
No `appsettings.json` entry is required — there is nothing to configure.
|
|
98
|
-
|
|
99
|
-
---
|
|
100
|
-
|
|
101
|
-
## Implementation blocks
|
|
102
|
-
|
|
103
|
-
### QRCoder
|
|
104
|
-
```xml
|
|
105
|
-
<!-- Directory.Packages.props --> <PackageVersion Include="AppWeaver.QrGenerator.QRCoder" Version="0.1.2" />
|
|
106
|
-
<!-- *.csproj --> <PackageReference Include="AppWeaver.QrGenerator.QRCoder" />
|
|
107
|
-
```
|
|
108
|
-
```csharp
|
|
109
|
-
using AppWeaver.QRGenerator.QRCoder.Extensions; // UseQRCoder
|
|
110
|
-
// inside AddQRGenerator builder:
|
|
111
|
-
builder.UseQRCoder();
|
|
112
|
-
// → set options.Provider = "QRCoder" in step 6
|
|
113
|
-
```
|
|
114
|
-
|
|
115
|
-
---
|
|
116
|
-
|
|
117
|
-
## Usage (feature slice — DTO + Command + Handler + Endpoint)
|
|
118
|
-
|
|
119
|
-
Generate inside an Application command handler; inject **`IQRGenerator`**. Return the image as
|
|
120
|
-
base64 in a DTO so the API layer owns the response shape (see [[cqrs]], [[vertical-slices]]).
|
|
121
|
-
|
|
122
|
-
**DTO** — `src/<SampleProjectName>.Application/Features/Qr/Dtos/QrCodeDto.cs`
|
|
123
|
-
```csharp
|
|
124
|
-
public record QrCodeDto
|
|
125
|
-
{
|
|
126
|
-
public string Base64Image { get; init; } = string.Empty;
|
|
127
|
-
public string ContentType { get; init; } = "image/png";
|
|
128
|
-
}
|
|
129
|
-
```
|
|
130
|
-
|
|
131
|
-
**Command + Handler** — `…/Features/Qr/Commands/GenerateQrCommand.cs`
|
|
132
|
-
```csharp
|
|
133
|
-
public record GenerateQrCommand(string Content) : ICommand<Result<QrCodeDto>>;
|
|
134
|
-
|
|
135
|
-
public class GenerateQrCommandHandler(IQRGenerator _qr) // using AppWeaver.QRGenerator;
|
|
136
|
-
: ICommandHandler<GenerateQrCommand, Result<QrCodeDto>>
|
|
137
|
-
{
|
|
138
|
-
public ValueTask<Result<QrCodeDto>> Handle(GenerateQrCommand req, CancellationToken ct)
|
|
139
|
-
{
|
|
140
|
-
if (string.IsNullOrWhiteSpace(req.Content)) // GenerateQR(null) throws; guard first
|
|
141
|
-
return ValueTask.FromResult(
|
|
142
|
-
Result<QrCodeDto>.Failure(new Error("qr.empty_content", "QR content is required.", 400)));
|
|
143
|
-
|
|
144
|
-
byte[] png = _qr.GenerateQR(req.Content);
|
|
145
|
-
return ValueTask.FromResult(Result<QrCodeDto>.Ok(new QrCodeDto
|
|
146
|
-
{
|
|
147
|
-
Base64Image = Convert.ToBase64String(png),
|
|
148
|
-
ContentType = "image/png"
|
|
149
|
-
}));
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
```
|
|
153
|
-
|
|
154
|
-
**Endpoint** — `src/<SampleProjectName>.Service/Qr/Features/GenerateQr/GenerateQrEndpoint.cs`
|
|
155
|
-
```csharp
|
|
156
|
-
public class GenerateQrEndpoint(IMediator _mediator) : Endpoint<GenerateQrRequest>
|
|
157
|
-
{
|
|
158
|
-
public override void Configure()
|
|
159
|
-
{
|
|
160
|
-
Post("/qr-codes");
|
|
161
|
-
Summary(s => s.Summary = "Generate a QR-code PNG from text content");
|
|
162
|
-
Options(o => o.WithTags("04 - QR - (FastEndpoints)"));
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
public override async Task HandleAsync(GenerateQrRequest req, CancellationToken ct)
|
|
166
|
-
{
|
|
167
|
-
var result = await _mediator.Send(new GenerateQrCommand(req.Content), ct);
|
|
168
|
-
if (result.IsSuccess && result.Value is not null)
|
|
169
|
-
{
|
|
170
|
-
var bytes = Convert.FromBase64String(result.Value.Base64Image);
|
|
171
|
-
HttpContext.Response.ContentType = result.Value.ContentType; // image/png
|
|
172
|
-
HttpContext.Response.Headers["Content-Disposition"] = "attachment; filename=qr-code.png";
|
|
173
|
-
await HttpContext.Response.Body.WriteAsync(bytes, ct);
|
|
174
|
-
}
|
|
175
|
-
else
|
|
176
|
-
{
|
|
177
|
-
HttpContext.Response.StatusCode = 400;
|
|
178
|
-
await HttpContext.Response.WriteAsJsonAsync(new { error = "Failed to generate QR code" }, ct);
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
public class GenerateQrRequest { public string Content { get; set; } = string.Empty; }
|
|
184
|
-
```
|
|
185
|
-
|
|
186
|
-
---
|
|
187
|
-
|
|
188
|
-
## Sample reference endpoint
|
|
189
|
-
|
|
190
|
-
The §Usage slice above routes through a command + handler. For a quick, self-contained sample there
|
|
191
|
-
is also a ready-to-run FastEndpoint bundled in this component folder at
|
|
192
|
-
[`ReferenceEndpoints/GenerateQrEndpoint.cs`](./ReferenceEndpoints/GenerateQrEndpoint.cs). It injects
|
|
193
|
-
`IQRGenerator` directly and does the **full sequence inline** — no command/handler/mediator. It is
|
|
194
|
-
**not** wired automatically; it is offered in workflow step 8.
|
|
195
|
-
|
|
196
|
-
**When the user opts in (step 8) — copy procedure:**
|
|
197
|
-
|
|
198
|
-
1. Confirm `docs/components/QrGenerator/ReferenceEndpoints/GenerateQrEndpoint.cs` exists. If it is
|
|
199
|
-
missing, tell the user and skip (do not hand-write a replacement).
|
|
200
|
-
2. Create `src/<HostProject>/ReferenceEndpoints/` if it does not exist (`<HostProject>` = the project
|
|
201
|
-
resolved in step 2).
|
|
202
|
-
3. Copy the file to `src/<HostProject>/ReferenceEndpoints/GenerateQrEndpoint.cs`, replacing the
|
|
203
|
-
namespace token `__HOSTPROJECT__` with `<HostProject>`'s root namespace (e.g.
|
|
204
|
-
`AppWeaver.Health.Service`). Keep the `using AppWeaver.QRGenerator;` (capital R) — do not "fix" it
|
|
205
|
-
to match the package id. Change nothing else.
|
|
206
|
-
4. Do **not** touch the C1–C3 wiring. FastEndpoints auto-discovers the endpoint by assembly scan, so
|
|
207
|
-
no extra registration is required.
|
|
208
|
-
|
|
209
|
-
**Endpoint:** `POST /reference/qr-codes` — body `{ "content" }` — returns an `image/png` body.
|
|
210
|
-
An empty `content` string is valid (returns a small PNG); `null` is rejected with 400.
|
|
211
|
-
|
|
212
|
-
---
|
|
213
|
-
|
|
214
|
-
## Verification
|
|
215
|
-
|
|
216
|
-
1. **Builds** — `dotnet build` succeeds; `using` directives use `AppWeaver.QRGenerator…`; no
|
|
217
|
-
`Version=` on package references.
|
|
218
|
-
2. **Startup validates** — app boots; `options.Provider` is `"QRCoder"`.
|
|
219
|
-
3. **Resolves** — `IQRGenerator` resolves from DI.
|
|
220
|
-
4. **Generates** — `GenerateQR("test")` returns a non-empty `byte[]`; `POST /qr-codes` returns an
|
|
221
|
-
`image/png` body that opens as a scannable QR image. (`null` content throws; empty string is
|
|
222
|
-
valid and returns a PNG.)
|
|
223
|
-
5. **Sample endpoint (only if copied in step 8)** — the build still succeeds with
|
|
224
|
-
`ReferenceEndpoints/GenerateQrEndpoint.cs` added, the `__HOSTPROJECT__` token was replaced, and
|
|
225
|
-
`POST /reference/qr-codes` returns an `image/png`.
|
|
226
|
-
|
|
227
|
-
## Related
|
|
228
|
-
|
|
229
|
-
- [README.md](./README.md) · [[components-readme]] · [[cqrs]] · [[vertical-slices]] ·
|
|
230
|
-
[[clean-architecture]] · [[dependency-policy]]
|
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
# Framework Components — Integration Catalog
|
|
2
|
-
|
|
3
|
-
> Pure technology (domain-agnostic). These docs are the authoritative integration contracts for
|
|
4
|
-
> opt-in AppWeaver framework components. An AI coding agent (or a human) MUST read the relevant
|
|
5
|
-
> component folder **before** wiring the component into a project.
|
|
6
|
-
> Commands: `/aw-spec-plan`, `/aw-spec-tasks`, `/aw-spec-implement`.
|
|
7
|
-
|
|
8
|
-
Each component is an optional NuGet-distributed building block that plugs into the standard
|
|
9
|
-
AppWeaver layout (see [[folder-structure]]) via the same **provider/registry** registration
|
|
10
|
-
pattern: `AddXxx(builder => builder.UseProvider(...), options => options.Provider = "Key")`.
|
|
11
|
-
Your application code depends only on the component's **abstraction interface** — the concrete
|
|
12
|
-
provider is a single registration decision and is swappable with zero changes to feature code.
|
|
13
|
-
|
|
14
|
-
## Available components
|
|
15
|
-
|
|
16
|
-
| Component | Abstraction | Folder | Use when… |
|
|
17
|
-
|---|---|---|---|
|
|
18
|
-
| **Email** | `IEmailService` | [Email](./Email/README.md) | Sending transactional/notification email (MsGraph, SMTP, SendGrid, OAuth2, File). |
|
|
19
|
-
| **QrGenerator** | `IQRGenerator` | [QrGenerator](./QrGenerator/README.md) | Generating QR-code PNG images from text. |
|
|
20
|
-
| **Jobs** | `IJobScheduler` | [Jobs](./Jobs/README.md) | Background jobs — fire-and-forget, delayed, recurring, with progress + status polling. |
|
|
21
|
-
|
|
22
|
-
## How each component folder is organized
|
|
23
|
-
|
|
24
|
-
Every folder contains two independent, agent-readable files:
|
|
25
|
-
|
|
26
|
-
| File | Purpose | Read it to… |
|
|
27
|
-
|---|---|---|
|
|
28
|
-
| `README.md` | Reference — what the component is, its contract, packages, implementations, file map. | Understand the API surface and the available implementations. |
|
|
29
|
-
| `integration.md` | **Agent runbook** — an ordered workflow that asks the user which implementation(s) to use, then adds the matching code. | Drive the integration interactively, end to end. |
|
|
30
|
-
|
|
31
|
-
## Integration workflow (every component's `integration.md` follows this shape)
|
|
32
|
-
|
|
33
|
-
1. The agent reads `README.md` + `integration.md` and locates the target projects.
|
|
34
|
-
2. The agent presents the component's **implementation catalog** as a selection list and **asks the
|
|
35
|
-
user** which implementation(s) to wire — via a multi-select prompt (`AskUserQuestion`,
|
|
36
|
-
`multiSelect: true`). The user may pick more than one (e.g. a production transport + a local-dev one).
|
|
37
|
-
3. The agent applies the **common steps** once, then the **per-implementation block** for each
|
|
38
|
-
selected implementation, and sets the default provider.
|
|
39
|
-
4. The agent runs the **verification** checklist and reports what was added.
|
|
40
|
-
|
|
41
|
-
## Cross-cutting rules (apply to every component)
|
|
42
|
-
|
|
43
|
-
- **R0.1** — NuGet versions live in `Directory.Packages.props` only (`<PackageVersion>`); project
|
|
44
|
-
files reference the package **without** a `Version=` attribute (see [[dependency-policy]]).
|
|
45
|
-
- **R0.2** — Register the component in the **startup/host project** — the one project that owns
|
|
46
|
-
`Program.cs` + the ASP.NET Core web SDK. Its name is template-dependent: `<App>.Service` (Vertical
|
|
47
|
-
Slice foundation), `<App>.API` (Modular Monolith foundation), and possibly other names as templates
|
|
48
|
-
are added — so **resolve it by locating `Program.cs`, never by assuming a fixed project name**.
|
|
49
|
-
These foundations have **no** separate `…Application` / `…Infrastructure` project. Put the
|
|
50
|
-
`AddXxx(...)` registration in that project's `Extensions/InfrastructureServiceRegistration.cs`
|
|
51
|
-
(`AddInfrastructure`) when that extension exists, otherwise directly in `Program.cs` before
|
|
52
|
-
`builder.Build()`. Never register in a feature slice or domain code (see [[clean-architecture]]).
|
|
53
|
-
- **R0.2a** — *(dual local/Aspire startup)* Some foundations split infrastructure registration across
|
|
54
|
-
**two** methods that `Program.cs` selects at runtime — `if (isAspire) AddAspireInfrastructure(...)`
|
|
55
|
-
(the Aspire path: `Extensions/AspireInfrastructureExtensions.cs`, typically PostgreSQL + Redis)
|
|
56
|
-
`else AddInfrastructure(...)` (the local/dev path:
|
|
57
|
-
`Extensions/InfrastructureServiceRegistration.cs`, typically SQLite + in-memory cache). When
|
|
58
|
-
**both** methods exist, add the component registration to **both**, or the abstraction is
|
|
59
|
-
unresolvable in whichever run mode you skipped (e.g. `IEmailService` throws at runtime under
|
|
60
|
-
Aspire). The registration block is **identical** in both — copy it verbatim — *except*
|
|
61
|
-
provider-specific calls (connection strings, `UseSqlite(...)` vs `UseNpgsql(...)`, cache provider),
|
|
62
|
-
which must match each method's existing provider. If only `AddInfrastructure` (or only `Program.cs`)
|
|
63
|
-
exists, register once as in R0.2.
|
|
64
|
-
- **R0.3** — Inject the **abstraction** (`IEmailService`, `IQRGenerator`, `IJobScheduler`) into
|
|
65
|
-
handlers — never the concrete provider type (see [[cqrs]]).
|
|
66
|
-
- **R0.4** — Always set `options.Provider = "<Key>"` explicitly. Do not rely on the implicit
|
|
67
|
-
"first registered" fallback — it is order-dependent and, for Email, hardcoded.
|
|
68
|
-
- **R0.5** — Secrets (client secrets, API keys, SMTP passwords, connection strings) MUST NOT be
|
|
69
|
-
committed. Use user-secrets / environment variables / key vault (see [[ai-rules]]).
|
|
70
|
-
|
|
71
|
-
## Related
|
|
72
|
-
|
|
73
|
-
- [[folder-structure]] · [[clean-architecture]] · [[cqrs]] · [[vertical-slices]] ·
|
|
74
|
-
[[dependency-policy]] · [[persistence]]
|
|
@@ -1,212 +0,0 @@
|
|
|
1
|
-
# Domain-Driven Design Standards (Tactical Patterns)
|
|
2
|
-
|
|
3
|
-
> Pure technology (domain-agnostic) — generic DDD tactical patterns; the business model, canonical aggregates + invariants are in [[domain-driven-design.domain]]. NON-NEGOTIABLE. Commands: `/aw-spec-specify`, `/aw-spec-plan`, `/aw-spec-tasks`, `/aw-spec-implement`.
|
|
4
|
-
|
|
5
|
-
> Note: this file lives under `domain/` because DDD is a domain-modeling methodology, but the
|
|
6
|
-
> rules here are domain-agnostic and reusable. The business-specific model is in the companion
|
|
7
|
-
> `.domain.md`.
|
|
8
|
-
|
|
9
|
-
## Ubiquitous language & bounded contexts
|
|
10
|
-
|
|
11
|
-
- Use a consistent ubiquitous language across each bounded context.
|
|
12
|
-
- Each bounded context = one module with `Domain / Features / Persistence / Shared`
|
|
13
|
-
(see [[modular-monolith]]).
|
|
14
|
-
|
|
15
|
-
## Aggregate design
|
|
16
|
-
|
|
17
|
-
- Every entity exposed to the Application layer is reached through an **Aggregate Root**.
|
|
18
|
-
- Aggregate roots implement `IAggregateRoot`, `EntityBase<TId>`, and the
|
|
19
|
-
audit/soft-delete/concurrency/domain-event interfaces they need:
|
|
20
|
-
`IFullAuditedObject` or `IAuditableEntityBase`, `ISoftDeletable`, `IConcurrencyTracked`,
|
|
21
|
-
`IHasDomainEvents`, `INamedEntity<TId>` when listable.
|
|
22
|
-
|
|
23
|
-
### Aggregate root responsibilities
|
|
24
|
-
|
|
25
|
-
- Enforce all invariants for the aggregate.
|
|
26
|
-
- Own the lifecycle of its child entities.
|
|
27
|
-
- Raise domain events for state transitions.
|
|
28
|
-
|
|
29
|
-
### Child entity ownership
|
|
30
|
-
|
|
31
|
-
- Child entities implement `IAggregateChild` and expose `GetAggregateRoot()`.
|
|
32
|
-
- They are mutated **only** through methods on the aggregate root.
|
|
33
|
-
|
|
34
|
-
## Transactional / consistency boundaries
|
|
35
|
-
|
|
36
|
-
- **Transaction boundary equals aggregate boundary**: a single command mutates a single
|
|
37
|
-
aggregate per transaction.
|
|
38
|
-
- Cross-aggregate effects flow through **domain events** or **follow-up commands**
|
|
39
|
-
(see [[eventing]], [[cqrs]]).
|
|
40
|
-
|
|
41
|
-
## Strongly typed IDs
|
|
42
|
-
|
|
43
|
-
- IDs are **strongly-typed Vogen value objects** (`<Aggregate>Id` as
|
|
44
|
-
`[ValueObject<Guid>] partial struct`).
|
|
45
|
-
- Raw `Guid` parameters at the Application boundary **MUST be wrapped** at the handler edge.
|
|
46
|
-
|
|
47
|
-
## Value objects
|
|
48
|
-
|
|
49
|
-
- Value Objects are **sealed**, have **private constructors**, **validate in the constructor**,
|
|
50
|
-
throw `DomainException` on invariant violations, and expose a `From(...)` factory.
|
|
51
|
-
|
|
52
|
-
## Domain events
|
|
53
|
-
|
|
54
|
-
- Derive from `DomainEventBase`, are immutable, added inside aggregate methods via
|
|
55
|
-
`AddDomainEvent(...)`, published explicitly by command handlers. See [[eventing]].
|
|
56
|
-
|
|
57
|
-
## Domain services, repositories, specifications
|
|
58
|
-
|
|
59
|
-
- Invariants are enforced **inside the aggregate**; validators check request shape only.
|
|
60
|
-
- Repository usage is limited to `IRepository<T>` / `IReadRepository<T>` + `Specification<T>`.
|
|
61
|
-
- Specification pattern: query composition lives in the Domain. See [[persistence]].
|
|
62
|
-
|
|
63
|
-
## Domain exception handling
|
|
64
|
-
|
|
65
|
-
- Exceptions are thrown by the Domain only (`DomainException`, `InvariantViolationException`).
|
|
66
|
-
Application code returns `Result<T>` (see [[cqrs]]).
|
|
67
|
-
|
|
68
|
-
## Encapsulation, immutability, audit-awareness
|
|
69
|
-
|
|
70
|
-
- **Rich domain model** — no public setters on aggregates, no business logic in handlers.
|
|
71
|
-
- Soft-delete and audit behavior are first-class on protected aggregates
|
|
72
|
-
(see [[persistence]], [[auditing]]).
|
|
73
|
-
|
|
74
|
-
## Anti-patterns (FORBIDDEN)
|
|
75
|
-
|
|
76
|
-
- **Anemic domain models** — no public setters, no "manager/service" classes mutating
|
|
77
|
-
aggregate state from outside, no primitive-typed IDs, no business logic in handlers.
|
|
78
|
-
- Cross-aggregate EF navigation properties — reference by strongly-typed ID only.
|
|
79
|
-
- Cross-bounded-context interaction except via Shared DTOs / `IMediator` / anti-corruption
|
|
80
|
-
adapters.
|
|
81
|
-
|
|
82
|
-
## Canonical example (agent reference — `<SampleProjectName>` sample)
|
|
83
|
-
|
|
84
|
-
Aggregate root with a strongly-typed Vogen ID, owned child collection, concurrency token,
|
|
85
|
-
domain events, and an invariant enforced **inside** the aggregate:
|
|
86
|
-
|
|
87
|
-
```csharp
|
|
88
|
-
// <SampleProjectName>.Service/Projects/Domain/Project.cs
|
|
89
|
-
[ValueObject<Guid>]
|
|
90
|
-
public partial struct ProjectId { }
|
|
91
|
-
|
|
92
|
-
public class Project :
|
|
93
|
-
EntityBase<ProjectId>, IAggregateRoot, IFullAuditedObject, IFieldAudited,
|
|
94
|
-
ISoftDeletable, IConcurrencyTracked, IHasDomainEvents, INamedEntity<ProjectId>
|
|
95
|
-
{
|
|
96
|
-
private readonly List<ProjectTask> _tasks = new();
|
|
97
|
-
|
|
98
|
-
public string Name { get; set; } = default!; // INamedEntity
|
|
99
|
-
public ProjectCode Code { get; private set; } = default!; // value object
|
|
100
|
-
public ProjectStatus Status { get; private set; }
|
|
101
|
-
public TaskUserId? OwnerId { get; private set; } // cross-aggregate ref by ID only
|
|
102
|
-
public IReadOnlyCollection<ProjectTask> Tasks => _tasks.AsReadOnly();
|
|
103
|
-
|
|
104
|
-
public byte[] RowVersion { get; set; } = Array.Empty<byte>(); // IConcurrencyTracked
|
|
105
|
-
|
|
106
|
-
private readonly List<DomainEventBase> _domainEvents = new();
|
|
107
|
-
[NotMapped] public IEnumerable<DomainEventBase> DomainEvents => _domainEvents.AsReadOnly();
|
|
108
|
-
public void ClearDomainEvents() => _domainEvents.Clear();
|
|
109
|
-
|
|
110
|
-
private Project() { } // EF Core ctor
|
|
111
|
-
|
|
112
|
-
public Project(string code, string name, DateOnly startDate, TaskUserId? ownerId)
|
|
113
|
-
{
|
|
114
|
-
Id = ProjectId.From(Guid.NewGuid());
|
|
115
|
-
Code = ProjectCode.From(code); // VO validates in its ctor
|
|
116
|
-
Name = ProjectTitle.From(name).Value;
|
|
117
|
-
Status = ProjectStatus.Planning;
|
|
118
|
-
OwnerId = ownerId;
|
|
119
|
-
AddDomainEvent(new ProjectCreatedEvent(Id.Value, Code.Value, Name));
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
// Invariant lives in the aggregate — never in the handler/validator.
|
|
123
|
-
public ProjectTask AddTask(string title, TaskPriority priority)
|
|
124
|
-
{
|
|
125
|
-
if (Status is ProjectStatus.Cancelled or ProjectStatus.Archived)
|
|
126
|
-
throw new DomainException("Cannot add tasks to a Cancelled or Archived project.");
|
|
127
|
-
var task = new ProjectTask(Id, title, priority);
|
|
128
|
-
_tasks.Add(task);
|
|
129
|
-
AddDomainEvent(new TaskCreatedEvent(Id.Value, task.Id.Value, task.Title.Value));
|
|
130
|
-
return task;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
private void AddDomainEvent(DomainEventBase e) => _domainEvents.Add(e);
|
|
134
|
-
}
|
|
135
|
-
```
|
|
136
|
-
|
|
137
|
-
Value object — sealed, private ctor, validates in the ctor, `From(...)` factory,
|
|
138
|
-
throws `DomainException`:
|
|
139
|
-
|
|
140
|
-
```csharp
|
|
141
|
-
// <SampleProjectName>.Service/Projects/Domain/ProjectCode.cs
|
|
142
|
-
public sealed class ProjectCode : IEquatable<ProjectCode>
|
|
143
|
-
{
|
|
144
|
-
private static readonly Regex _valid = new(@"^[A-Z0-9][A-Z0-9\-]{1,14}$", RegexOptions.Compiled);
|
|
145
|
-
public string Value { get; private set; } = default!;
|
|
146
|
-
|
|
147
|
-
private ProjectCode() { }
|
|
148
|
-
private ProjectCode(string value)
|
|
149
|
-
{
|
|
150
|
-
if (string.IsNullOrWhiteSpace(value))
|
|
151
|
-
throw new DomainException("Project code cannot be empty.");
|
|
152
|
-
var trimmed = value.Trim().ToUpperInvariant();
|
|
153
|
-
if (!_valid.IsMatch(trimmed))
|
|
154
|
-
throw new DomainException("Project code must be uppercase alphanumeric with hyphens.");
|
|
155
|
-
Value = trimmed;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
public static ProjectCode From(string value) => new(value);
|
|
159
|
-
public bool Equals(ProjectCode? o) => o is not null && Value == o.Value;
|
|
160
|
-
public override bool Equals(object? obj) => Equals(obj as ProjectCode);
|
|
161
|
-
public override int GetHashCode() => HashCode.Combine(Value);
|
|
162
|
-
}
|
|
163
|
-
```
|
|
164
|
-
|
|
165
|
-
Child entity — implements `IAggregateChild`, exposes `GetAggregateRoot()`, has only `private`
|
|
166
|
-
setters, and is mutated **only** through methods the parent aggregate calls:
|
|
167
|
-
|
|
168
|
-
```csharp
|
|
169
|
-
// <SampleProjectName>.Service/Projects/Domain/ProjectTask.cs
|
|
170
|
-
[ValueObject<Guid>]
|
|
171
|
-
public partial struct ProjectTaskId { }
|
|
172
|
-
|
|
173
|
-
public class ProjectTask : EntityBase<ProjectTaskId>, IAggregateChild
|
|
174
|
-
{
|
|
175
|
-
public ProjectId ProjectId { get; private set; } // FK back to the root
|
|
176
|
-
public TaskTitle Title { get; private set; } = default!; // value object
|
|
177
|
-
public ProjectTaskStatus Status { get; private set; }
|
|
178
|
-
public TaskUserId? AssigneeId { get; private set; } // cross-aggregate ref by ID
|
|
179
|
-
|
|
180
|
-
public Project Project { get; private set; } = default!; // EF-only nav, private setter
|
|
181
|
-
public object GetAggregateRoot() => Project; // IAggregateChild
|
|
182
|
-
|
|
183
|
-
private ProjectTask() { } // EF Core ctor
|
|
184
|
-
public ProjectTask(ProjectId projectId, string title, TaskPriority priority)
|
|
185
|
-
{
|
|
186
|
-
Id = ProjectTaskId.From(Guid.NewGuid());
|
|
187
|
-
ProjectId = projectId;
|
|
188
|
-
Title = TaskTitle.From(title);
|
|
189
|
-
Status = ProjectTaskStatus.Open;
|
|
190
|
-
Priority = priority;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
// State-machine invariant lives on the child; the parent calls it (project.ChangeTaskStatus).
|
|
194
|
-
public void ChangeStatus(ProjectTaskStatus newStatus)
|
|
195
|
-
{
|
|
196
|
-
if (Status == ProjectTaskStatus.Cancelled)
|
|
197
|
-
throw new DomainException("A cancelled task cannot be re-opened.");
|
|
198
|
-
Status = newStatus;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
public void AssignTo(TaskUserId assigneeId) => AssigneeId = assigneeId;
|
|
202
|
-
}
|
|
203
|
-
```
|
|
204
|
-
|
|
205
|
-
> The aggregate root mutates children only through its own methods (e.g. `Project.AssignTask`
|
|
206
|
-
> calls `task.AssignTo(...)` and raises the domain event) — callers never touch `ProjectTask`
|
|
207
|
-
> directly. EF mapping for the owned collection is in [[persistence]].
|
|
208
|
-
|
|
209
|
-
## Related
|
|
210
|
-
|
|
211
|
-
- [[modular-monolith]] · [[clean-architecture]] · [[cqrs]] · [[persistence]] ·
|
|
212
|
-
[[eventing]] · [[domain-driven-design.domain]]
|