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,300 +0,0 @@
|
|
|
1
|
-
# Email 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 `IEmailService` into a
|
|
5
|
-
> project, asking the user which transport implementation(s) to wire. Read [README.md](./README.md)
|
|
6
|
-
> first for the contract. Replace `<SampleProjectName>` with the project name.
|
|
7
|
-
> Commands: `/aw-spec-implement`.
|
|
8
|
-
|
|
9
|
-
---
|
|
10
|
-
|
|
11
|
-
## Agent workflow — execute in order
|
|
12
|
-
|
|
13
|
-
1. **Load context.** Read [README.md](./README.md) (contract + provider matrix) and this file.
|
|
14
|
-
2. **Resolve the host project** `<HostProject>` (see [[folder-structure]], [[components-readme]] R0.2):
|
|
15
|
-
the single startup project that owns **`Program.cs`** + the ASP.NET Core web SDK —
|
|
16
|
-
`<SampleProjectName>.Service` (Vertical Slice foundation), `<SampleProjectName>.API` (Modular
|
|
17
|
-
Monolith foundation), or another name in a future template. **Find it by locating `Program.cs`, not
|
|
18
|
-
by assuming the name.** In these foundations application/domain/infrastructure code all live in this
|
|
19
|
-
one project (modules are folders); there is **no** separate `…Application` / `…Infrastructure`
|
|
20
|
-
project. `<HostProject>` is the target for C2–C4 below.
|
|
21
|
-
3. **Present the implementation catalog (below) as a selection list and ASK the user which
|
|
22
|
-
implementation(s) to integrate.** Use an interactive multi-select prompt (`AskUserQuestion`,
|
|
23
|
-
`multiSelect: true`). The list options are the rows of the *Implementation catalog*.
|
|
24
|
-
- The user MAY select **more than one** (e.g., `MsGraph` for production + `File` for local dev).
|
|
25
|
-
- If more than one is selected, **ask a follow-up single-select**: *which one is the default
|
|
26
|
-
(active) provider?* — this becomes `options.Provider`.
|
|
27
|
-
- If the user selects exactly one, that one is the default.
|
|
28
|
-
4. **Apply “Common steps” once** (§Common steps) — abstraction package + registration scaffold.
|
|
29
|
-
5. **For each selected implementation, apply its block** under §Implementation blocks — add its
|
|
30
|
-
package, its `Use…` call inside the `AddEmailMessaging` builder, and its config section.
|
|
31
|
-
6. **Set the default provider:** `configureOptions: options => options.Provider = "<default key>"`.
|
|
32
|
-
All selected implementations stay registered; the active one is switchable later via config.
|
|
33
|
-
7. **(Optional) Wire a usage example** in the relevant feature handler (§Usage).
|
|
34
|
-
8. **Offer the bundled sample endpoint (ASK).** After the wiring above, ask the user (single-select
|
|
35
|
-
yes/no via `AskUserQuestion`) whether to add the ready-made sample endpoint. If **yes**, copy it
|
|
36
|
-
into the host project — see §Sample reference endpoint. If **no**, skip silently.
|
|
37
|
-
9. **Verify** (§Verification) and report what was added.
|
|
38
|
-
|
|
39
|
-
> **Guardrail — always set `options.Provider`.** When `Provider` is null the facade falls back to a
|
|
40
|
-
> **hardcoded `"SendGrid"`** key; if SendGrid is not registered, resolution throws at runtime. Never
|
|
41
|
-
> rely on the implicit fallback.
|
|
42
|
-
|
|
43
|
-
---
|
|
44
|
-
|
|
45
|
-
## Implementation catalog (selection list)
|
|
46
|
-
|
|
47
|
-
Present these as the multi-select options. `Key` is the provider key and the `Use…` method suffix.
|
|
48
|
-
|
|
49
|
-
| Key | Implementation | Best for | Package (v) | Required settings |
|
|
50
|
-
|---|---|---|---|---|
|
|
51
|
-
| `MsGraph` | Microsoft 365 / Graph API | Production on an O365 tenant | `AppWeaver.Messaging.Email.MsGraph` (0.1.2) | `TenantId`, `ClientId`, `ClientSecret`, `DefaultFrom` |
|
|
52
|
-
| `Smtp` | Classic SMTP | Any SMTP server | `AppWeaver.Messaging.Email.Smtp` (0.1.3) | `Host`, `Port`, `Username`, `Password`, `EnableSsl` |
|
|
53
|
-
| `SendGrid` | SendGrid API | SendGrid accounts | `AppWeaver.Messaging.Email.SendGrid` (0.1.2) | `ApiKey` *(required)*, `DefaultFrom` |
|
|
54
|
-
| `OAuth2` | OAuth2-authenticated SMTP | O365 SMTP with OAuth2 | `AppWeaver.Messaging.Email.OAuth2` (0.1.3) | `TenantId`, `ClientId`, `ClientSecret`, `Host`, `Port` |
|
|
55
|
-
| `File` | Writes email to a local `.txt` | Local dev / tests (no real send) | `AppWeaver.Messaging.Email.File` (0.1.2) | `DefaultFrom` |
|
|
56
|
-
|
|
57
|
-
> Recommended pairing: **`MsGraph` (default) + `File`** so developers can run locally without
|
|
58
|
-
> sending real mail, while production uses Graph.
|
|
59
|
-
|
|
60
|
-
---
|
|
61
|
-
|
|
62
|
-
## Common steps (apply once, regardless of selection)
|
|
63
|
-
|
|
64
|
-
### C1 — Abstraction package version
|
|
65
|
-
|
|
66
|
-
**File:** `Directory.Packages.props` (versions live here only — see [[dependency-policy]])
|
|
67
|
-
|
|
68
|
-
```xml
|
|
69
|
-
<ItemGroup>
|
|
70
|
-
<!-- AppWeaver — Email (abstraction) -->
|
|
71
|
-
<PackageVersion Include="AppWeaver.Messaging.Email" Version="0.1.3" />
|
|
72
|
-
<!-- + one <PackageVersion> per selected implementation — see each block below -->
|
|
73
|
-
</ItemGroup>
|
|
74
|
-
```
|
|
75
|
-
|
|
76
|
-
### C2 — Project references
|
|
77
|
-
|
|
78
|
-
**File:** `src/<HostProject>/<HostProject>.csproj` (the host project resolved in step 2 — single
|
|
79
|
-
project, no separate Application/Infrastructure). Add the abstraction **and** every selected
|
|
80
|
-
implementation's package here (no `Version=` — versions live in `Directory.Packages.props`, C1).
|
|
81
|
-
|
|
82
|
-
```xml
|
|
83
|
-
<ItemGroup>
|
|
84
|
-
<PackageReference Include="AppWeaver.Messaging.Email" /> <!-- no Version= -->
|
|
85
|
-
<!-- + each selected implementation’s package — see its block below -->
|
|
86
|
-
</ItemGroup>
|
|
87
|
-
```
|
|
88
|
-
|
|
89
|
-
### C3 — Registration scaffold
|
|
90
|
-
|
|
91
|
-
**File:** the host project `<HostProject>` (step 2) — add the registration in its
|
|
92
|
-
`Extensions/InfrastructureServiceRegistration.cs` (`AddInfrastructure`) when that file exists (it does
|
|
93
|
-
in the Vertical Slice / Modular Monolith foundations), otherwise directly in `Program.cs` **before**
|
|
94
|
-
`builder.Build()`. (`configuration` below = the `IConfiguration` in scope: `config` inside
|
|
95
|
-
`AddInfrastructure`, `builder.Configuration` in `Program.cs`.)
|
|
96
|
-
|
|
97
|
-
Add the base call; each selected implementation contributes one `builder.Use…(…)` line (C4).
|
|
98
|
-
|
|
99
|
-
```csharp
|
|
100
|
-
using AppWeaver.Messaging.Email.Extensions;
|
|
101
|
-
// + one using per selected implementation (see its block)
|
|
102
|
-
|
|
103
|
-
services.AddEmailMessaging(
|
|
104
|
-
configureBuilder: builder =>
|
|
105
|
-
{
|
|
106
|
-
// ⬇ one Use…() call per selected implementation is inserted here
|
|
107
|
-
},
|
|
108
|
-
configureOptions: options => options.Provider = "<default key>"); // step 6
|
|
109
|
-
```
|
|
110
|
-
|
|
111
|
-
> **Dual local/Aspire startup ([[components-readme]] R0.2a).** If the host project also has an
|
|
112
|
-
> `AddAspireInfrastructure` method (`Extensions/AspireInfrastructureExtensions.cs`) selected by
|
|
113
|
-
> `if (isAspire) … else …` in `Program.cs`, add this **same** `AddEmailMessaging(...)` block to
|
|
114
|
-
> **both** `AddInfrastructure` and `AddAspireInfrastructure`. The Email registration is DB-agnostic, so
|
|
115
|
-
> it is identical in both methods — copy it verbatim. Registering in only one leaves `IEmailService`
|
|
116
|
-
> unresolvable (throws at runtime) in the other run mode.
|
|
117
|
-
|
|
118
|
-
### C4 — Base appsettings block
|
|
119
|
-
|
|
120
|
-
**File:** `src/<HostProject>/appsettings.json` (the host project resolved in step 2)
|
|
121
|
-
|
|
122
|
-
```jsonc
|
|
123
|
-
{
|
|
124
|
-
"Messaging": {
|
|
125
|
-
"Email": {
|
|
126
|
-
"DefaultFrom": "no-reply@your-domain.com",
|
|
127
|
-
"AdminEmail": "admin@your-domain.com"
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
```
|
|
132
|
-
|
|
133
|
-
---
|
|
134
|
-
|
|
135
|
-
## Implementation blocks
|
|
136
|
-
|
|
137
|
-
For each implementation the user selected, do all three sub-steps: **package**, **registration
|
|
138
|
-
line**, **config**. Secrets MUST be supplied via user-secrets / env / key vault — never committed.
|
|
139
|
-
|
|
140
|
-
### MsGraph
|
|
141
|
-
```xml
|
|
142
|
-
<!-- Directory.Packages.props --> <PackageVersion Include="AppWeaver.Messaging.Email.MsGraph" Version="0.1.2" />
|
|
143
|
-
<!-- host .csproj --> <PackageReference Include="AppWeaver.Messaging.Email.MsGraph" />
|
|
144
|
-
```
|
|
145
|
-
```csharp
|
|
146
|
-
using AppWeaver.Messaging.Email.MsGraph.Extensions;
|
|
147
|
-
// inside AddEmailMessaging builder:
|
|
148
|
-
builder.UseMsGraph(o =>
|
|
149
|
-
{
|
|
150
|
-
o.TenantId = configuration["Messaging.Email.MsGraph:TenantId"];
|
|
151
|
-
o.ClientId = configuration["Messaging.Email.MsGraph:ClientId"];
|
|
152
|
-
o.ClientSecret = configuration["Messaging.Email.MsGraph:ClientSecret"]; // user-secrets
|
|
153
|
-
o.DefaultFrom = configuration["Messaging:Email:DefaultFrom"];
|
|
154
|
-
});
|
|
155
|
-
```
|
|
156
|
-
```jsonc
|
|
157
|
-
// appsettings.json — secret set out-of-band:
|
|
158
|
-
// dotnet user-secrets set "Messaging.Email.MsGraph:ClientSecret" "<secret>"
|
|
159
|
-
"Messaging.Email.MsGraph": { "TenantId": "<guid>", "ClientId": "<guid>", "DefaultFrom": "no-reply@your-domain.com" }
|
|
160
|
-
```
|
|
161
|
-
|
|
162
|
-
### Smtp
|
|
163
|
-
```xml
|
|
164
|
-
<PackageVersion Include="AppWeaver.Messaging.Email.Smtp" Version="0.1.3" />
|
|
165
|
-
<PackageReference Include="AppWeaver.Messaging.Email.Smtp" />
|
|
166
|
-
```
|
|
167
|
-
```csharp
|
|
168
|
-
using AppWeaver.Messaging.Email.Smtp.Extensions;
|
|
169
|
-
builder.UseSmtp(o =>
|
|
170
|
-
{
|
|
171
|
-
o.Host = configuration["Messaging.Email.Smtp:Host"]!;
|
|
172
|
-
o.Port = int.Parse(configuration["Messaging.Email.Smtp:Port"] ?? "587");
|
|
173
|
-
o.Username = configuration["Messaging.Email.Smtp:Username"]!;
|
|
174
|
-
o.Password = configuration["Messaging.Email.Smtp:Password"]!; // user-secrets
|
|
175
|
-
o.EnableSsl = true;
|
|
176
|
-
o.DefaultFrom = configuration["Messaging:Email:DefaultFrom"];
|
|
177
|
-
});
|
|
178
|
-
```
|
|
179
|
-
```jsonc
|
|
180
|
-
"Messaging.Email.Smtp": { "Host": "smtp.example.com", "Port": 587, "Username": "<user>", "EnableSsl": true }
|
|
181
|
-
```
|
|
182
|
-
> Note: the `SendAsync(from,to,…)` convenience overload of the SMTP service writes to a file; the
|
|
183
|
-
> `EmailMessage` overload performs the real SMTP send. Use `SendAsync(EmailMessage, ct)` for delivery.
|
|
184
|
-
|
|
185
|
-
### SendGrid
|
|
186
|
-
```xml
|
|
187
|
-
<PackageVersion Include="AppWeaver.Messaging.Email.SendGrid" Version="0.1.2" />
|
|
188
|
-
<PackageReference Include="AppWeaver.Messaging.Email.SendGrid" />
|
|
189
|
-
```
|
|
190
|
-
```csharp
|
|
191
|
-
using AppWeaver.Messaging.Email.SendGrid.Extensions;
|
|
192
|
-
builder.UseSendGrid(o =>
|
|
193
|
-
{
|
|
194
|
-
o.ApiKey = configuration["Messaging.Email.SendGrid:ApiKey"]!; // [Required] — user-secrets
|
|
195
|
-
o.DefaultFrom = configuration["Messaging:Email:DefaultFrom"];
|
|
196
|
-
});
|
|
197
|
-
```
|
|
198
|
-
> `ApiKey` is `[Required]` and validates on startup — a missing key fails app boot, not the first send.
|
|
199
|
-
|
|
200
|
-
### OAuth2
|
|
201
|
-
```xml
|
|
202
|
-
<PackageVersion Include="AppWeaver.Messaging.Email.OAuth2" Version="0.1.3" />
|
|
203
|
-
<PackageReference Include="AppWeaver.Messaging.Email.OAuth2" />
|
|
204
|
-
```
|
|
205
|
-
```csharp
|
|
206
|
-
using AppWeaver.Messaging.Email.OAuth2.Extensions;
|
|
207
|
-
builder.UseOAuth2(o =>
|
|
208
|
-
{
|
|
209
|
-
o.TenantId = configuration["Messaging.Email.OAuth2:TenantId"];
|
|
210
|
-
o.ClientId = configuration["Messaging.Email.OAuth2:ClientId"];
|
|
211
|
-
o.ClientSecret = configuration["Messaging.Email.OAuth2:ClientSecret"]; // user-secrets
|
|
212
|
-
o.Host = "smtp.office365.com"; o.Port = 587;
|
|
213
|
-
o.DefaultFrom = configuration["Messaging:Email:DefaultFrom"];
|
|
214
|
-
});
|
|
215
|
-
```
|
|
216
|
-
|
|
217
|
-
### File
|
|
218
|
-
```xml
|
|
219
|
-
<PackageVersion Include="AppWeaver.Messaging.Email.File" Version="0.1.2" />
|
|
220
|
-
<PackageReference Include="AppWeaver.Messaging.Email.File" />
|
|
221
|
-
```
|
|
222
|
-
```csharp
|
|
223
|
-
using AppWeaver.Messaging.Email.File.Extensions;
|
|
224
|
-
builder.UseFile(o => o.DefaultFrom = configuration["Messaging:Email:DefaultFrom"]);
|
|
225
|
-
```
|
|
226
|
-
> Pass `DefaultFrom` **inline** as shown. The File provider’s config binder reads the
|
|
227
|
-
> `Messaging.Email.Smtp` section (a known quirk), so do not rely on a `Messaging.Email.File` config
|
|
228
|
-
> section. Output is written to `Emails_<yyyy-MM-dd>.txt` in `AppContext.BaseDirectory`.
|
|
229
|
-
|
|
230
|
-
---
|
|
231
|
-
|
|
232
|
-
## Usage (inject the abstraction in a feature handler)
|
|
233
|
-
|
|
234
|
-
Email is a side effect of a business operation — call it from the Application command handler that
|
|
235
|
-
owns the operation (see [[cqrs]], [[vertical-slices]]). Inject **`IEmailService`**, never a provider.
|
|
236
|
-
|
|
237
|
-
```csharp
|
|
238
|
-
using AppWeaver.Messaging.Email;
|
|
239
|
-
|
|
240
|
-
public class CreateUserCommandHandler(IEmailService _email, IConfiguration _config /* … */)
|
|
241
|
-
: ICommandHandler<CreateUserCommand, Result<UserDto>>
|
|
242
|
-
{
|
|
243
|
-
public async ValueTask<Result<UserDto>> Handle(CreateUserCommand req, CancellationToken ct)
|
|
244
|
-
{
|
|
245
|
-
// … persist aggregate …
|
|
246
|
-
var from = _config["Messaging:Email:DefaultFrom"]!;
|
|
247
|
-
var to = _config["Messaging:Email:AdminEmail"]!;
|
|
248
|
-
await _email.SendAsync(from, to, $"New user: {req.Name}", "<p>Created.</p>", ct: ct);
|
|
249
|
-
return Result<UserDto>.Ok(/* dto */);
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
```
|
|
253
|
-
|
|
254
|
-
Multi-recipient / attachments — use the `EmailMessage` overload (To/Cc/Bcc + `EmailAttachment`,
|
|
255
|
-
see [README.md](./README.md)).
|
|
256
|
-
|
|
257
|
-
---
|
|
258
|
-
|
|
259
|
-
## Sample reference endpoint
|
|
260
|
-
|
|
261
|
-
A ready-to-run FastEndpoint is bundled in this component folder at
|
|
262
|
-
[`ReferenceEndpoints/SendEmailEndpoint.cs`](./ReferenceEndpoints/SendEmailEndpoint.cs). It injects
|
|
263
|
-
`IEmailService` directly and performs the **full send inline** — no command/handler/mediator — purely
|
|
264
|
-
as a copy-paste sample. It is **not** wired automatically; it is offered in workflow step 8.
|
|
265
|
-
|
|
266
|
-
**When the user opts in (step 8) — copy procedure:**
|
|
267
|
-
|
|
268
|
-
1. Confirm `docs/components/Email/ReferenceEndpoints/SendEmailEndpoint.cs` exists. If it is missing,
|
|
269
|
-
tell the user and skip (do not hand-write a replacement).
|
|
270
|
-
2. Create `src/<HostProject>/ReferenceEndpoints/` if it does not exist (`<HostProject>` = the project
|
|
271
|
-
resolved in step 2).
|
|
272
|
-
3. Copy the file to `src/<HostProject>/ReferenceEndpoints/SendEmailEndpoint.cs`, replacing the
|
|
273
|
-
namespace token `__HOSTPROJECT__` with `<HostProject>`'s root namespace (e.g.
|
|
274
|
-
`AppWeaver.Health.Service`). Change nothing else.
|
|
275
|
-
4. Do **not** touch the C1–C4 wiring. FastEndpoints auto-discovers the endpoint by assembly scan, so
|
|
276
|
-
no extra registration is required.
|
|
277
|
-
|
|
278
|
-
**Endpoint:** `POST /reference/email/send` — body `{ "to", "subject", "body", "isHtml"?, "from"? }`.
|
|
279
|
-
`from` falls back to `Messaging:Email:DefaultFrom`. With the `File` provider the message is written
|
|
280
|
-
to `Emails_<yyyy-MM-dd>.txt` in the app base directory (no real mail is sent).
|
|
281
|
-
|
|
282
|
-
---
|
|
283
|
-
|
|
284
|
-
## Verification
|
|
285
|
-
|
|
286
|
-
1. **Builds** — `dotnet build` succeeds; no `Version=` on package references.
|
|
287
|
-
2. **Startup validates** — app boots; options `ValidateOnStart()` passes; `options.Provider` equals
|
|
288
|
-
one of the selected keys.
|
|
289
|
-
3. **Resolves** — `IEmailService` resolves from DI.
|
|
290
|
-
4. **Sends** — a send succeeds; for `File`, confirm `Emails_<yyyy-MM-dd>.txt` appears in the app
|
|
291
|
-
base directory.
|
|
292
|
-
5. **No secrets in diff** — `git diff` shows no secret / API key / password literals.
|
|
293
|
-
6. **Sample endpoint (only if copied in step 8)** — the build still succeeds with
|
|
294
|
-
`ReferenceEndpoints/SendEmailEndpoint.cs` added, the `__HOSTPROJECT__` token was replaced, and
|
|
295
|
-
`POST /reference/email/send` is discoverable.
|
|
296
|
-
|
|
297
|
-
## Related
|
|
298
|
-
|
|
299
|
-
- [README.md](./README.md) · [[components-readme]] · [[cqrs]] · [[clean-architecture]] ·
|
|
300
|
-
[[vertical-slices]] · [[dependency-policy]] · [[ai-rules]]
|
|
@@ -1,113 +0,0 @@
|
|
|
1
|
-
# Jobs Component (`AppWeaver.Jobs`)
|
|
2
|
-
|
|
3
|
-
> Pure technology (domain-agnostic). Reference sheet for the background-job 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 plug-and-play background-job framework. Application code depends only on the `IJobScheduler`
|
|
10
|
-
and `IJobProgress` abstractions; the engine (Quartz.NET, optionally with EF Core status
|
|
11
|
-
persistence) is a single registration decision. Supports **fire-and-forget**, **delayed**, and
|
|
12
|
-
**recurring (CRON)** jobs with progress tracking, status polling, and cooperative cancellation.
|
|
13
|
-
|
|
14
|
-
**Use when:** work must run outside the request thread — exports, batch processing, archival,
|
|
15
|
-
report generation, scheduled maintenance, sending bulk notifications.
|
|
16
|
-
|
|
17
|
-
## Public contract (`AppWeaver.Jobs.Abstractions`)
|
|
18
|
-
|
|
19
|
-
```csharp
|
|
20
|
-
public interface IJob { } // marker — implement on every job class
|
|
21
|
-
|
|
22
|
-
public interface IJobScheduler
|
|
23
|
-
{
|
|
24
|
-
Task<string> EnqueueAsync<TJob>(Expression<Func<TJob, Task>> jobMethod,
|
|
25
|
-
string? description = null, string? userId = null) where TJob : class, IJob;
|
|
26
|
-
|
|
27
|
-
Task<string> ScheduleAsync<TJob>(Expression<Func<TJob, Task>> jobMethod, TimeSpan delay,
|
|
28
|
-
string? description = null, string? userId = null) where TJob : class, IJob;
|
|
29
|
-
|
|
30
|
-
Task AddOrUpdateRecurringAsync<TJob>(string jobId,
|
|
31
|
-
Expression<Func<TJob, Task>> jobMethod, string cronExpression) where TJob : class, IJob;
|
|
32
|
-
|
|
33
|
-
Task RemoveRecurringAsync(string jobId);
|
|
34
|
-
Task<JobStatus?> GetStatusAsync(string jobId);
|
|
35
|
-
Task<IEnumerable<JobStatus>> GetStatusesAsync(string userId, DateTime? from = null, DateTime? to = null);
|
|
36
|
-
Task CancelAsync(string jobId);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
public interface IJobProgress
|
|
40
|
-
{
|
|
41
|
-
Task ReportProgressAsync(int percent, string? message = null, object? progressDetails = null);
|
|
42
|
-
Task ReportResultUrlAsync(string url);
|
|
43
|
-
CancellationToken CancellationToken { get; } // check inside loops for cooperative cancel
|
|
44
|
-
}
|
|
45
|
-
```
|
|
46
|
-
|
|
47
|
-
`JobStatus` carries: `JobId`, `State`, `CreatedAt`, `StartedAt`, `CompletedAt`, `ProgressPercent`,
|
|
48
|
-
`ProgressMessage`, `ResultUrl`, `ErrorMessage`, `Description`, `UserId`, `ProgressDetails`.
|
|
49
|
-
`JobState` = `Pending → Running → Completed | Failed | Cancelled`.
|
|
50
|
-
|
|
51
|
-
## Packages
|
|
52
|
-
|
|
53
|
-
| Package | Version | Role | Builder method |
|
|
54
|
-
|---|---|---|---|
|
|
55
|
-
| `AppWeaver.Jobs` | `0.1.1` | **Required.** Abstractions + `AddAppWeaverJobs` + facade. | — |
|
|
56
|
-
| `AppWeaver.Jobs.Quartz` | `0.1.0` | **Recommended engine.** Quartz.NET scheduler; in-memory or persistent store. | `UseQuartzJobScheduler` |
|
|
57
|
-
| `AppWeaver.Jobs.Persistence.EfCore` | `0.1.0` | EF Core persistence of `JobStatus`. | `UseEfCoreJobs` + `UseJobDatabase` / `UseAppWeaverDataStore` |
|
|
58
|
-
| `AppWeaver.Jobs.InMemory` | `0.1.0` | ⚠️ **Work in progress — not functional. Do not use.** | — |
|
|
59
|
-
|
|
60
|
-
**Recommended stack:** `AppWeaver.Jobs` + `AppWeaver.Jobs.Quartz` + `AppWeaver.Jobs.Persistence.EfCore`.
|
|
61
|
-
|
|
62
|
-
## Registration shapes
|
|
63
|
-
|
|
64
|
-
```csharp
|
|
65
|
-
// In AppWeaver.Jobs.Extensions
|
|
66
|
-
IComponentBuilder<IJobScheduler> AddAppWeaverJobs(
|
|
67
|
-
this IServiceCollection services,
|
|
68
|
-
Action<IComponentBuilder<IJobScheduler>> configureBuilder, // required
|
|
69
|
-
Action<JobsConfiguration>? configureOptions = null, // set Provider = "Quartz"
|
|
70
|
-
IConfiguration? configuration = null); // OR bind "AppWeaver:Jobs"
|
|
71
|
-
|
|
72
|
-
// In AppWeaver.Jobs.Quartz.Extensions
|
|
73
|
-
builder.UseQuartzJobScheduler(opts => { opts.MaxConcurrency = 10; opts.ConfigureQuartz = q => { … }; });
|
|
74
|
-
|
|
75
|
-
// In AppWeaver.Jobs.Persistence.EfCore.Extensions (choose ONE store wiring)
|
|
76
|
-
builder.UseEfCoreJobs().UseJobDatabase(opts => opts.UseSqlite("Data Source=jobs.db")); // standalone
|
|
77
|
-
builder.UseEfCoreJobs().UseAppWeaverDataStore(); // shared DataStore
|
|
78
|
-
```
|
|
79
|
-
|
|
80
|
-
`JobsConfiguration.Provider` (config section `AppWeaver:Jobs`) selects the engine key; for Quartz
|
|
81
|
-
it is `"Quartz"`. `UseQuartzJobScheduler` registers `IJobProgress` (`QuartzJobProgress`) and a
|
|
82
|
-
Quartz hosted service automatically.
|
|
83
|
-
|
|
84
|
-
## Persistence modes (EF Core)
|
|
85
|
-
|
|
86
|
-
| Mode | Wiring | DbContext | Migration |
|
|
87
|
-
|---|---|---|---|
|
|
88
|
-
| **Standalone** | `.UseJobDatabase(opts => opts.UseXxx(conn))` | dedicated `JobStatusDbContext` | own migration / `EnsureCreated` |
|
|
89
|
-
| **Shared DataStore** | `.UseAppWeaverDataStore()` (after `AddDataStore().UseEfCoreDataStore()`) | shares DataStore connection | Shared: in `SharedComponentDbContext` migration; Isolated: own migration |
|
|
90
|
-
|
|
91
|
-
The Quartz **persistent store** (for distributed/durable scheduling) is configured separately via
|
|
92
|
-
`opts.ConfigureQuartz = q => q.UsePersistentStore(...)` and needs the Quartz DDL applied to the DB.
|
|
93
|
-
With no `ConfigureQuartz`, Quartz uses an in-memory store (single instance, lost on restart).
|
|
94
|
-
|
|
95
|
-
## Source map (framework)
|
|
96
|
-
|
|
97
|
-
```
|
|
98
|
-
framework/Jobs/
|
|
99
|
-
README.md ← full component manual (authoritative deep-dive)
|
|
100
|
-
src/
|
|
101
|
-
AppWeaver.Jobs/ ← IJob, IJobScheduler, IJobProgress, JobStatus, AddAppWeaverJobs
|
|
102
|
-
AppWeaver.Jobs.Quartz/ ← UseQuartzJobScheduler + QuartzJobScheduler/Wrapper/Progress
|
|
103
|
-
AppWeaver.Jobs.Persistence.EfCore/ ← UseEfCoreJobs/UseJobDatabase/UseAppWeaverDataStore
|
|
104
|
-
AppWeaver.Jobs.InMemory/ ← ⚠️ WIP, not functional
|
|
105
|
-
tests/
|
|
106
|
-
AppWeaver.Jobs.Tests/ ← DefaultJobSchedulerTests
|
|
107
|
-
TestApp/ ← Quartz + EF Core end-to-end scenarios (A–O)
|
|
108
|
-
```
|
|
109
|
-
|
|
110
|
-
## Related
|
|
111
|
-
|
|
112
|
-
- [integration.md](./integration.md) · [[components-readme]] · [[cqrs]] ·
|
|
113
|
-
[[persistence]] · [[clean-architecture]] · [[dependency-policy]]
|
|
@@ -1,259 +0,0 @@
|
|
|
1
|
-
# Jobs 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 `IJobScheduler` /
|
|
5
|
-
> `IJobProgress` into a project, asking the user which engine + persistence implementation(s) to
|
|
6
|
-
> wire. Read [README.md](./README.md) first for the contract. Replace `<SampleProjectName>` with
|
|
7
|
-
> the project name. Commands: `/aw-spec-implement`.
|
|
8
|
-
|
|
9
|
-
---
|
|
10
|
-
|
|
11
|
-
## Agent workflow — execute in order
|
|
12
|
-
|
|
13
|
-
1. **Load context.** Read [README.md](./README.md) and this file. The functional engine is
|
|
14
|
-
**Quartz** — `AppWeaver.Jobs` + `AppWeaver.Jobs.Quartz` are always installed.
|
|
15
|
-
2. **Resolve the host project** `<HostProject>` (see [[folder-structure]], [[components-readme]] R0.2):
|
|
16
|
-
the single startup project that owns **`Program.cs`** + the ASP.NET Core web SDK —
|
|
17
|
-
`<SampleProjectName>.Service` (Vertical Slice foundation), `<SampleProjectName>.API` (Modular
|
|
18
|
-
Monolith foundation), or another name in a future template. **Find it by locating `Program.cs`, not
|
|
19
|
-
by assuming the name.** In these foundations job classes, handlers, registration, `DbContext`, and
|
|
20
|
-
`appsettings.json` all live in this one project (modules are folders) — there is **no** separate
|
|
21
|
-
`…Application` / `…Infrastructure` project. `<HostProject>` is the target for the steps below.
|
|
22
|
-
3. **Present the implementation catalog (below) as a selection list and ASK the user which
|
|
23
|
-
implementation(s) to integrate.** Use a multi-select prompt (`AskUserQuestion`,
|
|
24
|
-
`multiSelect: true`). Enforce the grouping rules when reading the answer:
|
|
25
|
-
- **Group A — Scheduler store (pick exactly one; default `Quartz.InMemoryStore`).**
|
|
26
|
-
- **Group B — Status persistence (pick zero or one; recommended `EfCore.JobDatabase`).**
|
|
27
|
-
- A valid selection is one Group A item, optionally plus one Group B item
|
|
28
|
-
(e.g. `Quartz.PersistentStore` + `EfCore.DataStore`).
|
|
29
|
-
4. **Apply “Common steps” once** (§Common steps) — abstraction + Quartz + base registration.
|
|
30
|
-
5. **For each selected implementation, apply its block** (§Implementation blocks) — Group A tunes
|
|
31
|
-
`UseQuartzJobScheduler`; Group B chains `.UseEfCoreJobs().UseXxx(...)`.
|
|
32
|
-
6. **Create the DB schema** if a Group B (or persistent Group A) implementation was selected
|
|
33
|
-
(§Schema).
|
|
34
|
-
7. **Author job classes + dispatch/poll** in the feature slice (§Usage); register each job `Scoped`.
|
|
35
|
-
8. **Verify** (§Verification) and report what was added.
|
|
36
|
-
|
|
37
|
-
> **Guardrails.** Do **not** use `AppWeaver.Jobs.InMemory` (work-in-progress, not functional). Always
|
|
38
|
-
> set `options.Provider = "Quartz"`. Long-running jobs must check `IJobProgress.CancellationToken`
|
|
39
|
-
> (cancellation is cooperative). In a Quartz **persistent** store, do not rename a job class/method
|
|
40
|
-
> after jobs are scheduled — names are serialized as strings; version instead (`…JobV2`).
|
|
41
|
-
|
|
42
|
-
---
|
|
43
|
-
|
|
44
|
-
## Implementation catalog (selection list)
|
|
45
|
-
|
|
46
|
-
| Key | Group | Implementation | Best for | Extra package (v) |
|
|
47
|
-
|---|---|---|---|---|
|
|
48
|
-
| `Quartz.InMemoryStore` | A · Scheduler store | Quartz with in-memory store | Single instance; schedule may be lost on restart | — (Quartz only) |
|
|
49
|
-
| `Quartz.PersistentStore` | A · Scheduler store | Quartz with persistent store (Postgres/SqlServer/SQLite) | Durable + multi-instance scheduling | — (Quartz only; needs DB + DDL) |
|
|
50
|
-
| `EfCore.JobDatabase` | B · Status persistence | `JobStatus` in a dedicated EF Core DB | Standalone apps; status survives restart | `AppWeaver.Jobs.Persistence.EfCore` (0.1.0) |
|
|
51
|
-
| `EfCore.DataStore` | B · Status persistence | `JobStatus` in the shared AppWeaver DataStore | Apps already using `AppWeaver.DataStore` | `AppWeaver.Jobs.Persistence.EfCore` (0.1.0) |
|
|
52
|
-
|
|
53
|
-
> Recommended production set: **`Quartz.PersistentStore` + `EfCore.JobDatabase`** (or `EfCore.DataStore`
|
|
54
|
-
> when DataStore is present). Quartz store = durable *schedule*; EF Core store = durable *status*;
|
|
55
|
-
> they are independent concerns.
|
|
56
|
-
|
|
57
|
-
---
|
|
58
|
-
|
|
59
|
-
## Common steps (apply once, always)
|
|
60
|
-
|
|
61
|
-
### C1 — Package versions
|
|
62
|
-
|
|
63
|
-
**File:** `Directory.Packages.props`
|
|
64
|
-
|
|
65
|
-
```xml
|
|
66
|
-
<ItemGroup>
|
|
67
|
-
<!-- AppWeaver — Jobs (engine: Quartz) -->
|
|
68
|
-
<PackageVersion Include="AppWeaver.Jobs" Version="0.1.1" />
|
|
69
|
-
<PackageVersion Include="AppWeaver.Jobs.Quartz" Version="0.1.0" />
|
|
70
|
-
<!-- + AppWeaver.Jobs.Persistence.EfCore 0.1.0 if a Group B item is selected -->
|
|
71
|
-
</ItemGroup>
|
|
72
|
-
```
|
|
73
|
-
|
|
74
|
-
### C2 — Project references (no `Version=`)
|
|
75
|
-
|
|
76
|
-
**File:** `src/<HostProject>/<HostProject>.csproj` (the host project resolved in step 2 — single
|
|
77
|
-
project, no separate Application/Infrastructure). Add the abstraction, the Quartz engine, and (if a
|
|
78
|
-
Group B item is selected) the EF Core persistence package here:
|
|
79
|
-
```xml
|
|
80
|
-
<ItemGroup>
|
|
81
|
-
<PackageReference Include="AppWeaver.Jobs" /> <!-- abstractions: IJob, IJobScheduler, IJobProgress -->
|
|
82
|
-
<PackageReference Include="AppWeaver.Jobs.Quartz" />
|
|
83
|
-
<!-- + AppWeaver.Jobs.Persistence.EfCore if a Group B item is selected -->
|
|
84
|
-
</ItemGroup>
|
|
85
|
-
```
|
|
86
|
-
|
|
87
|
-
### C3 — appsettings
|
|
88
|
-
|
|
89
|
-
**File:** `src/<HostProject>/appsettings.json` (the host project resolved in step 2)
|
|
90
|
-
```jsonc
|
|
91
|
-
{ "AppWeaver": { "Jobs": { "Provider": "Quartz", "Quartz": { "MaxConcurrency": 10 } } } }
|
|
92
|
-
```
|
|
93
|
-
|
|
94
|
-
### C4 — Registration scaffold
|
|
95
|
-
|
|
96
|
-
**File:** the host project `<HostProject>` (step 2) — register in its
|
|
97
|
-
`Extensions/InfrastructureServiceRegistration.cs` (`AddInfrastructure`) when that file exists (it does
|
|
98
|
-
in the Vertical Slice / Modular Monolith foundations), otherwise directly in `Program.cs` **before**
|
|
99
|
-
`builder.Build()`. (`configuration` below = the `IConfiguration` in scope: `config` inside
|
|
100
|
-
`AddInfrastructure`, `builder.Configuration` in `Program.cs`.)
|
|
101
|
-
|
|
102
|
-
The selected blocks fill in the `UseQuartzJobScheduler` body (Group A) and the persistence chain
|
|
103
|
-
(Group B).
|
|
104
|
-
|
|
105
|
-
```csharp
|
|
106
|
-
using AppWeaver.Jobs.Extensions;
|
|
107
|
-
using AppWeaver.Jobs.Quartz.Extensions;
|
|
108
|
-
// + using AppWeaver.Jobs.Persistence.EfCore.Extensions; (if Group B selected)
|
|
109
|
-
// + using Microsoft.EntityFrameworkCore; (if Group B selected)
|
|
110
|
-
|
|
111
|
-
services.AddAppWeaverJobs(
|
|
112
|
-
configureBuilder: builder => builder
|
|
113
|
-
.UseQuartzJobScheduler(opts =>
|
|
114
|
-
{
|
|
115
|
-
opts.MaxConcurrency = 10;
|
|
116
|
-
// ⬇ Group A block sets opts.ConfigureQuartz (persistent) or leaves it unset (in-memory)
|
|
117
|
-
})
|
|
118
|
-
// ⬇ Group B block chains .UseEfCoreJobs().UseJobDatabase(...) or .UseAppWeaverDataStore()
|
|
119
|
-
,
|
|
120
|
-
configureOptions: options => options.Provider = "Quartz");
|
|
121
|
-
```
|
|
122
|
-
|
|
123
|
-
> **Dual local/Aspire startup ([[components-readme]] R0.2a).** If the host project also has an
|
|
124
|
-
> `AddAspireInfrastructure` method (`Extensions/AspireInfrastructureExtensions.cs`) selected by
|
|
125
|
-
> `if (isAspire) … else …` in `Program.cs`, add this `AddAppWeaverJobs(...)` registration to **both**
|
|
126
|
-
> `AddInfrastructure` and `AddAspireInfrastructure`, or `IJobScheduler` is unregistered whenever the
|
|
127
|
-
> app runs under Aspire. Keep the structure identical, but make any **provider-specific** call match
|
|
128
|
-
> each method's database: in `AddAspireInfrastructure` use the PostgreSQL forms (`s.UsePostgres(...)`,
|
|
129
|
-
> `opts.UseNpgsql(...)`); in `AddInfrastructure` use the SQLite forms (`s.UseMicrosoftSQLite(...)`,
|
|
130
|
-
> `opts.UseSqlite(...)`). `EfCore.DataStore` (`.UseAppWeaverDataStore()`) is provider-agnostic — it
|
|
131
|
-
> rides whichever DataStore that method already registered.
|
|
132
|
-
|
|
133
|
-
---
|
|
134
|
-
|
|
135
|
-
## Implementation blocks
|
|
136
|
-
|
|
137
|
-
### A · `Quartz.InMemoryStore`
|
|
138
|
-
Leave `opts.ConfigureQuartz` **unset** — Quartz uses its in-memory store automatically. No DB, no
|
|
139
|
-
DDL. Schedule and pending jobs are lost on restart; single instance only.
|
|
140
|
-
|
|
141
|
-
### A · `Quartz.PersistentStore`
|
|
142
|
-
Set `ConfigureQuartz` inside `UseQuartzJobScheduler`:
|
|
143
|
-
```csharp
|
|
144
|
-
opts.ConfigureQuartz = q => q.UsePersistentStore(s =>
|
|
145
|
-
{
|
|
146
|
-
s.UsePostgres(configuration.GetConnectionString("Quartz")!); // or UseSqlServer / UseMicrosoftSQLite
|
|
147
|
-
s.UseSystemTextJsonSerializer();
|
|
148
|
-
});
|
|
149
|
-
```
|
|
150
|
-
> Apply the official Quartz DDL to that database **before first run** (Postgres/SQL Server scripts —
|
|
151
|
-
> see framework `Jobs/README.md` → *Database Setup*). Do **not** set `opts.ConnectionString` without
|
|
152
|
-
> `ConfigureQuartz` — that combination throws at startup.
|
|
153
|
-
|
|
154
|
-
### B · `EfCore.JobDatabase` (standalone status DB)
|
|
155
|
-
Add the package (C1/C2) and chain a dedicated `JobStatusDbContext`:
|
|
156
|
-
```csharp
|
|
157
|
-
.UseEfCoreJobs()
|
|
158
|
-
.UseJobDatabase(opts => opts.UseNpgsql(configuration.GetConnectionString("Jobs"))) // or UseSqlite("Data Source=jobs.db")
|
|
159
|
-
```
|
|
160
|
-
|
|
161
|
-
### B · `EfCore.DataStore` (shared AppWeaver DataStore)
|
|
162
|
-
Requires `AddDataStore().UseEfCoreDataStore()` to be registered **first**. Chain:
|
|
163
|
-
```csharp
|
|
164
|
-
.UseEfCoreJobs()
|
|
165
|
-
.UseAppWeaverDataStore()
|
|
166
|
-
```
|
|
167
|
-
> `UseEfCoreJobs()` alone does **not** register a data context — it must be followed by exactly one
|
|
168
|
-
> of `UseJobDatabase(...)` or `UseAppWeaverDataStore()`.
|
|
169
|
-
|
|
170
|
-
---
|
|
171
|
-
|
|
172
|
-
## Schema (only if a Group B / persistent Group A item was selected)
|
|
173
|
-
|
|
174
|
-
- **`EfCore.JobDatabase`** — scaffold + apply a `JobStatusDbContext` migration (or
|
|
175
|
-
`EnsureCreatedAsync()` for dev):
|
|
176
|
-
```bash
|
|
177
|
-
dotnet ef migrations add InitialJobs --context JobStatusDbContext \
|
|
178
|
-
--project src/<SampleProjectName>.Infrastructure --startup-project src/<SampleProjectName>.Service
|
|
179
|
-
dotnet ef database update --context JobStatusDbContext --startup-project src/<SampleProjectName>.Service
|
|
180
|
-
```
|
|
181
|
-
- **`EfCore.DataStore`** — Shared strategy: job tables ride in the `SharedComponentDbContext`
|
|
182
|
-
migration (only `dataStore.MigrateAsync()` at runtime). Isolated strategy: run the
|
|
183
|
-
`JobStatusDbContext` migration too (see [[persistence]]).
|
|
184
|
-
- **`Quartz.PersistentStore`** — apply the Quartz DDL (above).
|
|
185
|
-
|
|
186
|
-
> AI MAY scaffold migrations but MUST NOT run them against shared environments (see [[ai-rules]]).
|
|
187
|
-
|
|
188
|
-
---
|
|
189
|
-
|
|
190
|
-
## Usage (job classes + dispatch)
|
|
191
|
-
|
|
192
|
-
### Author a job — implement the `IJob` marker; inject `IJobProgress` for progress/cancel
|
|
193
|
-
**File:** `src/<SampleProjectName>.Application/Jobs/ExportDataJob.cs`
|
|
194
|
-
```csharp
|
|
195
|
-
using AppWeaver.Jobs.Abstractions;
|
|
196
|
-
|
|
197
|
-
public class ExportDataJob(IExportService _exports, IJobProgress _progress) : IJob
|
|
198
|
-
{
|
|
199
|
-
public async Task RunAsync(string table, Guid requestId)
|
|
200
|
-
{
|
|
201
|
-
await _progress.ReportProgressAsync(0, "Starting export…");
|
|
202
|
-
var rows = await _exports.FetchAsync(table);
|
|
203
|
-
for (int i = 0; i < rows.Count; i++)
|
|
204
|
-
{
|
|
205
|
-
_progress.CancellationToken.ThrowIfCancellationRequested(); // cooperative cancel
|
|
206
|
-
await _exports.ProcessAsync(rows[i]);
|
|
207
|
-
await _progress.ReportProgressAsync((i + 1) * 100 / rows.Count);
|
|
208
|
-
}
|
|
209
|
-
await _progress.ReportResultUrlAsync(await _exports.UploadAsync(requestId));
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
```
|
|
213
|
-
|
|
214
|
-
### Register every job class as `Scoped`
|
|
215
|
-
**File:** the host project's `Extensions/InfrastructureServiceRegistration.cs` (or `Program.cs`) — the
|
|
216
|
-
same registration site as C4
|
|
217
|
-
```csharp
|
|
218
|
-
services.AddScoped<ExportDataJob>();
|
|
219
|
-
```
|
|
220
|
-
|
|
221
|
-
### Dispatch / poll / cancel — inject `IJobScheduler` in a handler or endpoint
|
|
222
|
-
```csharp
|
|
223
|
-
var jobId = await _scheduler.EnqueueAsync<ExportDataJob>(
|
|
224
|
-
j => j.RunAsync(req.Table, req.RequestId), description: $"Export {req.Table}", userId: currentUserId);
|
|
225
|
-
|
|
226
|
-
await _scheduler.ScheduleAsync<SendReminderJob>(j => j.SendAsync(userId), TimeSpan.FromMinutes(30)); // delayed
|
|
227
|
-
JobStatus? status = await _scheduler.GetStatusAsync(jobId); // null = unknown/expired → 404
|
|
228
|
-
await _scheduler.CancelAsync(jobId);
|
|
229
|
-
```
|
|
230
|
-
|
|
231
|
-
### Recurring jobs — register at startup via `IHostedService` (fire at next CRON occurrence)
|
|
232
|
-
```csharp
|
|
233
|
-
public class JobConfigurator(IJobScheduler _scheduler) : IHostedService
|
|
234
|
-
{
|
|
235
|
-
public Task StartAsync(CancellationToken ct) =>
|
|
236
|
-
_scheduler.AddOrUpdateRecurringAsync<NightlyReportJob>(
|
|
237
|
-
"nightly-report", j => j.GenerateAsync(), "0 2 * * *"); // daily 02:00
|
|
238
|
-
public Task StopAsync(CancellationToken ct) => Task.CompletedTask;
|
|
239
|
-
}
|
|
240
|
-
// services.AddHostedService<JobConfigurator>();
|
|
241
|
-
```
|
|
242
|
-
|
|
243
|
-
---
|
|
244
|
-
|
|
245
|
-
## Verification
|
|
246
|
-
|
|
247
|
-
1. **Builds** — `dotnet build` succeeds; no `Version=` on references; `AppWeaver.Jobs.InMemory` absent.
|
|
248
|
-
2. **Startup validates** — app boots; `Provider` is `"Quartz"`; the Quartz hosted service starts.
|
|
249
|
-
3. **Schema exists** — if a persistence implementation was selected, the `JobStatus` table (and/or
|
|
250
|
-
Quartz tables) are present.
|
|
251
|
-
4. **Resolves** — `IJobScheduler` resolves; each job class resolves (registered `Scoped`).
|
|
252
|
-
5. **Round-trip** — enqueue → poll `GetStatusAsync` → reaches `Completed` with expected
|
|
253
|
-
`ProgressPercent`/`ResultUrl`; status is readable from the persistence store.
|
|
254
|
-
6. **Cancel** — a cancellable job moves to `Cancelled` after `CancelAsync`.
|
|
255
|
-
|
|
256
|
-
## Related
|
|
257
|
-
|
|
258
|
-
- [README.md](./README.md) · framework `Jobs/README.md` (deep-dive) · [[components-readme]] ·
|
|
259
|
-
[[cqrs]] · [[persistence]] · [[vertical-slices]] · [[dependency-policy]] · [[ai-rules]]
|