aw-spec-cli 0.1.8 → 0.1.10
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/.appweaver/DESIGN-FORMAT.md +200 -0
- package/assets/.appweaver/ORCH-DIALECT.md +156 -0
- package/assets/.appweaver/RULE-PARTITION.md +175 -0
- package/assets/.appweaver/STACK-PROFILES.md +461 -0
- package/assets/.appweaver/ai-rules.md +180 -0
- package/assets/.appweaver/domain.json +1 -0
- package/assets/.appweaver/extensions/.registry +19 -0
- package/assets/.appweaver/extensions/agent-context/README.md +57 -0
- package/assets/.appweaver/extensions/agent-context/agent-context-config.yml +4 -0
- package/assets/.appweaver/extensions/agent-context/commands/aw-spec.agent-context.update.md +26 -0
- package/assets/.appweaver/extensions/agent-context/extension.yml +37 -0
- package/assets/.appweaver/extensions/agent-context/scripts/bash/update-agent-context.sh +201 -0
- package/assets/.appweaver/extensions/agent-context/scripts/powershell/update-agent-context.ps1 +237 -0
- package/assets/.appweaver/extensions.yml +16 -0
- package/assets/.appweaver/feature.json +3 -0
- package/assets/.appweaver/init-options.json +9 -0
- package/assets/.appweaver/integration.json +15 -0
- package/assets/.appweaver/integrations/aw-spec.manifest.json +9 -0
- package/assets/.appweaver/interpreter/run-skill.py +1176 -0
- package/assets/.appweaver/memory/constitution.md +50 -0
- package/assets/.appweaver/orchestrator/.smoke-out/smoke.md +6 -0
- package/assets/.appweaver/orchestrator/AGENT-PROFILE.md +94 -0
- package/assets/.appweaver/orchestrator/README.md +300 -0
- package/assets/.appweaver/orchestrator/adapters/claude_agent_sdk.py +74 -0
- package/assets/.appweaver/orchestrator/adapters/google_antigravity.py +74 -0
- package/assets/.appweaver/orchestrator/modules/delta-design.orch.yaml +105 -0
- package/assets/.appweaver/orchestrator/modules/delta-spec.orch.yaml +101 -0
- package/assets/.appweaver/orchestrator/modules/domain-select.orch.yaml +90 -0
- package/assets/.appweaver/orchestrator/modules/lane-gate.orch.yaml +126 -0
- package/assets/.appweaver/orchestrator/modules/tech-select.orch.yaml +123 -0
- package/assets/.appweaver/orchestrator/orchestrator.py +1721 -0
- package/assets/.appweaver/orchestrator/rev-templates/ai-rules.reference.md +40 -0
- package/assets/.appweaver/orchestrator/rev-templates/delta-design-template.skeleton.md +101 -0
- package/assets/.appweaver/orchestrator/rev-templates/design-template.skeleton.md +245 -0
- package/assets/.appweaver/orchestrator/rev-templates/domain-constitution.skeleton.md +63 -0
- package/assets/.appweaver/orchestrator/rev-templates/domain-requirements.skeleton.md +48 -0
- package/assets/.appweaver/orchestrator/rev-templates/domain-rule-bindings.skeleton.md +59 -0
- package/assets/.appweaver/orchestrator/rev-templates/domain-rule-file.skeleton.md +39 -0
- package/assets/.appweaver/orchestrator/rev-templates/frontend-rule-file.skeleton.md +63 -0
- package/assets/.appweaver/orchestrator/rev-templates/tech-rule-bindings.skeleton.md +116 -0
- package/assets/.appweaver/orchestrator/rev-templates/technical-constitution.skeleton.md +106 -0
- package/assets/.appweaver/orchestrator/rev-templates/technical-rule-file.skeleton.md +54 -0
- package/assets/.appweaver/orchestrator/rev-templates/ui-constitution.skeleton.md +125 -0
- package/assets/.appweaver/orchestrator/rev-templates/ui-delta-design-template.skeleton.md +123 -0
- package/assets/.appweaver/orchestrator/rev-templates/ui-design-template.skeleton.md +294 -0
- package/assets/.appweaver/orchestrator/workflows/_smoke.orch.yaml +160 -0
- package/assets/.appweaver/orchestrator/workflows/aw-spec-add-component.orch.yaml +270 -0
- package/assets/.appweaver/orchestrator/workflows/aw-spec-adopt.orch.yaml +934 -0
- package/assets/.appweaver/orchestrator/workflows/aw-spec-analyze-delta-design.orch.yaml +251 -0
- package/assets/.appweaver/orchestrator/workflows/aw-spec-analyze-design.orch.yaml +265 -0
- package/assets/.appweaver/orchestrator/workflows/aw-spec-analyze-slice.orch.yaml +437 -0
- package/assets/.appweaver/orchestrator/workflows/aw-spec-build.orch.yaml +197 -0
- package/assets/.appweaver/orchestrator/workflows/aw-spec-characterize.orch.yaml +475 -0
- package/assets/.appweaver/orchestrator/workflows/aw-spec-close-delta.orch.yaml +181 -0
- package/assets/.appweaver/orchestrator/workflows/aw-spec-constitution.orch.yaml +727 -0
- package/assets/.appweaver/orchestrator/workflows/aw-spec-demo.orch.yaml +100 -0
- package/assets/.appweaver/orchestrator/workflows/aw-spec-dependency-tree.orch.yaml +69 -0
- package/assets/.appweaver/orchestrator/workflows/aw-spec-deploy.orch.yaml +85 -0
- package/assets/.appweaver/orchestrator/workflows/aw-spec-design-ui.orch.yaml +386 -0
- package/assets/.appweaver/orchestrator/workflows/aw-spec-design.orch.yaml +382 -0
- package/assets/.appweaver/orchestrator/workflows/aw-spec-finalize-delta.orch.yaml +462 -0
- package/assets/.appweaver/orchestrator/workflows/aw-spec-foundation.orch.yaml +265 -0
- package/assets/.appweaver/orchestrator/workflows/aw-spec-implement-delta.orch.yaml +253 -0
- package/assets/.appweaver/orchestrator/workflows/aw-spec-implement-ui.orch.yaml +267 -0
- package/assets/.appweaver/orchestrator/workflows/aw-spec-implement.orch.yaml +287 -0
- package/assets/.appweaver/orchestrator/workflows/aw-spec-inventory.orch.yaml +238 -0
- package/assets/.appweaver/orchestrator/workflows/aw-spec-onboard.orch.yaml +1822 -0
- package/assets/.appweaver/orchestrator/workflows/aw-spec-parity.orch.yaml +293 -0
- package/assets/.appweaver/orchestrator/workflows/aw-spec-run.orch.yaml +194 -0
- package/assets/.appweaver/orchestrator/workflows/aw-spec-select-domain.orch.yaml +83 -0
- package/assets/.appweaver/orchestrator/workflows/aw-spec-select-feature.orch.yaml +128 -0
- package/assets/.appweaver/orchestrator/workflows/aw-spec-select-stack.orch.yaml +296 -0
- package/assets/.appweaver/orchestrator/workflows/aw-spec-specify.orch.yaml +439 -0
- package/assets/.appweaver/scripts/bash/aw-inventory.sh +17 -0
- package/assets/.appweaver/scripts/bash/aw-parity.sh +13 -0
- package/assets/.appweaver/scripts/bash/aw-rev-constitution.sh +13 -0
- package/assets/.appweaver/scripts/bash/aw-spec-actions.sh +2626 -0
- package/assets/.appweaver/scripts/bash/aw-xref.sh +15 -0
- package/assets/.appweaver/scripts/bash/common.sh +533 -0
- package/assets/.appweaver/scripts/powershell/aw-inventory.ps1 +17 -0
- package/assets/.appweaver/scripts/powershell/aw-parity.ps1 +13 -0
- package/assets/.appweaver/scripts/powershell/aw-rev-constitution.ps1 +13 -0
- package/assets/.appweaver/scripts/powershell/aw-spec-actions.ps1 +2232 -0
- package/assets/.appweaver/scripts/powershell/aw-xref.ps1 +15 -0
- package/assets/.appweaver/scripts/python/aw-component-catalog.py +121 -0
- package/assets/.appweaver/scripts/python/aw-inventory.py +1180 -0
- package/assets/.appweaver/scripts/python/aw-parity.py +380 -0
- package/assets/.appweaver/scripts/python/aw-rev-constitution.py +329 -0
- package/assets/.appweaver/scripts/python/aw-stack.py +1251 -0
- package/assets/.appweaver/scripts/python/aw-xref.py +1330 -0
- package/assets/.appweaver/scripts/python/validate_okf.py +352 -0
- package/assets/.appweaver/stack-profiles/angular.json +16 -0
- package/assets/.appweaver/stack-profiles/blazor-wasm.json +17 -0
- package/assets/.appweaver/stack-profiles/dotnet-webapi.json +15 -0
- package/assets/.appweaver/stack-profiles/go-chi.json +14 -0
- package/assets/.appweaver/stack-profiles/java-spring.json +16 -0
- package/assets/.appweaver/stack-profiles/nextjs.json +16 -0
- package/assets/.appweaver/stack-profiles/node-express.json +15 -0
- package/assets/.appweaver/stack-profiles/python-fastapi.json +16 -0
- package/assets/.appweaver/stack-profiles/react-vite.json +17 -0
- package/assets/.appweaver/stack-profiles/sveltekit.json +16 -0
- package/assets/.appweaver/stack-profiles/vue-vite.json +17 -0
- package/assets/.appweaver/stacks.json +3 -0
- package/assets/.appweaver/templates/constitution-domain-template.md +116 -0
- package/assets/.appweaver/templates/constitution-technical-template.md +108 -0
- package/assets/.appweaver/templates/constitution-ui-template.md +116 -0
- package/assets/.appweaver/templates/delta-design-template.md +69 -0
- package/assets/.appweaver/templates/delta-spec-template.md +63 -0
- package/assets/.appweaver/templates/design-template.md +383 -0
- package/assets/.appweaver/templates/okf-concept-template.md +114 -0
- package/assets/.appweaver/templates/parity-manifest-template.json +49 -0
- package/assets/.appweaver/templates/source-map-template.md +85 -0
- package/assets/.appweaver/templates/spec-template.md +309 -0
- package/assets/.appweaver/templates/ui-design-template.md +273 -0
- package/assets/foundations/dotnet/ModularMonolith/ModularMonolith.Reference.pack.txt +25248 -0
- package/assets/foundations/dotnet/ModularMonolith/ModularMonolith.pack.txt +5726 -0
- package/assets/foundations/dotnet/ModularMonolith/pack-foundation.ps1 +145 -0
- package/assets/foundations/dotnet/ModularMonolith/pack-foundation.sh +196 -0
- package/assets/foundations/dotnet/ModularMonolith/pack-reference-foundation.ps1 +157 -0
- package/assets/foundations/dotnet/ModularMonolith/pack-reference-foundation.sh +209 -0
- package/assets/foundations/dotnet/ModularMonolith/unpack-foundation.ps1 +126 -0
- package/assets/foundations/dotnet/ModularMonolith/unpack-foundation.sh +217 -0
- package/assets/foundations/dotnet/ModularMonolith/unpack-reference-foundation.ps1 +129 -0
- package/assets/foundations/dotnet/ModularMonolith/unpack-reference-foundation.sh +218 -0
- package/assets/foundations/dotnet/VerticalSlice/VerticalSlice.Reference.pack.txt +19318 -0
- package/assets/foundations/dotnet/VerticalSlice/VerticalSlice.pack.txt +2833 -0
- package/assets/foundations/dotnet/VerticalSlice/pack-foundation.ps1 +145 -0
- package/assets/foundations/dotnet/VerticalSlice/pack-foundation.sh +196 -0
- package/assets/foundations/dotnet/VerticalSlice/pack-reference-foundation.ps1 +157 -0
- package/assets/foundations/dotnet/VerticalSlice/pack-reference-foundation.sh +209 -0
- package/assets/foundations/dotnet/VerticalSlice/unpack-foundation.ps1 +126 -0
- package/assets/foundations/dotnet/VerticalSlice/unpack-foundation.sh +217 -0
- package/assets/foundations/dotnet/VerticalSlice/unpack-reference-foundation.ps1 +129 -0
- package/assets/foundations/dotnet/VerticalSlice/unpack-reference-foundation.sh +218 -0
- package/assets/foundations/dotnet/components/Email/README.md +119 -0
- package/assets/foundations/dotnet/components/Email/ReferenceEndpoints/SendEmailEndpoint.cs +84 -0
- package/assets/foundations/dotnet/components/Email/integration.md +300 -0
- package/assets/foundations/dotnet/components/Jobs/README.md +113 -0
- package/assets/foundations/dotnet/components/Jobs/integration.md +259 -0
- package/assets/foundations/dotnet/components/QrGenerator/README.md +85 -0
- package/assets/foundations/dotnet/components/QrGenerator/ReferenceEndpoints/GenerateQrEndpoint.cs +57 -0
- package/assets/foundations/dotnet/components/QrGenerator/integration.md +230 -0
- package/assets/foundations/dotnet/components/README.md +74 -0
- package/assets/guardrails/README.md +222 -0
- package/assets/guardrails/domain/healthcare/ai-rules.domain.md +34 -0
- package/assets/guardrails/domain/healthcare/api-versioning.domain.md +17 -0
- package/assets/guardrails/domain/healthcare/auditing.domain.md +97 -0
- package/assets/guardrails/domain/healthcare/authorization.domain.md +26 -0
- package/assets/guardrails/domain/healthcare/clinical-terms.md +20 -0
- package/assets/guardrails/domain/healthcare/coding-standards.domain.md +26 -0
- package/assets/guardrails/domain/healthcare/consent.md +19 -0
- package/assets/guardrails/domain/healthcare/cqrs.domain.md +12 -0
- package/assets/guardrails/domain/healthcare/data-classification.domain.md +28 -0
- package/assets/guardrails/domain/healthcare/dependency-policy.domain.md +11 -0
- package/assets/guardrails/domain/healthcare/dicom.md +15 -0
- package/assets/guardrails/domain/healthcare/domain-constitution.md +119 -0
- package/assets/guardrails/domain/healthcare/domain-driven-design.domain.md +28 -0
- package/assets/guardrails/domain/healthcare/eventing.domain.md +18 -0
- package/assets/guardrails/domain/healthcare/fhir.md +36 -0
- package/assets/guardrails/domain/healthcare/folder-structure.domain.md +25 -0
- package/assets/guardrails/domain/healthcare/hipaa.md +34 -0
- package/assets/guardrails/domain/healthcare/hl7.md +15 -0
- package/assets/guardrails/domain/healthcare/ihe-conformance.md +31 -0
- package/assets/guardrails/domain/healthcare/integration-terms.md +21 -0
- package/assets/guardrails/domain/healthcare/persistence.domain.md +20 -0
- package/assets/guardrails/domain/healthcare/terminology.md +30 -0
- package/assets/guardrails/domain/healthcare/testing.domain.md +15 -0
- package/assets/guardrails/domain/healthcare/ux/accessibility-standards.md +36 -0
- package/assets/guardrails/domain/healthcare/ux/content-design.md +32 -0
- package/assets/guardrails/domain/healthcare/ux/interaction-states.md +44 -0
- package/assets/guardrails/domain/healthcare/ux/journey-mapping.md +40 -0
- package/assets/guardrails/domain/healthcare/vertical-slices.domain.md +18 -0
- package/assets/guardrails/domain/healthcare/zero-trust.domain.md +12 -0
- package/assets/guardrails/domain/pharma/ai-rules.domain.md +32 -0
- package/assets/guardrails/domain/pharma/api-versioning.domain.md +20 -0
- package/assets/guardrails/domain/pharma/atc.md +22 -0
- package/assets/guardrails/domain/pharma/auditing.domain.md +90 -0
- package/assets/guardrails/domain/pharma/authorization.domain.md +25 -0
- package/assets/guardrails/domain/pharma/coding-standards.domain.md +28 -0
- package/assets/guardrails/domain/pharma/cqrs.domain.md +16 -0
- package/assets/guardrails/domain/pharma/data-classification.domain.md +24 -0
- package/assets/guardrails/domain/pharma/data-provenance.md +21 -0
- package/assets/guardrails/domain/pharma/dependency-policy.domain.md +11 -0
- package/assets/guardrails/domain/pharma/domain-constitution.md +113 -0
- package/assets/guardrails/domain/pharma/domain-driven-design.domain.md +36 -0
- package/assets/guardrails/domain/pharma/domain-requirements.sample.md +103 -0
- package/assets/guardrails/domain/pharma/eventing.domain.md +18 -0
- package/assets/guardrails/domain/pharma/fhir.md +31 -0
- package/assets/guardrails/domain/pharma/folder-structure.domain.md +26 -0
- package/assets/guardrails/domain/pharma/hl7.md +17 -0
- package/assets/guardrails/domain/pharma/idmp.md +26 -0
- package/assets/guardrails/domain/pharma/integration-terms.md +21 -0
- package/assets/guardrails/domain/pharma/ndc.md +28 -0
- package/assets/guardrails/domain/pharma/persistence.domain.md +23 -0
- package/assets/guardrails/domain/pharma/pharma-terms.md +27 -0
- package/assets/guardrails/domain/pharma/product-lifecycle.domain.md +30 -0
- package/assets/guardrails/domain/pharma/regulatory-compliance.md +19 -0
- package/assets/guardrails/domain/pharma/terminology.md +32 -0
- package/assets/guardrails/domain/pharma/testing.domain.md +19 -0
- package/assets/guardrails/domain/pharma/vertical-slices.domain.md +17 -0
- package/assets/guardrails/domain/pharma/zero-trust.domain.md +12 -0
- package/assets/guardrails/projects/dotnet-default-project/domain/domain-requirements.md +58 -0
- package/assets/guardrails/projects/dotnet-default-project/domain-rule-bindings.md +113 -0
- package/assets/guardrails/projects/dotnet-default-project/frontend/ui-requirements.md +43 -0
- package/assets/guardrails/projects/dotnet-default-project/service/technical-requirements.md +40 -0
- package/assets/guardrails/projects/dotnet-default-project/tech-rule-bindings.md +263 -0
- package/assets/guardrails/requirement-spec.txt +27 -0
- package/assets/guardrails/technology/dotnet-webapi/PACKAGING.md +184 -0
- package/assets/guardrails/technology/dotnet-webapi/api-versioning.md +44 -0
- package/assets/guardrails/technology/dotnet-webapi/auditing.md +96 -0
- package/assets/guardrails/technology/dotnet-webapi/authorization-matrix.md +14 -0
- package/assets/guardrails/technology/dotnet-webapi/authorization.md +39 -0
- package/assets/guardrails/technology/dotnet-webapi/clean-architecture.md +54 -0
- package/assets/guardrails/technology/dotnet-webapi/coding-standards.md +87 -0
- package/assets/guardrails/technology/dotnet-webapi/cqrs.md +63 -0
- package/assets/guardrails/technology/dotnet-webapi/data-classification.md +38 -0
- package/assets/guardrails/technology/dotnet-webapi/dependency-policy.md +40 -0
- package/assets/guardrails/technology/dotnet-webapi/domain-driven-design.md +91 -0
- package/assets/guardrails/technology/dotnet-webapi/eventing.md +57 -0
- package/assets/guardrails/technology/dotnet-webapi/examples/api-versioning.md +75 -0
- package/assets/guardrails/technology/dotnet-webapi/examples/auditing.md +84 -0
- package/assets/guardrails/technology/dotnet-webapi/examples/authorization.md +68 -0
- package/assets/guardrails/technology/dotnet-webapi/examples/clean-architecture.md +37 -0
- package/assets/guardrails/technology/dotnet-webapi/examples/cqrs.md +97 -0
- package/assets/guardrails/technology/dotnet-webapi/examples/domain-driven-design.md +141 -0
- package/assets/guardrails/technology/dotnet-webapi/examples/eventing.md +46 -0
- package/assets/guardrails/technology/dotnet-webapi/examples/persistence.md +207 -0
- package/assets/guardrails/technology/dotnet-webapi/examples/vertical-slices.md +151 -0
- package/assets/guardrails/technology/dotnet-webapi/examples/zero-trust.md +48 -0
- package/assets/guardrails/technology/dotnet-webapi/folder-structure.md +61 -0
- package/assets/guardrails/technology/dotnet-webapi/modular-monolith.md +47 -0
- package/assets/guardrails/technology/dotnet-webapi/persistence.md +82 -0
- package/assets/guardrails/technology/dotnet-webapi/technology-constitution.md +162 -0
- package/assets/guardrails/technology/dotnet-webapi/testing.md +55 -0
- package/assets/guardrails/technology/dotnet-webapi/vertical-slices.md +70 -0
- package/assets/guardrails/technology/dotnet-webapi/zero-trust.md +48 -0
- package/assets/guardrails/technology/java-spring/README.md +62 -0
- package/assets/guardrails/technology/nextjs/accessibility-implementation.md +56 -0
- package/assets/guardrails/technology/nextjs/api-client.md +56 -0
- package/assets/guardrails/technology/nextjs/api-integration.dotnet-webapi.md +174 -0
- package/assets/guardrails/technology/nextjs/api-integration.java-spring.md +203 -0
- package/assets/guardrails/technology/nextjs/auth-session.md +68 -0
- package/assets/guardrails/technology/nextjs/client-security.md +69 -0
- package/assets/guardrails/technology/nextjs/component-architecture.md +60 -0
- package/assets/guardrails/technology/nextjs/component-conventions.md +58 -0
- package/assets/guardrails/technology/nextjs/design-system.md +60 -0
- package/assets/guardrails/technology/nextjs/error-handling.md +72 -0
- package/assets/guardrails/technology/nextjs/forms-and-validation.md +75 -0
- package/assets/guardrails/technology/nextjs/frontend-testing.md +94 -0
- package/assets/guardrails/technology/nextjs/i18n.md +57 -0
- package/assets/guardrails/technology/nextjs/observability.md +67 -0
- package/assets/guardrails/technology/nextjs/performance-budgets.md +66 -0
- package/assets/guardrails/technology/nextjs/routing-navigation.md +52 -0
- package/assets/guardrails/technology/nextjs/state-management.md +70 -0
- package/assets/guardrails/technology/nextjs/ui-constitution.md +166 -0
- package/package.json +1 -1
- package/assets/docs/pharma-constitution.my-ref.md +0 -86
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# Authorization Standards — canonical example
|
|
2
|
+
|
|
3
|
+
> The worked `<SampleProjectName>` example for
|
|
4
|
+
> `guardrails/technology/dotnet-webapi/authorization.md`. It is reference material for the phases
|
|
5
|
+
> that generate code — mapped for `design` — and carries no rule of its own: where this
|
|
6
|
+
> example and the rule file disagree, the rule file wins.
|
|
7
|
+
>
|
|
8
|
+
> It is deliberately absent from the `## /aw-spec-constitution` set. The constitution is a
|
|
9
|
+
> thin authority document — one declarative line and a `→` pointer per principle — and has no
|
|
10
|
+
> use for sample code.
|
|
11
|
+
|
|
12
|
+
## Canonical example (agent reference — `<SampleProjectName>` sample)
|
|
13
|
+
|
|
14
|
+
Secure endpoint — declare auth scheme + roles in `Configure()`, and the security/cache policy
|
|
15
|
+
in `GetSecurityCachePolicy()`:
|
|
16
|
+
|
|
17
|
+
```csharp
|
|
18
|
+
public override void Configure()
|
|
19
|
+
{
|
|
20
|
+
Post("/projects");
|
|
21
|
+
AuthSchemes("Bearer"); // JWT from the external IdP
|
|
22
|
+
Roles("Admin"); // role-based gate (combine with scope checks for FHIR — see .domain)
|
|
23
|
+
Summary(s => s.Summary = "Create a new project");
|
|
24
|
+
Options(o => o.WithTags("02 - Project - (FastEndpoints)"));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
protected override SecurityCachePolicy GetSecurityCachePolicy() => new()
|
|
28
|
+
{
|
|
29
|
+
SecurityLevel = SecurityLevel.Internal, // Internal | Restricted (admin/sensitive)
|
|
30
|
+
CachePolicy = CachePolicy.NoStore // NoStore for mutating/sensitive; PrivateCache for cacheable reads
|
|
31
|
+
};
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
> Mutating endpoints use `CachePolicy.NoStore`. Admin/audit endpoints (e.g. list-including-deleted)
|
|
35
|
+
> use `SecurityLevel.Restricted`. `AllowAnonymous()` is only for infrastructure/discovery routes
|
|
36
|
+
> or pre-merge scaffolding (the template ships endpoints scaffolded with `AllowAnonymous()` and
|
|
37
|
+
> the `AuthSchemes`/`Roles` lines commented — replace them before merging to a protected branch).
|
|
38
|
+
|
|
39
|
+
Composition — external IdP only (no local identity store), role-based authorization:
|
|
40
|
+
|
|
41
|
+
```csharp
|
|
42
|
+
// <SampleProjectName>.Service/Program.cs
|
|
43
|
+
builder.Services.AddAppWeaverSecurity(security =>
|
|
44
|
+
{
|
|
45
|
+
security
|
|
46
|
+
.AddAuthentication(auth =>
|
|
47
|
+
{
|
|
48
|
+
if (authProvider == "EntraExternalID")
|
|
49
|
+
auth.UseEntraExternalID(o => { o.TenantId = cfg["EntraExternalID:TenantId"]!;
|
|
50
|
+
o.ClientId = cfg["EntraExternalID:ClientId"]!; }, cfg);
|
|
51
|
+
else
|
|
52
|
+
auth.UseAzureAdB2C(o => { o.Domain = cfg["AzureAdB2C:Domain"]!;
|
|
53
|
+
o.ClientId = cfg["AzureAdB2C:ClientId"]!;
|
|
54
|
+
o.SignUpSignInPolicyId = cfg["AzureAdB2C:SignUpSignInPolicyId"]!; }, cfg);
|
|
55
|
+
})
|
|
56
|
+
.AddAuthorization(authz => authz.UseRoleBased(o => o.AdminRole = "Admin"));
|
|
57
|
+
// NOTE: no .AddIdentity(...) — the IdP is the source of truth (see [[zero-trust]])
|
|
58
|
+
}, options => { }, configuration: cfg);
|
|
59
|
+
|
|
60
|
+
// ... pipeline order ...
|
|
61
|
+
app.UseAuthentication();
|
|
62
|
+
app.UseAuthorization();
|
|
63
|
+
app.UseFastEndpoints();
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Related
|
|
67
|
+
|
|
68
|
+
- Rule file: `guardrails/technology/dotnet-webapi/authorization.md`
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Clean Architecture Standards — canonical example
|
|
2
|
+
|
|
3
|
+
> The worked `<SampleProjectName>` example for
|
|
4
|
+
> `guardrails/technology/dotnet-webapi/clean-architecture.md`. It is reference material for the
|
|
5
|
+
> phases that generate code — mapped for `design` and `build` — and carries no rule of its
|
|
6
|
+
> own: where this example and the rule file disagree, the rule file wins.
|
|
7
|
+
>
|
|
8
|
+
> It is deliberately absent from the `## /aw-spec-constitution` set. The constitution is a
|
|
9
|
+
> thin authority document — one declarative line and a `→` pointer per principle — and has no
|
|
10
|
+
> use for sample code.
|
|
11
|
+
|
|
12
|
+
## Canonical example (agent reference — `<SampleProjectName>` sample)
|
|
13
|
+
|
|
14
|
+
The dependency rule in practice — the **API** layer depends inward via `IMediator` only
|
|
15
|
+
(never `IRepository`/`DbContext`); the **Application** layer depends on Domain abstractions
|
|
16
|
+
(`IRepository<Project>`), never on concrete EF Core types:
|
|
17
|
+
|
|
18
|
+
```csharp
|
|
19
|
+
// API layer — <SampleProjectName>.Service/Projects/Features/CreateProject/CreateProjectEndpoint.cs
|
|
20
|
+
public class CreateProjectEndpoint(IMediator _mediator)
|
|
21
|
+
: SecureFastEndpoint<CreateProjectRequest, ProjectDto>
|
|
22
|
+
{
|
|
23
|
+
protected override Task<Result<ProjectDto>> ExecuteAsync(
|
|
24
|
+
CreateProjectRequest req, CancellationToken ct)
|
|
25
|
+
=> _mediator.Send(new CreateProjectCommand(
|
|
26
|
+
req.Code, req.Name, req.Description, req.StartDate, req.EndDate, req.OwnerId), ct);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Application layer — depends on Domain abstractions, not EF Core
|
|
30
|
+
public class CreateProjectCommandHandler(
|
|
31
|
+
IRepository<Project> _repo, IUnitOfWork _uow, IPublisher _eventBus)
|
|
32
|
+
: ICommandHandler<CreateProjectCommand, Result<ProjectDto>> { /* ... */ }
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Related
|
|
36
|
+
|
|
37
|
+
- Rule file: `guardrails/technology/dotnet-webapi/clean-architecture.md`
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# CQRS + Mediator Standards — canonical example
|
|
2
|
+
|
|
3
|
+
> The worked `<SampleProjectName>` example for `guardrails/technology/dotnet-webapi/cqrs.md`. It
|
|
4
|
+
> is reference material for the phases that generate code — mapped for `design` — and carries
|
|
5
|
+
> no rule of its own: where this example and the rule file disagree, the rule file wins.
|
|
6
|
+
>
|
|
7
|
+
> It is deliberately absent from the `## /aw-spec-constitution` set. The constitution is a
|
|
8
|
+
> thin authority document — one declarative line and a `→` pointer per principle — and has no
|
|
9
|
+
> use for sample code.
|
|
10
|
+
|
|
11
|
+
## Canonical example (agent reference — `<SampleProjectName>` sample)
|
|
12
|
+
|
|
13
|
+
Command as a `record`, write handler with transaction + event publication, and a read handler
|
|
14
|
+
returning `Result.Failure(...)` for the expected not-found outcome (no exceptions):
|
|
15
|
+
|
|
16
|
+
```csharp
|
|
17
|
+
// Command — record implementing ICommand<Result<TDto>>
|
|
18
|
+
public record CreateProjectCommand(
|
|
19
|
+
string Code, string Name, string? Description,
|
|
20
|
+
DateOnly StartDate, DateOnly? EndDate, Guid? OwnerId
|
|
21
|
+
) : ICommand<Result<ProjectDto>>;
|
|
22
|
+
|
|
23
|
+
// Write handler — primary-ctor DI, transaction, publish + clear events, Result<T>
|
|
24
|
+
public class CreateProjectCommandHandler(
|
|
25
|
+
IRepository<Project> _repo, IUnitOfWork _uow, IPublisher _eventBus
|
|
26
|
+
) : ICommandHandler<CreateProjectCommand, Result<ProjectDto>>
|
|
27
|
+
{
|
|
28
|
+
public async ValueTask<Result<ProjectDto>> Handle(
|
|
29
|
+
CreateProjectCommand request, CancellationToken ct)
|
|
30
|
+
=> await _uow.ExecuteInTransactionAsync(async () =>
|
|
31
|
+
{
|
|
32
|
+
var ownerId = request.OwnerId.HasValue
|
|
33
|
+
? TaskUserId.From(request.OwnerId.Value) : (TaskUserId?)null;
|
|
34
|
+
var entity = new Project(request.Code, request.Name, request.StartDate, ownerId);
|
|
35
|
+
|
|
36
|
+
await _repo.AddAsync(entity, ct);
|
|
37
|
+
await _uow.SaveChangesAsync(ct);
|
|
38
|
+
|
|
39
|
+
foreach (var e in entity.DomainEvents) await _eventBus.Publish(e, ct);
|
|
40
|
+
entity.ClearDomainEvents();
|
|
41
|
+
|
|
42
|
+
return Result<ProjectDto>.Ok(entity.ToDto());
|
|
43
|
+
}, IsolationLevel.ReadCommitted, ct);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Read handler — IReadRepository + Specification; not-found is a Result, not an exception
|
|
47
|
+
public class GetProjectByIdQueryHandler(IReadRepository<Project> _repo)
|
|
48
|
+
: IQueryHandler<GetProjectByIdQuery, Result<ProjectDto>>
|
|
49
|
+
{
|
|
50
|
+
public async ValueTask<Result<ProjectDto>> Handle(
|
|
51
|
+
GetProjectByIdQuery request, CancellationToken ct)
|
|
52
|
+
{
|
|
53
|
+
Guard.NotDefault(request.Id, nameof(request.Id));
|
|
54
|
+
var entity = await _repo.FirstOrDefaultAsync(
|
|
55
|
+
new ProjectWithTasksSpec(ProjectId.From(request.Id)), ct);
|
|
56
|
+
|
|
57
|
+
return entity is null
|
|
58
|
+
? Result<ProjectDto>.Failure(new Error(
|
|
59
|
+
"project.not_found", $"Project {request.Id} not found", 404, "projects"))
|
|
60
|
+
: Result<ProjectDto>.Ok(entity.ToDto());
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Commands with no return payload use `ICommand<Result>` (e.g. delete) and return
|
|
66
|
+
`Result.Success()` / `Result.Failure(...)`:
|
|
67
|
+
|
|
68
|
+
```csharp
|
|
69
|
+
public record DeleteProjectCommand(Guid Id, string RowVersionBase64) : ICommand<Result>;
|
|
70
|
+
|
|
71
|
+
public class DeleteProjectCommandHandler(IRepository<Project> _repo, IUnitOfWork _uow, IPublisher _bus)
|
|
72
|
+
: ICommandHandler<DeleteProjectCommand, Result> { /* ... returns Result.Success() */ }
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Composition — register the mediator with the logging + validation pipeline behaviors,
|
|
76
|
+
the validators, and the in-process event bus (one-time startup wiring):
|
|
77
|
+
|
|
78
|
+
```csharp
|
|
79
|
+
// <SampleProjectName>.Service/Extensions/ApplicationServiceRegistration.cs
|
|
80
|
+
services.AddMediator(builder =>
|
|
81
|
+
{
|
|
82
|
+
builder.UseDefault()
|
|
83
|
+
.UseBehaviors()
|
|
84
|
+
.AddLoggingBehavior()
|
|
85
|
+
.AddFluentValidationBehavior(); // domain projects also add PHI-audit — see [[cqrs.domain]]
|
|
86
|
+
}, options => options.Provider = "Default");
|
|
87
|
+
|
|
88
|
+
services.AddAppWeaverMediatorHandlersFrom(typeof(ApplicationServiceRegistration));
|
|
89
|
+
services.AddValidatorsFromAssembly(typeof(ApplicationServiceRegistration).Assembly);
|
|
90
|
+
|
|
91
|
+
services.AddEventBus(b => b.UseDefaultEventBus(o =>
|
|
92
|
+
o.RegisterServicesFromAssembly(typeof(ApplicationServiceRegistration).Assembly)));
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Related
|
|
96
|
+
|
|
97
|
+
- Rule file: `guardrails/technology/dotnet-webapi/cqrs.md`
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# Domain-Driven Design Standards (Tactical Patterns) — canonical example
|
|
2
|
+
|
|
3
|
+
> The worked `<SampleProjectName>` example for
|
|
4
|
+
> `guardrails/technology/dotnet-webapi/domain-driven-design.md`. It is reference material for the
|
|
5
|
+
> phases that generate code — mapped for `specify` — and carries no rule of its own: where
|
|
6
|
+
> this example and the rule file disagree, the rule file wins.
|
|
7
|
+
>
|
|
8
|
+
> It is deliberately absent from the `## /aw-spec-constitution` set. The constitution is a
|
|
9
|
+
> thin authority document — one declarative line and a `→` pointer per principle — and has no
|
|
10
|
+
> use for sample code.
|
|
11
|
+
|
|
12
|
+
## Canonical example (agent reference — `<SampleProjectName>` sample)
|
|
13
|
+
|
|
14
|
+
Aggregate root with a strongly-typed Vogen ID, owned child collection, concurrency token,
|
|
15
|
+
domain events, and an invariant enforced **inside** the aggregate:
|
|
16
|
+
|
|
17
|
+
```csharp
|
|
18
|
+
// <SampleProjectName>.Service/Projects/Domain/Project.cs
|
|
19
|
+
[ValueObject<Guid>]
|
|
20
|
+
public partial struct ProjectId { }
|
|
21
|
+
|
|
22
|
+
public class Project :
|
|
23
|
+
EntityBase<ProjectId>, IAggregateRoot, IFullAuditedObject, IFieldAudited,
|
|
24
|
+
ISoftDeletable, IConcurrencyTracked, IHasDomainEvents, INamedEntity<ProjectId>
|
|
25
|
+
{
|
|
26
|
+
private readonly List<ProjectTask> _tasks = new();
|
|
27
|
+
|
|
28
|
+
public string Name { get; set; } = default!; // INamedEntity
|
|
29
|
+
public ProjectCode Code { get; private set; } = default!; // value object
|
|
30
|
+
public ProjectStatus Status { get; private set; }
|
|
31
|
+
public TaskUserId? OwnerId { get; private set; } // cross-aggregate ref by ID only
|
|
32
|
+
public IReadOnlyCollection<ProjectTask> Tasks => _tasks.AsReadOnly();
|
|
33
|
+
|
|
34
|
+
public byte[] RowVersion { get; set; } = Array.Empty<byte>(); // IConcurrencyTracked
|
|
35
|
+
|
|
36
|
+
private readonly List<DomainEventBase> _domainEvents = new();
|
|
37
|
+
[NotMapped] public IEnumerable<DomainEventBase> DomainEvents => _domainEvents.AsReadOnly();
|
|
38
|
+
public void ClearDomainEvents() => _domainEvents.Clear();
|
|
39
|
+
|
|
40
|
+
private Project() { } // EF Core ctor
|
|
41
|
+
|
|
42
|
+
public Project(string code, string name, DateOnly startDate, TaskUserId? ownerId)
|
|
43
|
+
{
|
|
44
|
+
Id = ProjectId.From(Guid.NewGuid());
|
|
45
|
+
Code = ProjectCode.From(code); // VO validates in its ctor
|
|
46
|
+
Name = ProjectTitle.From(name).Value;
|
|
47
|
+
Status = ProjectStatus.Planning;
|
|
48
|
+
OwnerId = ownerId;
|
|
49
|
+
AddDomainEvent(new ProjectCreatedEvent(Id.Value, Code.Value, Name));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Invariant lives in the aggregate — never in the handler/validator.
|
|
53
|
+
public ProjectTask AddTask(string title, TaskPriority priority)
|
|
54
|
+
{
|
|
55
|
+
if (Status is ProjectStatus.Cancelled or ProjectStatus.Archived)
|
|
56
|
+
throw new DomainException("Cannot add tasks to a Cancelled or Archived project.");
|
|
57
|
+
var task = new ProjectTask(Id, title, priority);
|
|
58
|
+
_tasks.Add(task);
|
|
59
|
+
AddDomainEvent(new TaskCreatedEvent(Id.Value, task.Id.Value, task.Title.Value));
|
|
60
|
+
return task;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
private void AddDomainEvent(DomainEventBase e) => _domainEvents.Add(e);
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Value object — sealed, private ctor, validates in the ctor, `From(...)` factory,
|
|
68
|
+
throws `DomainException`:
|
|
69
|
+
|
|
70
|
+
```csharp
|
|
71
|
+
// <SampleProjectName>.Service/Projects/Domain/ProjectCode.cs
|
|
72
|
+
public sealed class ProjectCode : IEquatable<ProjectCode>
|
|
73
|
+
{
|
|
74
|
+
private static readonly Regex _valid = new(@"^[A-Z0-9][A-Z0-9\-]{1,14}$", RegexOptions.Compiled);
|
|
75
|
+
public string Value { get; private set; } = default!;
|
|
76
|
+
|
|
77
|
+
private ProjectCode() { }
|
|
78
|
+
private ProjectCode(string value)
|
|
79
|
+
{
|
|
80
|
+
if (string.IsNullOrWhiteSpace(value))
|
|
81
|
+
throw new DomainException("Project code cannot be empty.");
|
|
82
|
+
var trimmed = value.Trim().ToUpperInvariant();
|
|
83
|
+
if (!_valid.IsMatch(trimmed))
|
|
84
|
+
throw new DomainException("Project code must be uppercase alphanumeric with hyphens.");
|
|
85
|
+
Value = trimmed;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
public static ProjectCode From(string value) => new(value);
|
|
89
|
+
public bool Equals(ProjectCode? o) => o is not null && Value == o.Value;
|
|
90
|
+
public override bool Equals(object? obj) => Equals(obj as ProjectCode);
|
|
91
|
+
public override int GetHashCode() => HashCode.Combine(Value);
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Child entity — implements `IAggregateChild`, exposes `GetAggregateRoot()`, has only `private`
|
|
96
|
+
setters, and is mutated **only** through methods the parent aggregate calls:
|
|
97
|
+
|
|
98
|
+
```csharp
|
|
99
|
+
// <SampleProjectName>.Service/Projects/Domain/ProjectTask.cs
|
|
100
|
+
[ValueObject<Guid>]
|
|
101
|
+
public partial struct ProjectTaskId { }
|
|
102
|
+
|
|
103
|
+
public class ProjectTask : EntityBase<ProjectTaskId>, IAggregateChild
|
|
104
|
+
{
|
|
105
|
+
public ProjectId ProjectId { get; private set; } // FK back to the root
|
|
106
|
+
public TaskTitle Title { get; private set; } = default!; // value object
|
|
107
|
+
public ProjectTaskStatus Status { get; private set; }
|
|
108
|
+
public TaskUserId? AssigneeId { get; private set; } // cross-aggregate ref by ID
|
|
109
|
+
|
|
110
|
+
public Project Project { get; private set; } = default!; // EF-only nav, private setter
|
|
111
|
+
public object GetAggregateRoot() => Project; // IAggregateChild
|
|
112
|
+
|
|
113
|
+
private ProjectTask() { } // EF Core ctor
|
|
114
|
+
public ProjectTask(ProjectId projectId, string title, TaskPriority priority)
|
|
115
|
+
{
|
|
116
|
+
Id = ProjectTaskId.From(Guid.NewGuid());
|
|
117
|
+
ProjectId = projectId;
|
|
118
|
+
Title = TaskTitle.From(title);
|
|
119
|
+
Status = ProjectTaskStatus.Open;
|
|
120
|
+
Priority = priority;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// State-machine invariant lives on the child; the parent calls it (project.ChangeTaskStatus).
|
|
124
|
+
public void ChangeStatus(ProjectTaskStatus newStatus)
|
|
125
|
+
{
|
|
126
|
+
if (Status == ProjectTaskStatus.Cancelled)
|
|
127
|
+
throw new DomainException("A cancelled task cannot be re-opened.");
|
|
128
|
+
Status = newStatus;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
public void AssignTo(TaskUserId assigneeId) => AssigneeId = assigneeId;
|
|
132
|
+
}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
> The aggregate root mutates children only through its own methods (e.g. `Project.AssignTask`
|
|
136
|
+
> calls `task.AssignTo(...)` and raises the domain event) — callers never touch `ProjectTask`
|
|
137
|
+
> directly. EF mapping for the owned collection is in [[persistence]].
|
|
138
|
+
|
|
139
|
+
## Related
|
|
140
|
+
|
|
141
|
+
- Rule file: `guardrails/technology/dotnet-webapi/domain-driven-design.md`
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Eventing Standards — canonical example
|
|
2
|
+
|
|
3
|
+
> The worked `<SampleProjectName>` example for `guardrails/technology/dotnet-webapi/eventing.md`.
|
|
4
|
+
> It is reference material for the phases that generate code — mapped for `design` — and
|
|
5
|
+
> carries no rule of its own: where this example and the rule file disagree, the rule file
|
|
6
|
+
> wins.
|
|
7
|
+
>
|
|
8
|
+
> It is deliberately absent from the `## /aw-spec-constitution` set. The constitution is a
|
|
9
|
+
> thin authority document — one declarative line and a `→` pointer per principle — and has no
|
|
10
|
+
> use for sample code.
|
|
11
|
+
|
|
12
|
+
## Canonical example (agent reference — `<SampleProjectName>` sample)
|
|
13
|
+
|
|
14
|
+
Immutable domain event, raised inside the aggregate, published explicitly by the handler:
|
|
15
|
+
|
|
16
|
+
```csharp
|
|
17
|
+
// <SampleProjectName>.Service/Projects/Domain/Events/ProjectCreatedEvent.cs
|
|
18
|
+
public class ProjectCreatedEvent(Guid projectId, string code, string name) : DomainEventBase
|
|
19
|
+
{
|
|
20
|
+
public Guid ProjectId { get; } = projectId; // get-only — immutable
|
|
21
|
+
public string Code { get; } = code;
|
|
22
|
+
public string Name { get; } = name;
|
|
23
|
+
}
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
```csharp
|
|
27
|
+
// Raised inside the aggregate method (never in the handler):
|
|
28
|
+
AddDomainEvent(new ProjectCreatedEvent(Id.Value, Code.Value, Name));
|
|
29
|
+
|
|
30
|
+
// Published by the command handler after SaveChanges, then cleared:
|
|
31
|
+
foreach (var domainEvent in entity.DomainEvents)
|
|
32
|
+
await _eventBus.Publish(domainEvent, ct); // IPublisher — in-process today, broker tomorrow
|
|
33
|
+
entity.ClearDomainEvents();
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Composition — register the event bus (handlers discovered from the assembly):
|
|
37
|
+
|
|
38
|
+
```csharp
|
|
39
|
+
// <SampleProjectName>.Service/Extensions/ApplicationServiceRegistration.cs
|
|
40
|
+
services.AddEventBus(b => b.UseDefaultEventBus(o =>
|
|
41
|
+
o.RegisterServicesFromAssembly(typeof(ApplicationServiceRegistration).Assembly)));
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Related
|
|
45
|
+
|
|
46
|
+
- Rule file: `guardrails/technology/dotnet-webapi/eventing.md`
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
# Persistence Standards — canonical example
|
|
2
|
+
|
|
3
|
+
> The worked `<SampleProjectName>` example for
|
|
4
|
+
> `guardrails/technology/dotnet-webapi/persistence.md`. It is reference material for the phases
|
|
5
|
+
> that generate code — mapped for `design` and `run` — and carries no rule of its own: where
|
|
6
|
+
> this example and the rule file disagree, the rule file wins.
|
|
7
|
+
>
|
|
8
|
+
> It is deliberately absent from the `## /aw-spec-constitution` set. The constitution is a
|
|
9
|
+
> thin authority document — one declarative line and a `→` pointer per principle — and has no
|
|
10
|
+
> use for sample code.
|
|
11
|
+
|
|
12
|
+
## Canonical example (agent reference — `<SampleProjectName>` sample)
|
|
13
|
+
|
|
14
|
+
EF Core configuration (Vogen ID + value object conversions, unique business key,
|
|
15
|
+
cross-aggregate FK stored as a raw `Guid` with no navigation):
|
|
16
|
+
|
|
17
|
+
```csharp
|
|
18
|
+
// <SampleProjectName>.Service/Projects/Persistence/ProjectConfiguration.cs
|
|
19
|
+
public class ProjectConfiguration : IEntityTypeConfiguration<Project>
|
|
20
|
+
{
|
|
21
|
+
public void Configure(EntityTypeBuilder<Project> builder)
|
|
22
|
+
{
|
|
23
|
+
builder.ToTable("Projects");
|
|
24
|
+
builder.HasKey(e => e.Id);
|
|
25
|
+
|
|
26
|
+
// Strongly-typed Vogen ID -> Guid, never DB-generated
|
|
27
|
+
builder.Property(e => e.Id)
|
|
28
|
+
.HasConversion(id => id.Value, v => ProjectId.From(v))
|
|
29
|
+
.ValueGeneratedNever();
|
|
30
|
+
|
|
31
|
+
// Value object -> column + unique business key
|
|
32
|
+
builder.Property(e => e.Code)
|
|
33
|
+
.HasConversion(c => c.Value, v => ProjectCode.From(v))
|
|
34
|
+
.HasColumnName("Code").HasMaxLength(20).IsRequired();
|
|
35
|
+
builder.HasIndex(e => e.Code).IsUnique();
|
|
36
|
+
|
|
37
|
+
builder.Property(e => e.Status).HasConversion<string>().HasMaxLength(30).IsRequired();
|
|
38
|
+
|
|
39
|
+
// Cross-aggregate FK as nullable Guid — NO EF navigation to TaskUser
|
|
40
|
+
builder.Property(e => e.OwnerId)
|
|
41
|
+
.HasConversion(
|
|
42
|
+
id => id.HasValue ? id.Value.Value : (Guid?)null,
|
|
43
|
+
v => v.HasValue ? TaskUserId.From(v.Value) : (TaskUserId?)null)
|
|
44
|
+
.HasColumnName("OwnerId");
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Composable specification (filter + include + paging):
|
|
50
|
+
|
|
51
|
+
```csharp
|
|
52
|
+
// {Module}/Domain/Specifications/ProjectSearchSpec.cs
|
|
53
|
+
public sealed class ProjectSearchSpec : Specification<Project>
|
|
54
|
+
{
|
|
55
|
+
public ProjectSearchSpec(string? search, ProjectStatus? status = null)
|
|
56
|
+
{
|
|
57
|
+
Query.Include(p => p.Tasks);
|
|
58
|
+
if (!string.IsNullOrWhiteSpace(search))
|
|
59
|
+
{
|
|
60
|
+
var s = search.Trim().ToLowerInvariant();
|
|
61
|
+
Query.Where(p => p.Code.Value.ToLower().Contains(s) || p.Name.ToLower().Contains(s));
|
|
62
|
+
}
|
|
63
|
+
if (status.HasValue) Query.Where(p => p.Status == status.Value);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
public ProjectSearchSpec WithPaging(int pageNumber, int pageSize)
|
|
67
|
+
{
|
|
68
|
+
Query.Skip(Math.Max(0, (pageNumber - 1) * pageSize)).Take(Math.Max(1, pageSize));
|
|
69
|
+
return this;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Read handler using the spec + typed ordering helper, returning `PagedResult<T>`:
|
|
75
|
+
|
|
76
|
+
```csharp
|
|
77
|
+
var spec = new ProjectSearchSpec(request.Search, request.Status)
|
|
78
|
+
.OrderByIdDesc<ProjectSearchSpec, Project, ProjectId>(); // typed helper, not ad-hoc OrderBy
|
|
79
|
+
|
|
80
|
+
var total = await _repo.CountAsync(spec, ct);
|
|
81
|
+
var items = await _repo.ListAsync(spec.WithPaging(request.PageNumber, request.PageSize), ct);
|
|
82
|
+
|
|
83
|
+
return Result<PagedResult<ProjectDto>>.Ok(new PagedResult<ProjectDto>
|
|
84
|
+
{
|
|
85
|
+
Items = items.ToDtoList(), TotalCount = total,
|
|
86
|
+
PageNumber = request.PageNumber, PageSize = request.PageSize
|
|
87
|
+
});
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Owned child-entity configuration (separate table, FK + cascade, soft-delete mirror filter,
|
|
91
|
+
no EF navigation to other aggregates):
|
|
92
|
+
|
|
93
|
+
```csharp
|
|
94
|
+
// {Module}/Persistence/ProjectTaskConfiguration.cs
|
|
95
|
+
public class ProjectTaskConfiguration : IEntityTypeConfiguration<ProjectTask>
|
|
96
|
+
{
|
|
97
|
+
public void Configure(EntityTypeBuilder<ProjectTask> builder)
|
|
98
|
+
{
|
|
99
|
+
builder.ToTable("ProjectTasks");
|
|
100
|
+
builder.HasKey(e => e.Id);
|
|
101
|
+
builder.Property(e => e.Id)
|
|
102
|
+
.HasConversion(id => id.Value, v => ProjectTaskId.From(v)).ValueGeneratedNever();
|
|
103
|
+
|
|
104
|
+
builder.Property(e => e.ProjectId) // FK back to the root
|
|
105
|
+
.HasConversion(id => id.Value, v => ProjectId.From(v)).IsRequired();
|
|
106
|
+
|
|
107
|
+
builder.Property(e => e.Title) // Vogen value object
|
|
108
|
+
.HasConversion(t => t.Value, v => TaskTitle.From(v)).HasMaxLength(300).IsRequired();
|
|
109
|
+
builder.Property(e => e.Status).HasConversion<string>().HasMaxLength(30).IsRequired();
|
|
110
|
+
|
|
111
|
+
// Cross-aggregate ref stored as raw Guid — NO navigation to TaskUser
|
|
112
|
+
builder.Property(e => e.AssigneeId)
|
|
113
|
+
.HasConversion(
|
|
114
|
+
id => id.HasValue ? id.Value.Value : (Guid?)null,
|
|
115
|
+
v => v.HasValue ? TaskUserId.From(v.Value) : (TaskUserId?)null);
|
|
116
|
+
|
|
117
|
+
builder.HasOne(e => e.Project).WithMany(p => p.Tasks) // owned collection
|
|
118
|
+
.HasForeignKey(e => e.ProjectId).OnDelete(DeleteBehavior.Cascade);
|
|
119
|
+
|
|
120
|
+
// Mirror the parent's ISoftDeletable filter onto the required dependent
|
|
121
|
+
builder.HasQueryFilter(t => !t.Project.IsDeleted);
|
|
122
|
+
builder.HasIndex(e => e.ProjectId);
|
|
123
|
+
builder.HasIndex(e => e.AssigneeId);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
DbContext — `ContextAwareDbContext`, assembly-scanned configs, provider-aware `timestamptz`,
|
|
129
|
+
cross-cutting behaviors, and the client-managed concurrency token override:
|
|
130
|
+
|
|
131
|
+
```csharp
|
|
132
|
+
// <SampleProjectName>.Service/Infrastructure/ApplicationDbContext.cs
|
|
133
|
+
public class ApplicationDbContext(
|
|
134
|
+
DbContextOptions<ApplicationDbContext> options, ICurrentTenant? tenant = null)
|
|
135
|
+
: ContextAwareDbContext<ApplicationDbContext>(options, tenant)
|
|
136
|
+
{
|
|
137
|
+
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
|
138
|
+
{
|
|
139
|
+
modelBuilder.ApplyConfigurationsFromAssembly(typeof(ApplicationDbContext).Assembly);
|
|
140
|
+
|
|
141
|
+
if (Database.ProviderName == "Npgsql.EntityFrameworkCore.PostgreSQL")
|
|
142
|
+
foreach (var et in modelBuilder.Model.GetEntityTypes())
|
|
143
|
+
foreach (var p in et.GetProperties()
|
|
144
|
+
.Where(p => p.ClrType == typeof(DateTimeOffset) || p.ClrType == typeof(DateTimeOffset?)))
|
|
145
|
+
p.SetColumnType("timestamptz");
|
|
146
|
+
|
|
147
|
+
modelBuilder.ApplyCrossCuttingBehaviors(this); // soft-delete + tenant filters + audit
|
|
148
|
+
|
|
149
|
+
// Client-managed concurrency token (works on SQLite + PostgreSQL; not SQL Server IsRowVersion)
|
|
150
|
+
foreach (var et in modelBuilder.Model.GetEntityTypes())
|
|
151
|
+
if (typeof(IConcurrencyTracked).IsAssignableFrom(et.ClrType))
|
|
152
|
+
modelBuilder.Entity(et.ClrType).Property<byte[]>("RowVersion")
|
|
153
|
+
.IsConcurrencyToken().ValueGeneratedNever();
|
|
154
|
+
|
|
155
|
+
base.OnModelCreating(modelBuilder);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
public DbSet<Project> Projects => Set<Project>();
|
|
159
|
+
public DbSet<ProjectTask> ProjectTasks => Set<ProjectTask>();
|
|
160
|
+
}
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Soft-delete: logical delete via `MarkAsDeleted(...)` + `DeleteAsync` (never a hard delete),
|
|
164
|
+
and a separate `IgnoreQueryFilters()` spec for admin/audit reads:
|
|
165
|
+
|
|
166
|
+
```csharp
|
|
167
|
+
// Delete handler — soft delete + concurrency guard (returns Result, no DTO)
|
|
168
|
+
var entity = await _repo.GetByIdAsync(ProjectId.From(request.Id), ct);
|
|
169
|
+
if (entity is null)
|
|
170
|
+
return Result.Failure(new Error("project.not_found", $"Project {request.Id} not found", 404, "projects"));
|
|
171
|
+
// ... concurrency token check (see api-versioning) ...
|
|
172
|
+
entity.MarkAsDeleted(); // sets IsDeleted/DeletedOn + raises ProjectDeletedEvent
|
|
173
|
+
await _repo.DeleteAsync(entity, ct); // framework translates to UPDATE, not DELETE
|
|
174
|
+
|
|
175
|
+
// Admin/audit spec that bypasses the global soft-delete filter
|
|
176
|
+
public sealed class ProjectSearchIncludeDeletedSpec : Specification<Project>
|
|
177
|
+
{
|
|
178
|
+
public ProjectSearchIncludeDeletedSpec(string? search, ProjectStatus? status = null)
|
|
179
|
+
{
|
|
180
|
+
Query.Include(p => p.Tasks).IgnoreQueryFilters(); // include soft-deleted rows
|
|
181
|
+
// ... same Where filters as ProjectSearchSpec ...
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
Composition — repository + cross-cutting behaviors registered once at startup:
|
|
187
|
+
|
|
188
|
+
```csharp
|
|
189
|
+
// <SampleProjectName>.Service/Extensions/InfrastructureServiceRegistration.cs
|
|
190
|
+
services.AddRepositories(builder =>
|
|
191
|
+
{
|
|
192
|
+
builder
|
|
193
|
+
.UseSingleDbContextEFRepositories<ApplicationDbContext>(db => db.UseSqlite(/* or UseNpgsql */)
|
|
194
|
+
.ConfigureWarnings(w => w.Ignore(RelationalEventId.PendingModelChangesWarning)))
|
|
195
|
+
.EnableTenancy()
|
|
196
|
+
.EnableSoftDelete()
|
|
197
|
+
.EnableClientManagedConcurrency()
|
|
198
|
+
.EnableAuditing().EnableAdvancedAuditing() // see [[auditing]]
|
|
199
|
+
.Done();
|
|
200
|
+
}, options => options.Provider = "EfCore");
|
|
201
|
+
|
|
202
|
+
services.TryAddScoped(typeof(IUnitOfWork), typeof(EfUnitOfWork<ApplicationDbContext>));
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
## Related
|
|
206
|
+
|
|
207
|
+
- Rule file: `guardrails/technology/dotnet-webapi/persistence.md`
|