aw-spec-cli 0.1.2 → 0.1.3
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/docs/ModularMonolith/ModularMonolith.Reference.pack.txt +25248 -0
- package/assets/docs/ModularMonolith/ModularMonolith.pack.txt +5726 -0
- package/assets/docs/ModularMonolith/pack-foundation.ps1 +145 -0
- package/assets/docs/ModularMonolith/pack-foundation.sh +196 -0
- package/assets/docs/ModularMonolith/pack-reference-foundation.ps1 +157 -0
- package/assets/docs/ModularMonolith/pack-reference-foundation.sh +209 -0
- package/assets/docs/ModularMonolith/unpack-foundation.ps1 +126 -0
- package/assets/docs/ModularMonolith/unpack-foundation.sh +217 -0
- package/assets/docs/ModularMonolith/unpack-reference-foundation.ps1 +129 -0
- package/assets/docs/ModularMonolith/unpack-reference-foundation.sh +218 -0
- package/assets/docs/VerticalSlice/VerticalSlice.Reference.pack.txt +19318 -0
- package/assets/docs/VerticalSlice/pack-reference-foundation.ps1 +157 -0
- package/assets/docs/VerticalSlice/pack-reference-foundation.sh +209 -0
- package/assets/docs/VerticalSlice/unpack-reference-foundation.ps1 +129 -0
- package/assets/docs/VerticalSlice/unpack-reference-foundation.sh +218 -0
- package/assets/docs/components/Email/README.md +7 -0
- package/assets/docs/components/Email/ReferenceEndpoints/SendEmailEndpoint.cs +84 -0
- package/assets/docs/components/Email/integration.md +57 -17
- package/assets/docs/components/Jobs/integration.md +29 -15
- package/assets/docs/components/QrGenerator/README.md +7 -0
- package/assets/docs/components/QrGenerator/ReferenceEndpoints/GenerateQrEndpoint.cs +57 -0
- package/assets/docs/components/QrGenerator/integration.md +51 -6
- package/assets/docs/components/README.md +19 -2
- package/assets/docs/domainspecs/healthcare/comandrulemap.md +47 -0
- package/assets/docs/domainspecs/pharma/comandrulemap.md +50 -0
- package/package.json +5 -2
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# unpack-reference-foundation.sh — bash/macOS port of unpack-reference-foundation.ps1
|
|
4
|
+
#
|
|
5
|
+
# SYNOPSIS
|
|
6
|
+
# Unpack an AW-Spec reference pack file into a target project, renaming the
|
|
7
|
+
# sentinel app name to the caller's app name.
|
|
8
|
+
#
|
|
9
|
+
# DESCRIPTION
|
|
10
|
+
# Reads --pack-file (produced by pack-reference-foundation.sh/.ps1), takes the sentinel
|
|
11
|
+
# from its header (override with --sentinel), derives four case forms of the target app
|
|
12
|
+
# name, and writes every file block to --out-dir with the sentinel rewritten in both file
|
|
13
|
+
# CONTENTS and file/dir PATHS. Text blocks are rewritten and written as UTF-8; base64
|
|
14
|
+
# blocks are decoded as-is. Refuses to overwrite existing files unless --force.
|
|
15
|
+
#
|
|
16
|
+
# EXAMPLE
|
|
17
|
+
# ./unpack-reference-foundation.sh \
|
|
18
|
+
# --pack-file ./VerticalSlice.Reference.pack.txt \
|
|
19
|
+
# --target-app-name AppWeaver.Pharma \
|
|
20
|
+
# --out-dir ../../
|
|
21
|
+
#
|
|
22
|
+
set -euo pipefail
|
|
23
|
+
|
|
24
|
+
# ------------------------------------------------------------------ defaults ---
|
|
25
|
+
PACK_FILE=''
|
|
26
|
+
TARGET_APP_NAME=''
|
|
27
|
+
OUT_DIR='.'
|
|
28
|
+
SENTINEL=''
|
|
29
|
+
FORCE=0
|
|
30
|
+
DRYRUN=0
|
|
31
|
+
|
|
32
|
+
usage() {
|
|
33
|
+
cat >&2 <<'EOF'
|
|
34
|
+
Usage: unpack-reference-foundation.sh --pack-file <file> --target-app-name <name>
|
|
35
|
+
[--out-dir <dir>] [--sentinel <name>]
|
|
36
|
+
[--force] [--dry-run]
|
|
37
|
+
|
|
38
|
+
-p, --pack-file <file> Reference pack file to unpack (required)
|
|
39
|
+
-t, --target-app-name <name> New app name to rename the sentinel into (required)
|
|
40
|
+
-o, --out-dir <dir> Output root (default: .)
|
|
41
|
+
--sentinel <name> Override the sentinel from the pack header
|
|
42
|
+
--force Overwrite files that already exist
|
|
43
|
+
--dry-run List what would be written, write nothing
|
|
44
|
+
-h, --help Show this help
|
|
45
|
+
|
|
46
|
+
Positional fallback: unpack-reference-foundation.sh <pack-file> <target-app-name> [out-dir]
|
|
47
|
+
EOF
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
# -------------------------------------------------------------- arg parsing ---
|
|
51
|
+
while [ $# -gt 0 ]; do
|
|
52
|
+
case "$1" in
|
|
53
|
+
-p|--pack-file) PACK_FILE="${2:?--pack-file needs a value}"; shift 2 ;;
|
|
54
|
+
-t|--target-app-name) TARGET_APP_NAME="${2:?--target-app-name needs a value}"; shift 2 ;;
|
|
55
|
+
-o|--out-dir) OUT_DIR="${2:?--out-dir needs a value}"; shift 2 ;;
|
|
56
|
+
--sentinel) SENTINEL="${2:?--sentinel needs a value}"; shift 2 ;;
|
|
57
|
+
--force) FORCE=1; shift ;;
|
|
58
|
+
--dry-run) DRYRUN=1; shift ;;
|
|
59
|
+
-h|--help) usage; exit 0 ;;
|
|
60
|
+
--) shift; break ;;
|
|
61
|
+
-*) echo "Error: unknown option: $1" >&2; usage; exit 2 ;;
|
|
62
|
+
*)
|
|
63
|
+
if [ -z "$PACK_FILE" ]; then PACK_FILE="$1"
|
|
64
|
+
elif [ -z "$TARGET_APP_NAME" ]; then TARGET_APP_NAME="$1"
|
|
65
|
+
else OUT_DIR="$1"
|
|
66
|
+
fi
|
|
67
|
+
shift ;;
|
|
68
|
+
esac
|
|
69
|
+
done
|
|
70
|
+
|
|
71
|
+
[ -n "$PACK_FILE" ] || { echo "Error: --pack-file is required." >&2; usage; exit 2; }
|
|
72
|
+
[ -n "$TARGET_APP_NAME" ] || { echo "Error: --target-app-name is required." >&2; usage; exit 2; }
|
|
73
|
+
[ -f "$PACK_FILE" ] || { echo "Error: pack file not found: $PACK_FILE" >&2; exit 1; }
|
|
74
|
+
|
|
75
|
+
case "$TARGET_APP_NAME" in
|
|
76
|
+
*[!A-Za-z0-9._-]*)
|
|
77
|
+
echo "Error: --target-app-name may contain only letters, digits, '.', '_' or '-': $TARGET_APP_NAME" >&2
|
|
78
|
+
exit 2 ;;
|
|
79
|
+
esac
|
|
80
|
+
|
|
81
|
+
# ------------------------------------------------ sentinel from header --------
|
|
82
|
+
if [ -z "$SENTINEL" ]; then
|
|
83
|
+
SENTINEL="$(LC_ALL=C sed -n 's/^#[[:space:]]*sentinel:[[:space:]]*\(.*[^[:space:]]\)[[:space:]]*$/\1/p' "$PACK_FILE" | head -n1)"
|
|
84
|
+
fi
|
|
85
|
+
[ -n "$SENTINEL" ] || { echo "Error: Sentinel not found in pack header; supply it with --sentinel." >&2; exit 1; }
|
|
86
|
+
|
|
87
|
+
# ------------------------------------------------ derive case forms -----------
|
|
88
|
+
to_lower() { printf '%s' "$1" | LC_ALL=C tr '[:upper:]' '[:lower:]'; }
|
|
89
|
+
|
|
90
|
+
S_DOT="$SENTINEL"; T_DOT="$TARGET_APP_NAME"
|
|
91
|
+
S_UND="${SENTINEL//./_}"; T_UND="${TARGET_APP_NAME//./_}"
|
|
92
|
+
s_low="$(to_lower "$SENTINEL")"; t_low="$(to_lower "$TARGET_APP_NAME")"
|
|
93
|
+
S_HYP="${s_low//./-}"; T_HYP="${t_low//./-}"
|
|
94
|
+
S_LDOT="$s_low"; T_LDOT="$t_low"
|
|
95
|
+
|
|
96
|
+
FROM=(); TO=()
|
|
97
|
+
add_form() {
|
|
98
|
+
local from="$1" to="$2" e
|
|
99
|
+
[ -n "$from" ] || return 0
|
|
100
|
+
[ "$from" = "$to" ] && return 0
|
|
101
|
+
if [ "${#FROM[@]}" -gt 0 ]; then
|
|
102
|
+
for e in "${FROM[@]}"; do [ "$e" = "$from" ] && return 0; done
|
|
103
|
+
fi
|
|
104
|
+
FROM+=("$from"); TO+=("$to")
|
|
105
|
+
}
|
|
106
|
+
add_form "$S_DOT" "$T_DOT"
|
|
107
|
+
add_form "$S_UND" "$T_UND"
|
|
108
|
+
add_form "$S_HYP" "$T_HYP"
|
|
109
|
+
add_form "$S_LDOT" "$T_LDOT"
|
|
110
|
+
|
|
111
|
+
# Sort indices by FROM length descending
|
|
112
|
+
ORDER=()
|
|
113
|
+
{
|
|
114
|
+
local_n="${#FROM[@]}"
|
|
115
|
+
used=(); for ((k = 0; k < local_n; k++)); do used[$k]=0; done
|
|
116
|
+
for ((pass = 0; pass < local_n; pass++)); do
|
|
117
|
+
best=-1; bestlen=-1
|
|
118
|
+
for ((j = 0; j < local_n; j++)); do
|
|
119
|
+
if [ "${used[$j]}" -eq 0 ] && [ "${#FROM[$j]}" -gt "$bestlen" ]; then
|
|
120
|
+
bestlen="${#FROM[$j]}"; best="$j"
|
|
121
|
+
fi
|
|
122
|
+
done
|
|
123
|
+
used[$best]=1; ORDER+=("$best")
|
|
124
|
+
done
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
REPL_OUT=''
|
|
128
|
+
apply_repl() {
|
|
129
|
+
local text="$1" idx from to
|
|
130
|
+
if [ "${#ORDER[@]}" -gt 0 ]; then
|
|
131
|
+
for idx in "${ORDER[@]}"; do
|
|
132
|
+
from="${FROM[$idx]}"; to="${TO[$idx]}"
|
|
133
|
+
text="${text//"$from"/$to}"
|
|
134
|
+
done
|
|
135
|
+
fi
|
|
136
|
+
REPL_OUT="$text"
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
# ------------------------------------------------ output root ----------------
|
|
140
|
+
[ -d "$OUT_DIR" ] || mkdir -p "$OUT_DIR"
|
|
141
|
+
OUT_ROOT="$(cd "$OUT_DIR" && pwd -P)"
|
|
142
|
+
|
|
143
|
+
# ------------------------------------------------ base64 decoder (portable) --
|
|
144
|
+
B64D=()
|
|
145
|
+
if printf 'aGk=' | base64 -d >/dev/null 2>&1; then B64D=(base64 -d)
|
|
146
|
+
elif printf 'aGk=' | base64 -D >/dev/null 2>&1; then B64D=(base64 -D)
|
|
147
|
+
elif printf 'aGk=' | base64 --decode >/dev/null 2>&1; then B64D=(base64 --decode)
|
|
148
|
+
else B64D=(openssl base64 -d -A)
|
|
149
|
+
fi
|
|
150
|
+
|
|
151
|
+
# ------------------------------------------------ parse file blocks ----------
|
|
152
|
+
rels=(); encs=(); contents=()
|
|
153
|
+
in_block=0; cur_rel=''; cur_enc=''; cur_content=''; first_line=1
|
|
154
|
+
file_re='^<<<FILE path="([^"]+)"( encoding="([^"]+)")?>>>[[:space:]]*$'
|
|
155
|
+
|
|
156
|
+
while IFS= read -r line || [ -n "$line" ]; do
|
|
157
|
+
line="${line%$'\r'}"
|
|
158
|
+
if [ "$in_block" -eq 0 ]; then
|
|
159
|
+
if [[ "$line" =~ $file_re ]]; then
|
|
160
|
+
cur_rel="${BASH_REMATCH[1]}"
|
|
161
|
+
cur_enc="${BASH_REMATCH[3]:-}"
|
|
162
|
+
in_block=1; first_line=1; cur_content=''
|
|
163
|
+
fi
|
|
164
|
+
else
|
|
165
|
+
if [ "$line" = '<<<ENDFILE>>>' ]; then
|
|
166
|
+
rels+=("$cur_rel"); encs+=("$cur_enc"); contents+=("$cur_content")
|
|
167
|
+
in_block=0
|
|
168
|
+
elif [ "$first_line" -eq 1 ]; then
|
|
169
|
+
cur_content="$line"; first_line=0
|
|
170
|
+
else
|
|
171
|
+
cur_content="$cur_content"$'\n'"$line"
|
|
172
|
+
fi
|
|
173
|
+
fi
|
|
174
|
+
done < "$PACK_FILE"
|
|
175
|
+
|
|
176
|
+
[ "${#rels[@]}" -gt 0 ] || { echo "Error: No <<<FILE>>> blocks found in pack: $PACK_FILE" >&2; exit 1; }
|
|
177
|
+
|
|
178
|
+
# ------------------------------------------------ resolve destinations -------
|
|
179
|
+
dests=()
|
|
180
|
+
for ((i = 0; i < ${#rels[@]}; i++)); do
|
|
181
|
+
apply_repl "${rels[$i]}"
|
|
182
|
+
dests+=("$OUT_ROOT/$REPL_OUT")
|
|
183
|
+
done
|
|
184
|
+
|
|
185
|
+
# ------------------------------------------------ conflict guard -------------
|
|
186
|
+
if [ "$FORCE" -eq 0 ] && [ "$DRYRUN" -eq 0 ]; then
|
|
187
|
+
conflicts=()
|
|
188
|
+
for d in "${dests[@]}"; do [ -e "$d" ] && conflicts+=("$d"); done
|
|
189
|
+
if [ "${#conflicts[@]}" -gt 0 ]; then
|
|
190
|
+
echo "Error: Refusing to overwrite existing files (re-run with --force):" >&2
|
|
191
|
+
printf ' %s\n' "${conflicts[@]}" >&2
|
|
192
|
+
exit 1
|
|
193
|
+
fi
|
|
194
|
+
fi
|
|
195
|
+
|
|
196
|
+
# ------------------------------------------------ write ----------------------
|
|
197
|
+
written=0
|
|
198
|
+
for ((i = 0; i < ${#rels[@]}; i++)); do
|
|
199
|
+
rel="${rels[$i]}"; enc="${encs[$i]}"; dest="${dests[$i]}"
|
|
200
|
+
if [ "$DRYRUN" -eq 1 ]; then
|
|
201
|
+
echo "WOULD WRITE $rel -> $dest"
|
|
202
|
+
continue
|
|
203
|
+
fi
|
|
204
|
+
mkdir -p "$(dirname "$dest")"
|
|
205
|
+
if [ "$enc" = 'base64' ]; then
|
|
206
|
+
printf '%s' "${contents[$i]}" | tr -d '[:space:]' | "${B64D[@]}" > "$dest"
|
|
207
|
+
else
|
|
208
|
+
apply_repl "${contents[$i]}"
|
|
209
|
+
printf '%s' "$REPL_OUT" > "$dest"
|
|
210
|
+
fi
|
|
211
|
+
written=$((written + 1))
|
|
212
|
+
done
|
|
213
|
+
|
|
214
|
+
if [ "$DRYRUN" -eq 1 ]; then
|
|
215
|
+
echo "DryRun: ${#rels[@]} file(s) would be written to $OUT_ROOT (sentinel '$SENTINEL' -> '$TARGET_APP_NAME')."
|
|
216
|
+
else
|
|
217
|
+
echo "Unpacked $written file(s) to $OUT_ROOT (sentinel '$SENTINEL' -> '$TARGET_APP_NAME')."
|
|
218
|
+
fi
|
|
@@ -106,6 +106,13 @@ framework/Messaging.Email/
|
|
|
106
106
|
TestApp/Program.cs ← OAuth2 + File multi-provider sample
|
|
107
107
|
```
|
|
108
108
|
|
|
109
|
+
## Sample reference endpoint
|
|
110
|
+
|
|
111
|
+
A ready-to-run FastEndpoint that sends mail inline (no command/handler) is bundled at
|
|
112
|
+
[`ReferenceEndpoints/SendEmailEndpoint.cs`](./ReferenceEndpoints/SendEmailEndpoint.cs). It is offered
|
|
113
|
+
(opt-in) at the end of the [integration.md](./integration.md) runbook and, if accepted, copied into
|
|
114
|
+
the host project's `ReferenceEndpoints/` folder. Endpoint: `POST /reference/email/send`.
|
|
115
|
+
|
|
109
116
|
## Related
|
|
110
117
|
|
|
111
118
|
- [integration.md](./integration.md) · [[components-readme]] · [[cqrs]] ·
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
using FastEndpoints;
|
|
2
|
+
using Microsoft.Extensions.Configuration;
|
|
3
|
+
using AppWeaver.Messaging.Email;
|
|
4
|
+
|
|
5
|
+
// NOTE: This is a bundled SAMPLE endpoint for the AppWeaver Email component.
|
|
6
|
+
// It lives under docs/ and is NOT compiled here. /aw-spec-addcomponent copies it into the
|
|
7
|
+
// host project's ReferenceEndpoints/ folder and replaces the __HOSTPROJECT__ namespace token
|
|
8
|
+
// with the host project's root namespace (e.g. AppWeaver.Health.Service).
|
|
9
|
+
namespace __HOSTPROJECT__.ReferenceEndpoints;
|
|
10
|
+
|
|
11
|
+
/// <summary>
|
|
12
|
+
/// Sample reference endpoint for the AppWeaver Email component.
|
|
13
|
+
///
|
|
14
|
+
/// Demonstrates the full send sequence inline — it injects the <see cref="IEmailService"/>
|
|
15
|
+
/// abstraction directly and calls <c>SendAsync</c> with the provider wired during DI
|
|
16
|
+
/// registration (see the component's integration runbook). There is intentionally no
|
|
17
|
+
/// command / command-handler / mediator: this is a copy-paste sample only.
|
|
18
|
+
///
|
|
19
|
+
/// The active transport is whatever <c>options.Provider</c> was set to when
|
|
20
|
+
/// <c>AddEmailMessaging(...)</c> was registered. With the <c>File</c> provider the message is
|
|
21
|
+
/// written to <c>Emails_<yyyy-MM-dd>.txt</c> in the application base directory.
|
|
22
|
+
/// </summary>
|
|
23
|
+
public class SendEmailEndpoint(IEmailService _email, IConfiguration _config)
|
|
24
|
+
: Endpoint<SendEmailRequest>
|
|
25
|
+
{
|
|
26
|
+
public override void Configure()
|
|
27
|
+
{
|
|
28
|
+
Post("/reference/email/send");
|
|
29
|
+
AllowAnonymous();
|
|
30
|
+
Summary(s =>
|
|
31
|
+
{
|
|
32
|
+
s.Summary = "Send a sample email via the configured Email provider";
|
|
33
|
+
s.Description = "Reference endpoint for the AppWeaver Email component. Injects "
|
|
34
|
+
+ "IEmailService directly (no command/handler). 'from' falls back to "
|
|
35
|
+
+ "Messaging:Email:DefaultFrom when omitted.";
|
|
36
|
+
});
|
|
37
|
+
Options(o => o.WithTags("Reference - Email"));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
public override async Task HandleAsync(SendEmailRequest req, CancellationToken ct)
|
|
41
|
+
{
|
|
42
|
+
if (string.IsNullOrWhiteSpace(req.To) || string.IsNullOrWhiteSpace(req.Subject))
|
|
43
|
+
{
|
|
44
|
+
HttpContext.Response.StatusCode = 400;
|
|
45
|
+
await HttpContext.Response.WriteAsJsonAsync(
|
|
46
|
+
new { error = "'to' and 'subject' are required." }, ct);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// 'from' is optional in the request — fall back to the configured DefaultFrom.
|
|
51
|
+
var from = string.IsNullOrWhiteSpace(req.From)
|
|
52
|
+
? _config["Messaging:Email:DefaultFrom"] ?? "no-reply@your-domain.com"
|
|
53
|
+
: req.From;
|
|
54
|
+
|
|
55
|
+
await _email.SendAsync(from, req.To, req.Subject, req.Body ?? string.Empty, req.IsHtml, ct);
|
|
56
|
+
|
|
57
|
+
await HttpContext.Response.WriteAsJsonAsync(new
|
|
58
|
+
{
|
|
59
|
+
status = "sent",
|
|
60
|
+
from,
|
|
61
|
+
to = req.To,
|
|
62
|
+
note = "With the File provider, output is written to Emails_<yyyy-MM-dd>.txt "
|
|
63
|
+
+ "in the app base directory."
|
|
64
|
+
}, ct);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
public class SendEmailRequest
|
|
69
|
+
{
|
|
70
|
+
/// <summary>Recipient address (required).</summary>
|
|
71
|
+
public string To { get; set; } = string.Empty;
|
|
72
|
+
|
|
73
|
+
/// <summary>Subject line (required).</summary>
|
|
74
|
+
public string Subject { get; set; } = string.Empty;
|
|
75
|
+
|
|
76
|
+
/// <summary>HTML or plain-text body.</summary>
|
|
77
|
+
public string Body { get; set; } = string.Empty;
|
|
78
|
+
|
|
79
|
+
/// <summary>Treat <see cref="Body"/> as HTML. Defaults to true.</summary>
|
|
80
|
+
public bool IsHtml { get; set; } = true;
|
|
81
|
+
|
|
82
|
+
/// <summary>Optional sender; falls back to Messaging:Email:DefaultFrom when omitted.</summary>
|
|
83
|
+
public string? From { get; set; }
|
|
84
|
+
}
|
|
@@ -11,9 +11,13 @@
|
|
|
11
11
|
## Agent workflow — execute in order
|
|
12
12
|
|
|
13
13
|
1. **Load context.** Read [README.md](./README.md) (contract + provider matrix) and this file.
|
|
14
|
-
2. **
|
|
15
|
-
|
|
16
|
-
|
|
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.
|
|
17
21
|
3. **Present the implementation catalog (below) as a selection list and ASK the user which
|
|
18
22
|
implementation(s) to integrate.** Use an interactive multi-select prompt (`AskUserQuestion`,
|
|
19
23
|
`multiSelect: true`). The list options are the rows of the *Implementation catalog*.
|
|
@@ -27,7 +31,10 @@
|
|
|
27
31
|
6. **Set the default provider:** `configureOptions: options => options.Provider = "<default key>"`.
|
|
28
32
|
All selected implementations stay registered; the active one is switchable later via config.
|
|
29
33
|
7. **(Optional) Wire a usage example** in the relevant feature handler (§Usage).
|
|
30
|
-
8. **
|
|
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.
|
|
31
38
|
|
|
32
39
|
> **Guardrail — always set `options.Provider`.** When `Provider` is null the facade falls back to a
|
|
33
40
|
> **hardcoded `"SendGrid"`** key; if SendGrid is not registered, resolution throws at runtime. Never
|
|
@@ -68,26 +75,24 @@ Present these as the multi-select options. `Key` is the provider key and the `Us
|
|
|
68
75
|
|
|
69
76
|
### C2 — Project references
|
|
70
77
|
|
|
71
|
-
**File:** `src/<
|
|
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).
|
|
72
81
|
|
|
73
82
|
```xml
|
|
74
83
|
<ItemGroup>
|
|
75
84
|
<PackageReference Include="AppWeaver.Messaging.Email" /> <!-- no Version= -->
|
|
76
|
-
|
|
77
|
-
```
|
|
78
|
-
|
|
79
|
-
**File:** `src/<SampleProjectName>.Infrastructure/<SampleProjectName>.Infrastructure.csproj`
|
|
80
|
-
(add the abstraction here too, plus each selected implementation’s package — see its block).
|
|
81
|
-
|
|
82
|
-
```xml
|
|
83
|
-
<ItemGroup>
|
|
84
|
-
<PackageReference Include="AppWeaver.Messaging.Email" />
|
|
85
|
+
<!-- + each selected implementation’s package — see its block below -->
|
|
85
86
|
</ItemGroup>
|
|
86
87
|
```
|
|
87
88
|
|
|
88
89
|
### C3 — Registration scaffold
|
|
89
90
|
|
|
90
|
-
**File:**
|
|
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`.)
|
|
91
96
|
|
|
92
97
|
Add the base call; each selected implementation contributes one `builder.Use…(…)` line (C4).
|
|
93
98
|
|
|
@@ -103,9 +108,16 @@ services.AddEmailMessaging(
|
|
|
103
108
|
configureOptions: options => options.Provider = "<default key>"); // step 6
|
|
104
109
|
```
|
|
105
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
|
+
|
|
106
118
|
### C4 — Base appsettings block
|
|
107
119
|
|
|
108
|
-
**File:** `src/<
|
|
120
|
+
**File:** `src/<HostProject>/appsettings.json` (the host project resolved in step 2)
|
|
109
121
|
|
|
110
122
|
```jsonc
|
|
111
123
|
{
|
|
@@ -128,7 +140,7 @@ line**, **config**. Secrets MUST be supplied via user-secrets / env / key vault
|
|
|
128
140
|
### MsGraph
|
|
129
141
|
```xml
|
|
130
142
|
<!-- Directory.Packages.props --> <PackageVersion Include="AppWeaver.Messaging.Email.MsGraph" Version="0.1.2" />
|
|
131
|
-
<!--
|
|
143
|
+
<!-- host .csproj --> <PackageReference Include="AppWeaver.Messaging.Email.MsGraph" />
|
|
132
144
|
```
|
|
133
145
|
```csharp
|
|
134
146
|
using AppWeaver.Messaging.Email.MsGraph.Extensions;
|
|
@@ -244,6 +256,31 @@ see [README.md](./README.md)).
|
|
|
244
256
|
|
|
245
257
|
---
|
|
246
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
|
+
|
|
247
284
|
## Verification
|
|
248
285
|
|
|
249
286
|
1. **Builds** — `dotnet build` succeeds; no `Version=` on package references.
|
|
@@ -253,6 +290,9 @@ see [README.md](./README.md)).
|
|
|
253
290
|
4. **Sends** — a send succeeds; for `File`, confirm `Emails_<yyyy-MM-dd>.txt` appears in the app
|
|
254
291
|
base directory.
|
|
255
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.
|
|
256
296
|
|
|
257
297
|
## Related
|
|
258
298
|
|
|
@@ -12,9 +12,13 @@
|
|
|
12
12
|
|
|
13
13
|
1. **Load context.** Read [README.md](./README.md) and this file. The functional engine is
|
|
14
14
|
**Quartz** — `AppWeaver.Jobs` + `AppWeaver.Jobs.Quartz` are always installed.
|
|
15
|
-
2. **
|
|
16
|
-
that
|
|
17
|
-
(`
|
|
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.
|
|
18
22
|
3. **Present the implementation catalog (below) as a selection list and ASK the user which
|
|
19
23
|
implementation(s) to integrate.** Use a multi-select prompt (`AskUserQuestion`,
|
|
20
24
|
`multiSelect: true`). Enforce the grouping rules when reading the answer:
|
|
@@ -69,17 +73,12 @@
|
|
|
69
73
|
|
|
70
74
|
### C2 — Project references (no `Version=`)
|
|
71
75
|
|
|
72
|
-
**File:** `src/<
|
|
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:
|
|
73
79
|
```xml
|
|
74
80
|
<ItemGroup>
|
|
75
|
-
<PackageReference Include="AppWeaver.Jobs" />
|
|
76
|
-
</ItemGroup>
|
|
77
|
-
```
|
|
78
|
-
|
|
79
|
-
**File:** `src/<SampleProjectName>.Infrastructure/<SampleProjectName>.Infrastructure.csproj`
|
|
80
|
-
```xml
|
|
81
|
-
<ItemGroup>
|
|
82
|
-
<PackageReference Include="AppWeaver.Jobs" />
|
|
81
|
+
<PackageReference Include="AppWeaver.Jobs" /> <!-- abstractions: IJob, IJobScheduler, IJobProgress -->
|
|
83
82
|
<PackageReference Include="AppWeaver.Jobs.Quartz" />
|
|
84
83
|
<!-- + AppWeaver.Jobs.Persistence.EfCore if a Group B item is selected -->
|
|
85
84
|
</ItemGroup>
|
|
@@ -87,14 +86,18 @@
|
|
|
87
86
|
|
|
88
87
|
### C3 — appsettings
|
|
89
88
|
|
|
90
|
-
**File:** `src/<
|
|
89
|
+
**File:** `src/<HostProject>/appsettings.json` (the host project resolved in step 2)
|
|
91
90
|
```jsonc
|
|
92
91
|
{ "AppWeaver": { "Jobs": { "Provider": "Quartz", "Quartz": { "MaxConcurrency": 10 } } } }
|
|
93
92
|
```
|
|
94
93
|
|
|
95
94
|
### C4 — Registration scaffold
|
|
96
95
|
|
|
97
|
-
**File:**
|
|
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`.)
|
|
98
101
|
|
|
99
102
|
The selected blocks fill in the `UseQuartzJobScheduler` body (Group A) and the persistence chain
|
|
100
103
|
(Group B).
|
|
@@ -117,6 +120,16 @@ services.AddAppWeaverJobs(
|
|
|
117
120
|
configureOptions: options => options.Provider = "Quartz");
|
|
118
121
|
```
|
|
119
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
|
+
|
|
120
133
|
---
|
|
121
134
|
|
|
122
135
|
## Implementation blocks
|
|
@@ -199,7 +212,8 @@ public class ExportDataJob(IExportService _exports, IJobProgress _progress) : IJ
|
|
|
199
212
|
```
|
|
200
213
|
|
|
201
214
|
### Register every job class as `Scoped`
|
|
202
|
-
**File:**
|
|
215
|
+
**File:** the host project's `Extensions/InfrastructureServiceRegistration.cs` (or `Program.cs`) — the
|
|
216
|
+
same registration site as C4
|
|
203
217
|
```csharp
|
|
204
218
|
services.AddScoped<ExportDataJob>();
|
|
205
219
|
```
|
|
@@ -72,6 +72,13 @@ framework/QrGenerator/
|
|
|
72
72
|
QrGenerator.QrCoder.Tests/ ← QRCoderServiceTests (xUnit)
|
|
73
73
|
```
|
|
74
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
|
+
|
|
75
82
|
## Related
|
|
76
83
|
|
|
77
84
|
- [integration.md](./integration.md) · [[components-readme]] · [[cqrs]] ·
|
|
@@ -0,0 +1,57 @@
|
|
|
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
|
+
}
|