From cae3631128266e5f258a95e67a5e698312ff0bd9 Mon Sep 17 00:00:00 2001 From: mghabin <81494213+MohammadGhabin@users.noreply.github.com> Date: Thu, 30 Apr 2026 18:13:04 +0300 Subject: [PATCH] docs(structural): meta-docs sync per holistic critique MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - README/SCOPE: update baseline to .NET 10 / C# 14 / Aspire 9.x (net8/9) + Aspire 13 (net10), refresh advertised decision-tree list, route reading paths through decision-trees.md first. - glossary: add RequiredScope, DistributedApplicationTestingBuilder, Workload Identity, Federated Identity Credentials, slnx, Traversal SDK, dotnet-affected with primary-source citations. - coverage-map: name owners for ch02 sections (HttpClient, OutputCache, BackgroundWork, Security, gRPC, SignalR, HealthChecks endpoint mapping) and ch06 sections (Configuration, Resilience, HealthChecks probe contract, GracefulShutdown, CI/CD, Networking, Multi-tenancy, Cost). Weaken the per-line checklist deep-link claim to match reality (footer chapter list). - decision-trees: rewrite tree 12 (auth) to mirror ch02 §10 — separate delegated and app-only policies, no single OR-claims policy. Point tree 15 (resilience) at single owner ch02 §7. Convert all References lines to section anchors. Add tree 16 (Aspire scope: AppHost vs service) and tree 17 (Cosmos partition key) so the README list is honest. - patterns/anti-patterns: make 'God csproj' fix architecture-neutral — vertical slice, clean/onion, modular monolith all valid; rule is split at bounded-context seams with host as the only infra-referencing project. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- README.md | 16 +++-- SCOPE.md | 18 +++-- coverage-map.md | 37 +++++++--- docs/decision-trees.md | 142 ++++++++++++++++++++++++++++---------- glossary.md | 35 ++++++++++ patterns/anti-patterns.md | 13 ++-- 6 files changed, 198 insertions(+), 63 deletions(-) diff --git a/README.md b/README.md index dc26579..8d0f213 100644 --- a/README.md +++ b/README.md @@ -3,14 +3,16 @@ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](./LICENSE) [![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/mghabin/dotnet-engineering-guide/badge)](https://securityscorecards.dev/viewer/?uri=github.com/mghabin/dotnet-engineering-guide) -An opinionated **.NET 10 / C# 13 / Aspire 9.x** doctrine for senior backend engineers who own long-lived services in production, compiled from primary sources (Microsoft Learn, the .NET / ASP.NET / EF Core team blogs, `dotnet/runtime` docs, IETF RFCs, the OpenTelemetry spec) and a small set of named industry voices, with every chapter ending in a Sources block so you can follow the citations. +An opinionated **.NET 10 / C# 14 / Aspire** doctrine for senior backend engineers who own long-lived services in production, compiled from primary sources (Microsoft Learn, the .NET / ASP.NET / EF Core team blogs, `dotnet/runtime` docs, IETF RFCs, the OpenTelemetry spec) and a small set of named industry voices, with every chapter ending in a Sources block so you can follow the citations. + +Aspire baseline is split by target framework: **Aspire 9.x GA** for `net8.0` / `net9.0` services, **Aspire 13 GA** for `net10.0` services (the version line jumped 9.x → 13.0 to align with the .NET 10 wave and the rebrand to "Aspire" with docs at ). ## Read this first These five pages are the synthesis. The numbered chapters are doctrine you reach for *when the decision tree points you there*. -- [`docs/decision-trees.md`](./docs/decision-trees.md) — one-screen decision trees for the questions .NET teams actually argue about (Minimal APIs vs Controllers, EF Core vs Dapper, render mode, NativeAOT, Aspire scope, Cosmos partition key, cache tier). +- [`docs/decision-trees.md`](./docs/decision-trees.md) — one-screen decision trees for the questions .NET teams actually argue about (Minimal APIs vs Controllers, EF Core vs Dapper / Cosmos vs SQL, EF migration deploy, Blazor render mode, client platform, test type, assertion library, NativeAOT, GC, `IHttpClientFactory`, auth policy shape, cache tier, K8s QoS, resilience, Aspire scope, Cosmos partition key). - [`checklist.md`](./checklist.md) — one-page do/don't card for code review, every line a `must`-severity rule. - [`SCOPE.md`](./SCOPE.md) — who this guide is for, who it isn't, and the technical and organisational envelope the defaults assume. - [`glossary.md`](./glossary.md) — canonical, opinionated definitions for every term used normatively, with primary-source citations where the ecosystem disagrees. @@ -19,15 +21,15 @@ The numbered chapters are doctrine you reach for *when the decision tree points ## Chapters (doctrine) Read in depth when the decision tree or checklist points you here. -The numbered order also works as a linear read for engineers new to the .NET 10 / Aspire 9.x stack. +The numbered order also works as a linear read for engineers new to the .NET 10 / C# 14 / Aspire stack. -- [`docs/01-foundations.md`](./docs/01-foundations.md) — language baseline (C# 13, NRT, analyzers), the `async`/`await` rules (Cleary canon), DI lifetimes, and runtime configuration defaults for the Generic Host. -- [`docs/02-aspnetcore.md`](./docs/02-aspnetcore.md) — Minimal APIs as the default, ProblemDetails (RFC 9457) for errors, Microsoft Entra bearer auth with `scp` vs `roles` modelled correctly, and OpenTelemetry wired from day one. +- [`docs/01-foundations.md`](./docs/01-foundations.md) — language baseline (C# 14, NRT, analyzers), the `async`/`await` rules (Cleary canon), DI lifetimes, and runtime configuration defaults for the Generic Host. +- [`docs/02-aspnetcore.md`](./docs/02-aspnetcore.md) — Minimal APIs as the default, ProblemDetails (RFC 9457) for errors, Microsoft Entra bearer auth with `scp` vs `roles` modelled as separate delegated and app-only policies, and OpenTelemetry wired from day one. - [`docs/03-data.md`](./docs/03-data.md) — EF Core 10 patterns, Cosmos DB modelling, zero-downtime migrations (expand-contract), and the cache decision matrix from in-memory through HybridCache to CDN. - [`docs/04-testing.md`](./docs/04-testing.md) — xUnit v3 on Microsoft.Testing.Platform (MTP), `WebApplicationFactory` for in-process integration, Testcontainers for real dependencies, and Aspire `DistributedApplicationTestingBuilder` for full App Host coverage. - [`docs/05-performance.md`](./docs/05-performance.md) — BenchmarkDotNet methodology, allocation analysis with pooling (`Span`, `ArrayPool`, `RecyclableMemoryStream`), NativeAOT trade-offs, and server GC / DATAS tuning. -- [`docs/06-cloud-native.md`](./docs/06-cloud-native.md) — .NET Aspire 9.x as the local-orchestration and deployment-manifest authority, Kubernetes probes and QoS classes, service discovery, and Data Protection keyring management in cloud. -- [`docs/07-client.md`](./docs/07-client.md) — Blazor render-mode decision tables (Static SSR, Server, WASM, Auto), MAUI Window lifecycle, and MVVM with the CommunityToolkit source generators. +- [`docs/06-cloud-native.md`](./docs/06-cloud-native.md) — .NET Aspire (9.x for net8/9, 13 for net10) as the local-orchestration and deployment-manifest authority, Kubernetes probes and QoS classes, service discovery, and Data Protection keyring management in cloud. +- [`docs/07-client.md`](./docs/07-client.md) — Blazor render-mode decision tables (Static SSR, Server, WASM, Auto) on .NET 10 / C# 14, MAUI Window lifecycle, and MVVM with the CommunityToolkit source generators. ## Patterns diff --git a/SCOPE.md b/SCOPE.md index ea5b569..8d62898 100644 --- a/SCOPE.md +++ b/SCOPE.md @@ -13,12 +13,12 @@ If your situation falls outside this envelope, individual chapters may still be Concrete reader profiles. If two or more apply, you are the target reader. -- A senior or staff backend .NET engineer owning one or more long-lived services on .NET 10 / C# 13. +- A senior or staff backend .NET engineer owning one or more long-lived services on .NET 10 / C# 14. - A solutions or application architect choosing the default project, hosting, and data shape for a new bounded context. - A tech lead inheriting a .NET estate and asked to "make it boring" before scaling the team. - A platform engineer standardising `Directory.Build.props`, Central Package Management, analyzers, and CI templates across many .NET repos ([learn.microsoft.com/nuget/consume-packages/central-package-management](https://learn.microsoft.com/nuget/consume-packages/central-package-management)). - An engineer wiring a new ASP.NET Core service to AKS, App Service, or Azure Container Apps and picking between Minimal APIs and Controllers ([learn.microsoft.com/aspnet/core/fundamentals/minimal-apis/overview](https://learn.microsoft.com/aspnet/core/fundamentals/minimal-apis/overview)). -- An engineer adopting .NET Aspire 9.x for local orchestration, service discovery, and OTel defaults ([learn.microsoft.com/dotnet/aspire/whats-new/dotnet-aspire-9](https://learn.microsoft.com/dotnet/aspire/whats-new/dotnet-aspire-9)). +- An engineer adopting .NET Aspire for local orchestration, service discovery, and OTel defaults — Aspire 9.x for `net8.0` / `net9.0` services and Aspire 13 for `net10.0` services ([learn.microsoft.com/dotnet/aspire/whats-new/dotnet-aspire-9](https://learn.microsoft.com/dotnet/aspire/whats-new/dotnet-aspire-9), [aspire.dev](https://aspire.dev)). - An SRE or perf-focused engineer using BenchmarkDotNet, dotnet-counters, and PerfView to investigate allocations, GC, or NativeAOT trade-offs ([devblogs.microsoft.com/dotnet/performance-improvements-in-net-9](https://devblogs.microsoft.com/dotnet/performance-improvements-in-net-9)). --- @@ -30,7 +30,7 @@ Specifically: - **.NET 10 GA on the current LTS / STS cadence** ([learn.microsoft.com/dotnet/core/releases-and-support](https://learn.microsoft.com/dotnet/core/releases-and-support)). Older TFMs are mentioned only when guidance differs. -- **C# 13 language defaults**, NRT enabled, `TreatWarningsAsErrors=true`, analyzers on by default ([learn.microsoft.com/dotnet/csharp/whats-new/csharp-13](https://learn.microsoft.com/dotnet/csharp/whats-new/csharp-13)). +- **C# 14 language defaults** (the compiler default tied to `net10.0`), NRT enabled, `TreatWarningsAsErrors=true`, analyzers on by default ([learn.microsoft.com/dotnet/csharp/whats-new/csharp-14](https://learn.microsoft.com/dotnet/csharp/whats-new/csharp-14), [learn.microsoft.com/dotnet/csharp/language-reference/configure-language-version](https://learn.microsoft.com/dotnet/csharp/language-reference/configure-language-version)). - **Cloud as the deployment target.** AKS, Azure App Service, Azure Container Apps, and Azure Functions on the isolated worker model ([learn.microsoft.com/azure/azure-functions/dotnet-isolated-process-guide](https://learn.microsoft.com/azure/azure-functions/dotnet-isolated-process-guide)). On-prem IIS is not the default. - **Multi-team monorepos or bounded-context services**, not single-solution single-team codebases. @@ -107,19 +107,22 @@ Each path is ordered; do not skip steps. ### 6.1 First 90 days on a new .NET 10 service -- Start at [`patterns/anti-patterns.md`](./patterns/anti-patterns.md) for the decision trees implied by the rejected patterns. +- Start at [`docs/decision-trees.md`](./docs/decision-trees.md) — the synthesis entrypoint that routes you to the chapter that owns each decision. - Then [`checklist.md`](./checklist.md) as the one-page review card. +- Then [`patterns/anti-patterns.md`](./patterns/anti-patterns.md) for the rejected shapes you will see most often. - Then [`docs/01-foundations.md`](./docs/01-foundations.md) for solution layout, CPM, analyzers, async, and DI. ### 6.2 Architect choosing the shape of a new bounded context -- Start at [`patterns/anti-patterns.md`](./patterns/anti-patterns.md) and [`patterns/patterns.md`](./patterns/patterns.md) for the rejected and preferred shapes. +- Start at [`docs/decision-trees.md`](./docs/decision-trees.md) for Minimal vs Controllers, EF vs Dapper vs Cosmos, render mode, Aspire scope, Cosmos partition key. +- Then [`patterns/anti-patterns.md`](./patterns/anti-patterns.md) and [`patterns/patterns.md`](./patterns/patterns.md) for the rejected and preferred shapes. - Then [`docs/02-aspnetcore.md`](./docs/02-aspnetcore.md) for Minimal vs Controllers, ProblemDetails, versioning, OpenAPI, resilience. - Then [`docs/06-cloud-native.md`](./docs/06-cloud-native.md) for Aspire, AKS probes, container hygiene. ### 6.3 SRE / performance engineer chasing latency or cost -- Start at [`docs/05-performance.md`](./docs/05-performance.md) for BenchmarkDotNet methodology, allocation analysis, `Span`, pooling, source generators, NativeAOT. +- Start at [`docs/decision-trees.md`](./docs/decision-trees.md) for the NativeAOT, GC, resilience, QoS, and cache trees. +- Then [`docs/05-performance.md`](./docs/05-performance.md) for BenchmarkDotNet methodology, allocation analysis, `Span`, pooling, source generators, NativeAOT. - Then [`docs/06-cloud-native.md`](./docs/06-cloud-native.md) for OTel defaults, probes, limits, graceful shutdown. - Then [`checklist.md`](./checklist.md) for the perf-relevant review items. @@ -130,7 +133,8 @@ Each path is ordered; do not skip steps. - **Targets the current GA stable .NET.** At time of writing, .NET 10 (STS / LTS per [learn.microsoft.com/dotnet/core/releases-and-support](https://learn.microsoft.com/dotnet/core/releases-and-support)). When .NET 11 ships, guidance moves with it; the previous LTS is called out inline only where defaults differ. -- **Targets the current Aspire GA, currently 9.x** ([learn.microsoft.com/dotnet/aspire/whats-new/dotnet-aspire-9](https://learn.microsoft.com/dotnet/aspire/whats-new/dotnet-aspire-9)). +- **Targets the current Aspire GA, split by TFM**: Aspire 9.x for `net8.0` / `net9.0`, Aspire 13 for `net10.0` ([learn.microsoft.com/dotnet/aspire/whats-new/dotnet-aspire-9](https://learn.microsoft.com/dotnet/aspire/whats-new/dotnet-aspire-9), [aspire.dev](https://aspire.dev)). + The version line jumped 9.x → 13.0 to align with the .NET 10 wave, alongside the rebrand from "**.NET Aspire**" to "**Aspire**". - **Pre-GA features are labeled.** Anything behind a preview feature flag, an `[Experimental]` attribute, or a `RequiresPreviewFeatures` gate is marked `preview` inline and is not a default. - **EF Core, ASP.NET Core, and runtime release notes are the source of truth** for breaking changes between minor versions ([learn.microsoft.com/ef/core/what-is-new](https://learn.microsoft.com/ef/core/what-is-new), [learn.microsoft.com/aspnet/core/release-notes](https://learn.microsoft.com/aspnet/core/release-notes)). diff --git a/coverage-map.md b/coverage-map.md index 1cf211f..ee0ea73 100644 --- a/coverage-map.md +++ b/coverage-map.md @@ -67,10 +67,18 @@ Source: [`docs/02-aspnetcore.md`](./docs/02-aspnetcore.md). - `ProblemDetails` per RFC 9457 as the single error contract (`AddProblemDetails`, `IProblemDetailsService`, exception handler middleware). - API versioning policy (`Asp.Versioning.Http`, URL/segment vs header) and deprecation headers. - OpenAPI generation via `Microsoft.AspNetCore.OpenApi` (transformer pipeline, schema customization, replacing Swashbuckle). -- AuthN/AuthZ on the request pipeline: JWT bearer with Microsoft Entra, validation of `iss`/`aud`/`scp`/`roles`/`azp`, policy-based authorization. -- Antiforgery / CSRF handling for cookie-auth surfaces and Blazor server interactions on the API side. -- OpenTelemetry HTTP wiring on the request pipeline (`AddAspNetCoreInstrumentation`, `AddHttpClientInstrumentation`, propagation). -- Rate limiting middleware, request size limits, and Kestrel surface defaults relevant to APIs. +- AuthN/AuthZ on the request pipeline (§10): JWT bearer with Microsoft Entra, validation of `iss`/`aud`/`scp`/`roles`/`azp`, **separate delegated (`scp`) and app-only (`roles` + `azp` allow-list) policies** (no single OR-claims policy), CAE / claims-challenge wiring. +- Antiforgery / CSRF handling for cookie-auth surfaces and Blazor server interactions on the API side (§14 — Security). +- OpenTelemetry HTTP wiring on the request pipeline (§6 — `AddAspNetCoreInstrumentation`, `AddHttpClientInstrumentation`, propagation). +- **HTTP resilience for outbound calls** (§7): `IHttpClientFactory` + `AddStandardResilienceHandler` as the single owner of retry / timeout / circuit-breaker / hedging defaults. Chapter 06 references this section, does not re-decide. +- **`HttpClient` factory and typed clients** (§11): `AddHttpClient`, named vs typed, `SocketsHttpHandler` defaults, `PooledConnectionLifetime`. +- **OutputCache** (§9): HTTP-response caching middleware (`AddOutputCache`, policies, tag invalidation). Chapter 03's caching matrix links here for the OutputCache row. +- **Background work in the request host** (§12): `IHostedService` / `BackgroundService` patterns, `Channel` pipelines, graceful drain of in-flight work — composition lifetime owned here; the cluster-side drain contract is owned by [06 §11](#chapter-06--cloud-native). +- **Security middleware** (§14): HTTPS redirection, HSTS, security headers, antiforgery, request-size limits, forwarded headers (paired with §15). +- **gRPC surface** (§16): `MapGrpcService`, gRPC-Web, code-first vs proto-first, interceptors, deadlines. +- **SignalR surface** (§17): hub authorization, backplane choice, scaling rules, sticky sessions. +- **Health-check endpoint mapping** (§18): the in-process `MapHealthChecks` helpers and how the request host exposes them — but the **probe contract** (endpoint names, semantics, what a check may do) is owned by [06 §10](#chapter-06--cloud-native). +- Rate limiting middleware (§8), request size limits, and Kestrel surface defaults relevant to APIs. ### References (does not re-decide) @@ -188,13 +196,22 @@ Source: [`docs/06-cloud-native.md`](./docs/06-cloud-native.md). ### Owns -- .NET Aspire 9.x AppHost composition and client integrations (`Aspire.Hosting.*`, `Aspire.*`). -- `ServiceDefaults` shared project: OTel wiring, health checks, resilience handlers, service discovery defaults. +- .NET Aspire AppHost composition and client integrations — **Aspire 9.x** for `net8.0` / `net9.0` services, **Aspire 13** for `net10.0` services (`Aspire.Hosting.*`, `Aspire.*`). +- `ServiceDefaults` shared project (§1): OTel wiring, default health checks, resilience handlers, service discovery defaults. +- **Configuration in cluster** (§4): ConfigMap + CSI Key Vault driver as the configuration substrate; `appsettings.Production.json` rejected. Foundations §6 owns the in-process options pattern; this chapter owns where the bytes come from in the cluster. +- **Resilience pipeline defaults at the platform layer** (§6): Polly v8 standard-pipeline composition for non-HTTP resources (queues, blobs, repositories) and the rationale for keeping HTTP resilience in [02 §7](#chapter-02--aspnetcore). HTTP retry verb policy is owned by ch02; this chapter does not re-decide it. +- **Health-check probe contract** (§10): `/health/live`, `/health/ready`, `/health/startup` endpoint names, semantics, what each probe may do, and the K8s probe wiring — matches the Aspire `ServiceDefaults` mapping. Chapter 02 §18 only owns the in-process `MapHealthChecks` plumbing and links here for the contract. - Kubernetes runtime contract: liveness / readiness / startup probes, QoS class, the CPU-limits-considered-harmful stance, requests sizing. -- Service discovery via `Microsoft.Extensions.ServiceDiscovery` and `HttpClient` integration. -- OTLP exporters (traces, metrics, logs) and collector topology assumptions. -- ASP.NET Core Data Protection key-ring storage (Azure Blob + Key Vault, or equivalent) for multi-replica deployments. +- Service discovery via `Microsoft.Extensions.ServiceDiscovery` and `HttpClient` integration (§7). +- **Networking** (§13): HTTP/2 + HTTP/3 enablement, forwarded headers in cluster, mTLS via service mesh, TLS termination boundary. +- OTLP exporters (traces, metrics, logs) and collector topology assumptions (§5). +- **Secrets & identity** (§8): Workload Identity + Federated Identity Credentials only — no client secrets, no pod-identity v1, no service-principal passwords on disk. +- ASP.NET Core Data Protection key-ring storage (Azure Blob + Key Vault, or equivalent) for multi-replica deployments (§9). +- **Graceful shutdown** (§11): `IHostApplicationLifetime`, `preStop` hook, drain order, K8s `terminationGracePeriodSeconds`. Chapter 02's background-work section composes against this contract. - Outbox **dispatcher** topology (hosted service / sidecar / worker pool) — the pattern itself is owned by [03](#chapter-03--data). +- **CI/CD** (§12): reproducible image build, Sigstore/cosign signing, SBOM, scanning gate. Source-build hygiene is owned by [01 §11](#chapter-01--foundations); this chapter owns the cluster-deployment pipeline. +- **Multi-tenancy at runtime** (§14): tenant-scoped DI, per-tenant configuration / connection-string resolution, isolation expectations at the cluster boundary. +- **Cost & efficiency** (§15): right-sizing requests, scale-to-zero suitability, image size baseline, the cluster-level levers a service team owns. - Dockerfile baseline for .NET 10 images, including the runtime env-vars selected in [05](#chapter-05--performance). ### References (does not re-decide) @@ -276,7 +293,7 @@ Source: [`checklist.md`](./checklist.md). ### References (does not re-decide) -- Every checklist line links to the owning chapter for rationale; the rule itself lives in the chapter. +- The card itself does not encode rationale; each line restates a rule whose canonical wording lives in the owning chapter listed in the checklist's footer chapter index. - `should` / `prefer` / `avoid` nuance lives in chapters, not on the card. ### Cross-links diff --git a/docs/decision-trees.md b/docs/decision-trees.md index 0087680..61a108d 100644 --- a/docs/decision-trees.md +++ b/docs/decision-trees.md @@ -33,7 +33,7 @@ flowchart TD E -->|No| G[Stay Minimal — ch02] ``` -References: `docs/02-aspnetcore.md`. +References: [`docs/02-aspnetcore.md#1-minimal-apis-vs-controllers`](./02-aspnetcore.md#1-minimal-apis-vs-controllers). --- @@ -59,7 +59,7 @@ flowchart TD F --> G ``` -References: `docs/03-data.md`. +References: [`docs/03-data.md#1-choosing-the-tool`](./03-data.md#1-choosing-the-tool). --- @@ -85,7 +85,7 @@ flowchart TD G -->|No| F ``` -References: `docs/03-data.md`. +References: [`docs/03-data.md#1-choosing-the-tool`](./03-data.md#1-choosing-the-tool), [`docs/03-data.md#8-provider-notes`](./03-data.md#8-provider-notes). --- @@ -112,7 +112,7 @@ flowchart TD G -->|No| E ``` -References: `docs/03-data.md`. +References: [`docs/03-data.md#4-migrations`](./03-data.md#4-migrations). --- @@ -139,7 +139,7 @@ flowchart TD D -->|Mixed, want first paint fast
then offload| H[Interactive Auto
only if both modes supported — ch07] ``` -References: `docs/07-client.md`. +References: [`docs/07-client.md#part-1--blazor-unified-web-app-model`](./07-client.md#part-1--blazor-unified-web-app-model). --- @@ -167,7 +167,7 @@ flowchart TD D -->|One platform dominates,
platform-only APIs| I[Native SDK — ch07] ``` -References: `docs/07-client.md`. +References: [`docs/07-client.md#part-2--maui-net-10`](./07-client.md#part-2--maui-net-10). --- @@ -192,7 +192,7 @@ flowchart TD D -->|Multi-service composition,
Aspire AppHost in scope| G[DistributedApplicationTestingBuilder — ch04] ``` -References: `docs/04-testing.md`. +References: [`docs/04-testing.md#5-webapplicationfactory-for-aspnet-core`](./04-testing.md#5-webapplicationfactory-for-aspnet-core), [`docs/04-testing.md#6-testcontainers`](./04-testing.md#6-testcontainers). --- @@ -219,7 +219,7 @@ flowchart TD D -->|Also switching runner| I[TUnit + its assertions — ch04] ``` -References: `docs/04-testing.md`. +References: [`docs/04-testing.md#3-assertions`](./04-testing.md#3-assertions). --- @@ -244,7 +244,7 @@ flowchart TD F -->|No| C ``` -References: `docs/05-performance.md`. +References: [`docs/05-performance.md#8-jit--aot`](./05-performance.md#8-jit--aot). --- @@ -267,7 +267,7 @@ flowchart TD B -->|Sidecar / small container
< 1 vCPU, tight RAM| D ``` -References: `docs/05-performance.md`. +References: [`docs/05-performance.md#9-gc`](./05-performance.md#9-gc). --- @@ -290,32 +290,39 @@ flowchart TD D -->|No, multiple endpoints / policies| C ``` -References: `docs/01-foundations.md`. +References: [`docs/01-foundations.md#5-di--lifetimes`](./01-foundations.md#5-di--lifetimes), [`docs/02-aspnetcore.md#11-httpclient`](./02-aspnetcore.md#11-httpclient). --- -## 12. Auth policy shape: scp vs roles vs combined +## 12. Auth policy shape: delegated (`scp`) vs app-only (`roles` + `azp`) - Trigger: protecting an endpoint that may be hit by delegated users, app-only callers, or both. -- Cost of wrong call: a single "is authenticated" check that lets an - app-only token reach a user-only endpoint, or a `scp`-only policy that - silently rejects every daemon caller. -- Default per `ch02`: separate policies for delegated (`scp`) and app-only - (`roles`); a combined policy only when both flows are intentional. +- Cost of wrong call: a single OR-claims policy that lets an app-only token + reach a user-only endpoint (no `azp` allow-list, no `scp` check), or a + user token satisfy an app-only endpoint by carrying an unrelated `scp`. + Both are real privilege-escalation bugs in production APIs. +- Default per `ch02` §10: **two separate named policies** — one delegated + (requires `scp` and a specific scope, rejects tokens that carry `roles` + but no `scp`), one app-only (requires `roles`, an `azp`/`appid` in an + allow-list, and the **absence** of `scp`). For endpoints that legitimately + accept both, list both policies on the endpoint + (`RequireAuthorization("OrdersReadDelegated", "OrdersReadApp")`); each + policy still enforces its own invariants. **Never** a single assertion + that ORs `scp` and `roles`. ```mermaid flowchart TD A[New protected endpoint] --> B{Caller identity model?} - B -->|Delegated user only| C[Policy requires scp claim — ch02] - B -->|App-only / daemon only| D[Policy requires roles claim — ch02] - B -->|Both flows in scope| E[Combined policy
require scp OR roles — ch02] - C --> F[Reject tokens missing scp — ch02] - D --> G[Reject tokens missing roles — ch02] - E --> H[Document which paths
each flow may reach — ch02] + B -->|Delegated user only| C[Policy requires scp + scope
reject if roles present without scp — ch02 §10] + B -->|App-only / daemon only| D[Policy requires roles
+ azp / appid allow-list
+ no scp — ch02 §10] + B -->|Both flows in scope| E[Compose two named policies
on the endpoint — never OR claims — ch02 §10] + C --> F[Reject tokens missing scp — ch02 §10] + D --> G[Reject tokens missing roles or azp — ch02 §10] + E --> H[Each policy enforces its own invariants;
document which paths each flow may reach — ch02 §10] ``` -References: `docs/02-aspnetcore.md`. +References: [`docs/02-aspnetcore.md#10-authnauthz`](./02-aspnetcore.md#10-authnauthz). --- @@ -337,7 +344,7 @@ flowchart TD B -->|Plain distributed K/V
session, idempotency keys| E[IDistributedCache — ch03] ``` -References: `docs/03-data.md`. +References: [`docs/03-data.md#14-caching`](./03-data.md#14-caching), [`docs/02-aspnetcore.md#9-output-caching`](./02-aspnetcore.md#9-output-caching). --- @@ -362,7 +369,7 @@ flowchart TD F -->|Yes| G[BestEffort — never in prod — ch06] ``` -References: `docs/06-cloud-native.md`. +References: [`docs/06-cloud-native.md#3-kubernetes--aks--probes-limits-gc-shutdown`](./06-cloud-native.md#3-kubernetes--aks--probes-limits-gc-shutdown). --- @@ -372,22 +379,87 @@ References: `docs/06-cloud-native.md`. you. - Cost of wrong call: hand-rolling a retry loop that retries non-idempotent POSTs, or reaching for hedging on a write path and amplifying load. -- Default per `ch02` / `ch06`: `AddStandardResilienceHandler()` on every - typed `HttpClient`; a custom Polly v8 pipeline only when the standard - defaults do not fit; hedging strictly for idempotent fan-out reads. +- Default per `ch02` §7 (single owner; ch06 references this section): + `AddStandardResilienceHandler()` on every typed `HttpClient`; a custom + Polly v8 pipeline only when the standard defaults do not fit; hedging + strictly for idempotent fan-out reads. The standard handler retries safe + verbs (GET, HEAD, OPTIONS, PUT, DELETE) by default and skips POST/PATCH — + retry POST only when the server exposes an idempotency-key contract. ```mermaid flowchart TD - A[Outbound HTTP dependency] --> B{Standard defaults fit?
retry + timeout +
circuit + bulkhead} - B -->|Yes| C[AddStandardResilienceHandler — ch02/ch06] - B -->|No, need custom budget /
per-route policy / chaos| D[Custom Polly v8 pipeline — ch02/ch06] + A[Outbound HTTP dependency] --> B{Standard defaults fit?
retry safe verbs + timeout +
circuit + bulkhead} + B -->|Yes| C[AddStandardResilienceHandler — ch02 §7] + B -->|No, need custom budget /
per-route policy / chaos| D[Custom Polly v8 pipeline — ch02 §7] B -->|Idempotent read with
tail-latency SLO| E{Multiple replicas / regions
safe to fan out?} - E -->|Yes| F[AddHedgingHandler — ch02/ch06] + E -->|Yes| F[AddStandardHedgingHandler — ch02 §7] E -->|No| C - D --> G[Document the deviation
in the service README — ch02/ch06] + D --> G[Document the deviation
in the service README — ch02 §7] ``` -References: `docs/02-aspnetcore.md`, `docs/06-cloud-native.md`. +References: [`docs/02-aspnetcore.md#7-resilience`](./02-aspnetcore.md#7-resilience). + +--- + +## 16. Aspire scope: AppHost resource vs in-service client integration + +- Trigger: adopting a new Aspire integration (Postgres, Redis, Service Bus, + Azure Storage, …) and choosing where the `PackageReference` lives. +- Cost of wrong call: shipping `Aspire.Hosting.*` packages to production + service images (they are inner-loop-only and pull in container/orchestration + code that has no business in a deployed service), or wiring an + `Aspire..` client into the AppHost (so the dashboard and + resource model lose visibility into the dependency). +- Default per `ch06` §1: `Aspire.Hosting.*` packages live **only** in the + AppHost project (`*.AppHost.csproj`) — they model resources for + `dotnet run` / `aspire run`. `Aspire..` client integrations + live in the **service** project that consumes the resource — they register + typed clients in DI, OTel instrumentation, and health checks. + +```mermaid +flowchart TD + A[New Aspire integration] --> B{What does the package do?} + B -->|Models a resource:
runs container locally,
emits connection info| C[AppHost project only
Aspire.Hosting.* — ch06 §1] + B -->|Registers a typed client
in DI: OTel + health check| D[Service project
Aspire.<Vendor>.<Tech> — ch06 §1] + C --> E[Service project also references
matching client integration — ch06 §1] + D --> F{Solution has an AppHost?} + F -->|Yes| G[Add matching Aspire.Hosting.*
resource in AppHost — ch06 §1] + F -->|No| H[Configure connection from
cluster config / Key Vault — ch06 §4] +``` + +References: [`docs/06-cloud-native.md#1-net-aspire--what-it-is-what-it-isnt`](./06-cloud-native.md#1-net-aspire--what-it-is-what-it-isnt). + +--- + +## 17. Cosmos DB partition key + +- Trigger: modeling a new container in Azure Cosmos DB (NoSQL API). +- Cost of wrong call: a low-cardinality or write-skewed key creates hot + logical partitions, throttles RU/s, and forces cross-partition queries + on the dominant read path — every fix is a data migration, not a config + change. +- Default per `ch03` §1 / §8: pick the property that is (a) **high + cardinality**, (b) **present on the dominant read path** so the query is + single-partition, and (c) **write-distributed** so no single tenant / + user / day pins one logical partition. If two read patterns conflict, + duplicate the document into a second container keyed for the other + pattern (CDC / change-feed) before you reach for cross-partition fan-out. + +```mermaid +flowchart TD + A[New Cosmos container] --> B{Dominant read pattern
known?} + B -->|No| C[Stop. Model the read pattern first — ch03 §1] + B -->|Yes| D{Candidate key on every
dominant read query?} + D -->|No| E[Pick a different key,
or duplicate via change feed — ch03 §8] + D -->|Yes| F{High cardinality
+ write-distributed?} + F -->|No, hot tenant / day / user| G[Composite key:
tenantId + hash(id) or
tenantId + bucket — ch03 §8] + F -->|Yes| H[Use the candidate key
as partition key — ch03 §8] + H --> I{Second read pattern
also dominant?} + I -->|Yes| J[Second container,
fed by change feed,
keyed for that pattern — ch03 §8] + I -->|No| K[Done — ch03 §8] +``` + +References: [`docs/03-data.md#1-choosing-the-tool`](./03-data.md#1-choosing-the-tool), [`docs/03-data.md#8-provider-notes`](./03-data.md#8-provider-notes). --- diff --git a/glossary.md b/glossary.md index 49ee74f..90cad39 100644 --- a/glossary.md +++ b/glossary.md @@ -144,6 +144,16 @@ ASP.NET Core's symmetric key set used by `IDataProtector` for cookies, antiforge - **Used in**: ch05 §9, ch06 §3 - **Sources**: Performance Improvements in .NET 8 — *DATAS* — https://devblogs.microsoft.com/dotnet/performance-improvements-in-net-8/ ; GC config — `GCDynamicAdaptationMode` — https://learn.microsoft.com/dotnet/core/runtime-config/garbage-collector +### DistributedApplicationTestingBuilder +`Aspire.Hosting.Testing.DistributedApplicationTestingBuilder` — test-host builder that boots a full Aspire AppHost (every modeled resource and service) inside an integration test, exposing typed `HttpClient`s and connection strings so `WebApplicationFactory`-style tests can cover multi-service composition. +- **Used in**: ch04 §6, ch06 §1 +- **Sources**: .NET Aspire — *Testing .NET Aspire apps* — https://learn.microsoft.com/dotnet/aspire/fundamentals/testing ; `DistributedApplicationTestingBuilder` API — https://learn.microsoft.com/dotnet/api/aspire.hosting.testing.distributedapplicationtestingbuilder + +### dotnet-affected +.NET CLI tool (`dotnet tool install -g dotnet-affected`) that walks the MSBuild graph from a base git ref and prints only the projects affected by the diff. Default monorepo CI fan-out filter in this guide — feed its output to `dotnet test`/`dotnet build` so PR builds touch only the impacted projects. +- **Used in**: patterns/monorepo, ch04 §16 +- **Sources**: `dotnet-affected` (leonardochaia) — https://github.com/leonardochaia/dotnet-affected ; NuGet — https://www.nuget.org/packages/dotnet-affected + ## E ### Enhanced form (Blazor) @@ -188,6 +198,11 @@ Schema-evolution discipline for zero-downtime EF Core migrations: deploy additiv - **Used in**: ch04 §14 - **Sources**: `Microsoft.Extensions.TimeProvider.Testing` — https://learn.microsoft.com/dotnet/core/extensions/timeprovider ; NuGet — https://www.nuget.org/packages/Microsoft.Extensions.TimeProvider.Testing +### Federated Identity Credentials +Microsoft Entra app-registration credential that trusts an external OIDC issuer (GitHub Actions, AKS *Workload Identity*, Azure DevOps) instead of a client secret. Default mechanism for keyless auth from CI and pods in this guide; configure per-subject under the app's *Federated credentials* blade or via Microsoft Graph. +- **Used in**: ch06 §8, checklist +- **Sources**: Microsoft Entra — *Workload identity federation* — https://learn.microsoft.com/entra/workload-id/workload-identity-federation ; *Configure an app to trust a GitHub repo* — https://learn.microsoft.com/entra/workload-id/workload-identity-federation-create-trust + ### FluentAssertions / AwesomeAssertions / Shouldly / TUnit Assertion-library landscape after Fluent Assertions changed its license in 2024. **AwesomeAssertions** is the OSS hard-fork (Apache-2.0); **Shouldly** is the long-standing alternative (BSD-3); **TUnit** ships its own assertion model. New projects in this guide pick AwesomeAssertions or Shouldly; existing FluentAssertions code stays put unless commercial-use thresholds bite. - **Used in**: ch04 §3 @@ -502,6 +517,11 @@ Microsoft library (`Microsoft.IO.RecyclableMemoryStream`) — pooled `MemoryStre - **Used in**: ch05 §2 - **Sources**: Microsoft.IO.RecyclableMemoryStream — https://github.com/microsoft/Microsoft.IO.RecyclableMemoryStream +### RequiredScope +`Microsoft.Identity.Web.Resource.RequiredScopeAttribute` — declarative `scp`-claim check on a controller, action, or minimal endpoint (`[RequiredScope("Orders.Read")]` or `RequireScope(...)`). Use it as the inline guard on delegated endpoints; pair with a separate app-only policy that checks `roles` + `azp` for S2S callers (the policies must not be merged). +- **Used in**: ch02 §10 +- **Sources**: Microsoft.Identity.Web — *Verifying scopes or app roles* — https://learn.microsoft.com/entra/msal/dotnet/microsoft-identity-web/web-apis ; `RequiredScopeAttribute` API — https://learn.microsoft.com/dotnet/api/microsoft.identity.web.resource.requiredscopeattribute + ### RetainVM `GCSettings.LatencyMode` interaction: when `` is set in runtimeconfig, the GC keeps virtual address segments after collection rather than returning them to the OS — trades RSS for fewer page-faults / mmap churn under spiky workloads. - **Used in**: ch05 §9 @@ -549,6 +569,11 @@ Shared library project (`*.ServiceDefaults`) referenced by every service in an A - **Used in**: ch06 §9 - **Sources**: ASP.NET Core Data Protection — *Configuration: SetApplicationName* — https://learn.microsoft.com/aspnet/core/security/data-protection/configuration/overview#setapplicationname +### slnx (.NET 9 solution format) +XML solution-file format (`*.slnx`) that supersedes the legacy `.sln`; GA in Visual Studio 17.14 / .NET SDK 9.0.200 and supported end-to-end by `dotnet sln`, MSBuild, Visual Studio, and Rider. Diff-friendly, human-readable; new repos start here, existing ones migrate with `dotnet sln migrate`. +- **Used in**: ch01 §1, patterns/monorepo +- **Sources**: Visual Studio docs — *Solution (.slnx) file* — https://learn.microsoft.com/visualstudio/ide/solutions-files ; .NET Blog — *Introducing slnx support in the .NET CLI* — https://devblogs.microsoft.com/dotnet/introducing-slnx-support-dotnet-cli + ### SocketsHttpHandler Cross-platform managed `HttpMessageHandler` (`System.Net.Http.SocketsHttpHandler`) — the substrate `HttpClient` uses by default since .NET Core 2.1. Owns the connection pool, HTTP/2/3 selection, *PooledConnectionLifetime*. Configure it through *IHttpClientFactory*, not directly. - **Used in**: ch02 §11, ch05 §10 @@ -591,6 +616,11 @@ Dedicated mutual-exclusion type (`System.Threading.Lock`) that `lock(x)` uses an - **Used in**: ch05 §8 - **Sources**: .NET — *Tiered compilation* — https://learn.microsoft.com/dotnet/standard/runtime/tiered-compilation +### Traversal SDK (Microsoft.Build.Traversal) +MSBuild project SDK (``) for "build a list of projects" without producing an output assembly. Default mechanism in this guide for repo-wide `dirs.proj` files that drive monorepo build/test fan-out from CI without depending on a `.sln`/`.slnx`. +- **Used in**: ch01 §1, patterns/monorepo +- **Sources**: `microsoft/MSBuildSdks` — *Microsoft.Build.Traversal* — https://github.com/microsoft/MSBuildSdks/tree/main/src/Traversal ; NuGet — https://www.nuget.org/packages/Microsoft.Build.Traversal + ## V ### ValueTask / ValueTask @@ -625,6 +655,11 @@ ASP.NET Core test host (`Microsoft.AspNetCore.Mvc.Testing`) that boots the real - **Used in**: ch04 §5 - **Sources**: ASP.NET Core — *Integration tests with WebApplicationFactory* — https://learn.microsoft.com/aspnet/core/test/integration-tests +### Workload Identity +AKS / Kubernetes feature (`azure.workload.identity/use: "true"` on a `ServiceAccount`) that mints a projected OIDC token a pod exchanges, via *Federated Identity Credentials*, for a Microsoft Entra access token — no client secrets, no long-lived credentials, no pod-identity controller. Default keyless workload-auth mechanism in this guide. +- **Used in**: ch06 §8, checklist +- **Sources**: Microsoft Entra — *Workload identity federation* — https://learn.microsoft.com/entra/workload-id/workload-identity-federation ; AKS — *Use Microsoft Entra Workload ID* — https://learn.microsoft.com/azure/aks/workload-identity-overview + ### Workstation GC .NET garbage-collector mode tuned for client/UI processes: single heap, lower memory ceiling, optimised for responsiveness over throughput. Default for desktop/MAUI; do **not** select for ASP.NET services — use *Server GC* + *DATAS*. - **Used in**: ch05 §9 diff --git a/patterns/anti-patterns.md b/patterns/anti-patterns.md index 627296d..c9c124b 100644 --- a/patterns/anti-patterns.md +++ b/patterns/anti-patterns.md @@ -17,10 +17,15 @@ Each entry: **smell** → **why it bites** → **fix**, with severity. **Smell.** A single project pulls in MVC, EF Core, gRPC, MassTransit, Polly, Serilog, AutoMapper, MediatR, FluentValidation, … . **Why.** The project is doing the job of three: hosting, application, and infrastructure. Build times -explode and the test surface is unclear. **Fix.** Split into `*.Host`, -`*.Application`, `*.Infrastructure` per the [Clean Architecture -template](https://github.com/ardalis/CleanArchitecture); host is the only -project that references infrastructure. +explode and the test surface is unclear. **Fix.** Split at the **bounded-context +seam** — that is the rule, not the specific layered shape. The endpoint shape +is a team choice; vertical slice +([Jimmy Bogard, *Vertical Slice Architecture*](https://www.jimmybogard.com/vertical-slice-architecture/)), +clean / onion ([Ardalis Clean Architecture +template](https://github.com/ardalis/CleanArchitecture)), and modular monolith +([Kamil Grzybek, *Modular Monolith primer*](https://www.kamilgrzybek.com/blog/posts/modular-monolith-primer)) +are all valid landing places, as long as the host project is the only one that +references infrastructure. See also `patterns/patterns.md` §"Architecture style". ## 🔴 Per-project `` drift **Smell.** Half the solution is `net8.0`, half `net10.0`. **Why.** You lose