FullStackHero 10 .NET Starter Kit Release Merge#1152
Conversation
There was a problem hiding this comment.
Pull request overview
This PR introduces the FullStackHero 10 .NET Starter Kit, implementing a modular monolith architecture with comprehensive modules for Identity, Multitenancy, and Auditing. The implementation includes a mediator-based CQRS pattern, JWT authentication with refresh tokens, role/permission-based authorization, background job support, caching abstractions, mailing services, and storage abstractions (local and S3). The Blazor client uses Shadcn-inspired MudBlazor wrappers with generated API clients via NSwag, while the infrastructure includes multi-app AWS scaffolding using Terraform and OpenTelemetry-based observability.
Key Changes
- Modular architecture with separate Identity, Multitenancy, and Auditing modules implementing contracts and handlers
- JWT authentication, role/permission system, and Finbuckle multitenancy with per-tenant provisioning lifecycle
- Auditing pipeline with request/response/security/exception tracking and background sink for SQL persistence
- OpenTelemetry integration, rate limiting, storage abstraction (local/S3), and comprehensive building blocks for caching, jobs, mailing, and persistence
Reviewed changes
Copilot reviewed 295 out of 1048 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
| Directory.Packages.props | Updated package versions to .NET 10.0 and newer dependencies including Finbuckle 10.0.0, Mediator 3.1.0-preview.14, Hangfire 1.8.22, and OpenTelemetry 1.14.0 |
| Directory.Build.props | Enhanced with .NET 10.0 target, comprehensive code analysis settings, NuGet metadata, and stricter quality controls |
| BuildingBlocks/Web/*.cs | New Web building block with OpenAPI/Scalar integration, OpenTelemetry, Serilog logging, rate limiting, security headers, CORS, versioning, and module loading |
| BuildingBlocks/Storage/*.cs | Storage abstraction supporting local filesystem and AWS S3 with file type validation and upload/removal operations |
| BuildingBlocks/Shared/*.cs | Shared contracts for multitenancy (AppTenantInfo), identity (claims, permissions, roles), pagination, and database options |
| BuildingBlocks/Persistence/*.cs | Persistence infrastructure with specifications pattern, EF Core extensions, and database initialization interfaces |
| Modules/Identity/Modules.Identity.Contracts/*.cs | Identity module contracts including commands/queries for token generation, user management, role management, and associated DTOs |
| Modules/Auditing/Modules.Auditing.Contracts/*.cs | Auditing contracts with event types, payloads, DTOs, and interfaces for audit publishing, serialization, and sinking |
| Modules/Auditing/Modules.Auditing/*.cs | Auditing implementation with SQL sink, EF interceptor, HTTP middleware for request/response capture, channel-based publisher, and query handlers |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Wow! You woke up!! :) Excelent works, I going to clone and test it... Please check, you forgot push the /docs folders because is added in .gitignore: "/docs" Thanks in advanced. |
|
I am currently using VS2026. |
Perfect approach, check that may be some ideas are usefull: And this "spec driven AI design": |
|
@maxiar looks like its a member only story. any crucial takeaways? |
|
You are seeing this message because GitHub Code Scanning has recently been set up for this repository, or this pull request contains the workflow file for the Code Scanning tool. What Enabling Code Scanning Means:
For more information about GitHub Code Scanning, check out the documentation. |
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
fullstackhero | bb370a7 | May 15 2026, 08:50 PM |
New module: Modules.Webhooks with full CQRS vertical slice architecture:
Domain:
- WebhookSubscription: tenant-scoped, supports event filtering and wildcard (*)
- WebhookDelivery: tracks each delivery attempt with status and error
API Endpoints:
- POST /api/v1/webhooks/subscriptions — create subscription
- DELETE /api/v1/webhooks/subscriptions/{id} — delete subscription
- GET /api/v1/webhooks/subscriptions — list subscriptions (paged)
- GET /api/v1/webhooks/subscriptions/{id}/deliveries — list deliveries
- POST /api/v1/webhooks/subscriptions/{id}/test — send test event
Features:
- HMAC-SHA256 payload signing (X-Webhook-Signature header)
- Resilient HTTP delivery via AddHeroResilience (Polly v8)
- EF Core persistence with webhooks schema
- Health check integration
- FluentValidation on all commands
Add SSE BuildingBlock with: - SseConnectionManager: manages per-user channels with tenant awareness - Connect/Disconnect lifecycle with bounded channels (100 events, drop oldest) - TrySend(userId): targeted push to specific user - Broadcast(tenantId): push to all users in a tenant - BroadcastAll(): push to all connected users - SseEvent record: EventType, Data, optional Id for reconnection - SSE streaming endpoint at GET /api/v1/sse/stream - text/event-stream content type with proper SSE formatting - Nginx proxy buffering disabled (X-Accel-Buffering: no) - Graceful disconnect on client cancellation - EnableSse toggle in FshPlatformOptions - MapSseEndpoints toggle in FshPipelineOptions Next.js/React clients consume via native EventSource API.
- Footer: fix module links to /overview/, guides/adding-a-feature - Content: fix /adding-a-feature/ → /guides/adding-a-feature/ in 3 pages - Content: fix /identity/roles/ → /identity/roles-permissions/ - Content: fix /identity/sessions/ → /identity/sessions-groups/ - Content: fix /building-blocks/blazor-ui/ → /building-blocks/overview/ Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add MDX documentation for all Phase 2 features: - building-blocks/http-resilience.mdx — Polly v8 resilience pipelines - building-blocks/feature-flags.mdx — tenant-aware feature management - building-blocks/idempotency.mdx — HTTP request deduplication - building-blocks/sse.mdx — Server-Sent Events real-time push - modules/webhooks/overview.mdx — webhook subscriptions and delivery Update CLAUDE.md tech stack table with new features.
Flatten all nested documentation paths for better SEO:
- /modules/identity/roles-permissions/ → /roles-and-permissions/
- /modules/multitenancy/overview/ → /multitenancy/
- /building-blocks/caching/ → /caching/
- /cross-cutting/exception-handling/ → /exception-handling/
- /patterns/specifications/ → /specification-pattern/
- /deployment/aws-terraform/ → /aws-terraform-deployment/
- /guides/adding-a-feature/ → /adding-a-feature/
All URLs now: fullstackhero.net/dotnet-starter-kit/{keyword-slug}/
Max depth: 2 levels. Every slug is a search-friendly keyword.
Updated: sidebar config, all internal links, imports, nav, footer,
breadcrumbs, pagination, llms.txt, structured data.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ersion updates Add WAF module with AWS managed rule groups (CommonRuleSet, SQLi, IP Reputation, Known Bad Inputs, Linux OS, rate limiting) and CloudWatch logging. Add CloudWatch alarms module for ECS, RDS, Redis, and ALB monitoring with SNS email notifications. Add ECS auto-scaling with target tracking on CPU, memory, and request count. Security hardening across all modules: - VPC endpoint SG egress restricted to VPC CIDR (was 0.0.0.0/0) - Flow logs IAM scoped to specific log group ARN (was Resource: *) - Default VPC SG lockdown (deny all - AWS best practice) - KMS encryption support for all CloudWatch log groups - ALB desync mitigation mode and connection logging - ECS container readonlyRootFilesystem and initProcessEnabled - RDS IAM authentication and CloudWatch log exports - Redis slow-log and engine-log delivery - S3 SSL/TLS enforcement on all bucket policies by default Version updates: PostgreSQL 17, Redis 7.2, AWS provider >= 5.90.0, Graviton (t4g) instances for 20% better price-performance. Clean up stale tfvars with plaintext passwords and obsolete variables. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
… findings HIGH — Log entries from user input (6 findings): - Cache services: replace user-derived key in warning logs with key length (DistributedCacheService, HybridCacheService) - Idempotency filter: hash the idempotency key (SHA256 short) before logging MEDIUM — Private data exposure (3 findings): - TokenService: log subject ID instead of masked email, remove MaskEmail() - SimpleBffAuth: log tenant/subject instead of email hash, remove ComputeEmailHash() No PII or user-controlled strings now flow to log sinks.
…oyment guide Add Webhooks module to introduction, architecture, module-system, and project-structure docs (module count, tables, file trees, code samples). Rewrite aws-terraform-deployment from 163 to 933 lines covering all 9 reusable modules, 3-environment comparison, step-by-step deployment, security architecture, cost optimization, and troubleshooting. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…llation hero - Replace all em dashes with hyphens across 47 doc files - Add scroll-reveal animations with staggered card entrances (IntersectionObserver) - Add particle constellation background to hero section - Add hero entrance animation and terminal typing effect - Add smooth FAQ accordion using CSS grid-template-rows - Add card hover lift micro-interactions (GPU composited) - Add button hover polish and CTA glow pulse - Respect prefers-reduced-motion throughout Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Bump all mobile font sizes to readable range (0.88-1.05rem body text) - Fix author image to fill container edge-to-edge - Reduce hero bottom padding and stats section padding to close gap Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Rounded pill with green version badge, hover glow, and arrow. Links to quick-start. Collapses to version-only on small screens. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Max-width 52rem, image column narrowed to 280px. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…e positioning Grid uses 340px fixed column instead of auto. Image uses position absolute with all properties !important to override Astro's generated inline styles. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Consecutive own-author messages were stacking too tight (pt-0.5 + pb-1
= 6px gap) and on short bubbles ("hi" / "?") read as overlap. Bumped
the per-row vertical rhythm to py-1.5 with pt-1 on merged rows
(non-merged pt-3), so consecutive bubbles now sit ~10px apart while
still grouping into the same author block.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Backend:
- Message aggregate carries IsPinned + PinnedByUserId + PinnedAtUtc with
domain-enforced rules: Pin() rejects deleted messages, no-ops if already
pinned by the same user; Unpin() is idempotent. New MessagePinned /
MessageUnpinned domain events.
- Three feature folders mirror the existing Edit/Delete pattern:
PinMessage (POST /messages/{id}/pin), UnpinMessage (DELETE), and
GetPinnedMessages (GET /channels/{id}/pinned — most-recent first).
- AddMessagePinning migration adds the three columns + a partial index
on (ChannelId, IsPinned) WHERE IsPinned = true to keep the pinned-list
query cheap.
- Handlers verify channel membership and broadcast ChatMessagePinned /
ChatMessageUnpinned (full MessageDto) to channel:{id}.
- MessageDto + ChatMappers extended; permission Messages.Send is reused
for the gate.
Frontend:
- Hover action rail gets a Pin / Unpin glyph between Reply and Edit.
- Pinned messages render with a small "Pinned" mono caption above the
bubble and a brand-tinted ring on the bubble itself.
- New ChatPinnedDropdown in the channel header — Pin icon button opens
a popover listing pinned messages; click jumps the feed via
MessageListHandle.jumpToMessage.
- Realtime handlers patch top-level + per-parent caches and invalidate
the pinned-panel cache on Pinned / Unpinned events.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Backend (BuildingBlocks/Web/Realtime):
- New IPresenceTracker singleton — concurrent dictionary keyed by userId
tracking open connection counts. Returns true on 0→1 and 1→0 so the hub
can broadcast exactly one transition.
- AppHub.OnConnectedAsync increments and broadcasts PresenceChanged
{ userId, online: true } to Clients.All on the first connection; the
new OnDisconnectedAsync decrements and broadcasts online: false on the
last close.
- New GET /api/v1/realtime/presence?userIds=a,b,c endpoint returns the
snapshot for any set of ids; used by clients to bootstrap on mount
before the SignalR PresenceChanged stream fills in.
- Registration wired through AddHeroRealtime / MapHeroRealtime so any
app using the BuildingBlocks gets presence for free.
Note: in-memory tracker is per-host. Multi-replica deployments need a
shared store (Redis pub/sub on connect/disconnect); that's deferred.
Frontend:
- New usePresence(userId) hook in src/realtime/use-presence.ts —
useQuery against the snapshot endpoint with refetchInterval 60s plus
a PresenceChanged hub subscription that flips the cached value
instantly. TanStack dedupes per-userId, so N messages from 5 unique
authors fire 5 queries, not N.
- ChannelRail's 1-on-1 DM avatars now show an online/offline dot driven
by usePresence.
- Message bubble avatars (other-side branch only — own avatars are
hidden in the Teams layout) also show the dot.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…+ arch updates PinMessageTests (10): - Pin / Unpin happy path with stamp verification - Re-pin idempotence by the same user (no stamp change) - Unpin no-op when not pinned - GetPinnedMessages returns most-recently-pinned-first - Empty list for unpinned channels - 404 for missing message + non-member (don't leak channel existence) - ChatMessagePinned / Unpinned broadcasts arrive on the channel group PresenceTests (8): - Snapshot endpoint reports offline for users without a hub connection - Online while a hub is connected - Drops back to offline on disconnect - Multiple userIds in one call - PresenceChanged fires on connect and on last-connection close - Endpoint requires authentication Backend additions to make the tests pass cleanly: - PinMessageCommandValidator + UnpinMessageCommandValidator so the arch test HandlerValidatorPairing accepts the new commands. - EndpointConventionTests: added Pin/Unpin to the action-verb whitelist. Full chat suite now: 80 integration + 36 unit + 48 architecture = 164 tests, all passing (2 pre-existing skips on idempotency). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… group
Two correctness fixes flagged in yesterday's ship-readiness review.
Live read receipts:
MarkChannelReadCommandHandler now ALSO broadcasts ChatChannelMemberRead
to the channel:{id} group (not just user:{caller}), so other members
observe the watermark update without waiting for a manual refresh.
Payload: { channelId, userId, lastReadMessageId }. MessageList in the
dashboard patches the cached channel.members on receipt — ReadReceipt
re-derives instantly.
Presence scope:
AppHub now joins a tenant:{tenantId} group on connect (reading the
tenant claim from the principal) and broadcasts PresenceChanged to
that group instead of Clients.All. A 1000-user tenant no longer
fans every connect / disconnect out to every other tenant. Falls
back to Clients.All only when the tenant claim is missing.
No frontend change for presence — usePresence already subscribes to
PresenceChanged regardless of where the broadcast came from.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…migrations A new one-shot console project under src/Host/FSH.Starter.DbMigrator that brings every database in the solution up to head and optionally seeds, then exits with code 0 on success or 1 on failure. Replaces the "migrate at API startup" pattern for production deployments; the API's auto-migrate stays enabled in Development only. Usage: dotnet run --project src/Host/FSH.Starter.DbMigrator -- apply dotnet run --project src/Host/FSH.Starter.DbMigrator -- apply --tenant root dotnet run --project src/Host/FSH.Starter.DbMigrator -- apply --catalog-only dotnet run --project src/Host/FSH.Starter.DbMigrator -- apply --seed dotnet run --project src/Host/FSH.Starter.DbMigrator -- seed dotnet run --project src/Host/FSH.Starter.DbMigrator -- list-pending How it works: 1. Builds the same DI container the API does (every module's ConfigureServices), with web-only concerns (CORS, OpenAPI, jobs, mailing, SSE, realtime, OpenTelemetry, quotas, idempotency) off. 2. RemoveAll<IHostedService> so background workers don't interfere. 3. Migrates TenantDbContext and seeds the root tenant if missing (same logic TenantStoreInitializerHostedService runs at API startup — extracted here so the migrator can be the single source). 4. Iterates every AppTenantInfo and calls ITenantService.MigrateTenantAsync (and SeedTenantAsync when --seed is set), reusing the production code path so behaviour stays identical to the API's auto-migrate. Configuration shares appsettings.json + appsettings.Development.json with FSH.Starter.Api via linked file references — operators override with environment variables in production. Ships a DefaultContainer publish profile (image name: fsh-db-migrator) so it can run as a Kubernetes Job / Helm pre-install hook / CI step. README walks through deployment patterns. MigratorCommand.cs is a minimal arg parser (no System.CommandLine dependency for half a dozen flags). Slnx updated to include the new project under /Host/. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…e stutter SendMessageCommandHandler broadcasts ChatMessageCreated via SignalR before the HTTP response flushes, so the echo usually beats the composer's onSuccess. The cache would briefly hold BOTH the optimistic temp and the real DTO — a visible duplicate bubble. ChatMessageCreated handler now locates the own pending temp by (authorUserId, body, parentMessageId) — the only fields we control on the client without a clientId round-trip — and removes it before inserting/appending the real payload. Same dedup applies to the top-level prepend path and the reply append path. The composer's onSuccess cleanup remains as the fallback when the HTTP response wins the race instead.
Three targeted improvements on top of the initial migrator. Aspire orchestration: - AppHost.csproj references the migrator project. - AppHost.cs adds the migrator as an Aspire resource that WaitFor(postgres) and runs once with verb "apply"; the API now WaitForCompletion(migrator) so it never starts against an unmigrated database. Dev parity with production deploys: the migrator is the single source of migration truth. - appsettings.Development.json flips MultitenancyOptions.RunTenantMigrations OnStartup to false so the API no longer double-migrates in dev. The migrator runs first; the API expects an up-to-date schema. Postgres advisory lock (concurrent-run safety): - New PostgresMigratorLock with a dedicated session-level pg_advisory_lock on a constant 64-bit key (0xFE514EC0_DEB1ADE4). Concurrent invocations block at acquire; whichever wins runs alone. Lock auto-releases on connection close, so a crashed migrator doesn't orphan it. - First-run safety: if the target database doesn't exist yet (SQLSTATE 3D000), the lock acquire falls back to a no-op holder — EF will create the database on MigrateAsync, and subsequent runs get the real lock. Wait-for-database with backoff: - WaitForDatabaseAsync polls Postgres with 1s → 10s exponential backoff up to a 2-minute deadline. Catches Aspire / K8s cold-starts where the Postgres container is still initialising when the migrator boots. - Treats SQLSTATE 3D000 as a success (server reachable, target DB missing — EF creates it on Migrate). Treats transient Npgsql / socket / timeout exceptions as retryable. What's still on the to-do list (not in this commit): --script mode for DBA SQL review, parallel per-tenant for thousand-tenant scale, JSON output for CI/CD parsing. None are blocking for current scale. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…dService surgery Three corrections needed for the migrator to actually run end-to-end: 1. NoOpJobService satisfies IJobService in the DI graph. Multitenancy's TenantProvisioningService constructor takes IJobService, which Hangfire registers only when EnableJobs is true. We can't enable Hangfire in the migrator (it needs its own DB schema + worker threads), so a throwing no-op satisfies the constructor chain. All operations throw with a clear error message in the unlikely event a future migration path tries to enqueue something. 2. RemoveAll<IHostedService> was too aggressive — it stripped Serilog's flush-on-shutdown service so logs never reached stdout. Now we surgically remove only TenantStoreInitializerHostedService (the one that would race with our explicit catalog migration). 3. Operator-facing progress messages switched from ILogger.LogInformation to Console.WriteLine so they're guaranteed to surface regardless of Serilog config quirks. Internal logging via ILogger still flows for diagnostic info; the visible "migrating…/done/finished" lines are reliable. Verified end-to-end against the Aspire-managed Postgres: [migrator] waiting for postgres… [migrator] postgres ready [migrator] acquiring advisory lock… [migrator] advisory lock acquired [tenant-catalog] already at head [root] migrating… [acme] migrating… [globex] migrating… [migrator] finished successfully. Exit code 0. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Earlier bump of merged-row pt to pt-1 (10px gap with py-1.5's pb) still read as overlap on long sequences of short bubbles (a string of "?" / "hi" / "ok" messages). The bubble rounding + brand-tinted background visually fused them into one shape. Bumped baseline to pb-1 only (4px below), pt-2 on merged (8px above — total 12px between merged-merged), pt-4 on non-merged (16px above — total 20px between author-changes). Long runs of short messages now clearly separate while staying grouped as one block.
Two related visual issues addressed together. Halo: The bubble had transition-shadow duration-500 layered on top of three possible box-shadow states (default 1px hairline, isFlashing 3px ring, isPinned 1px primary ring via Tailwind's ring-* utility). CSS shadow interpolation between those states during state transitions produced a ghost halo that bled well outside the rounded fill — most visible on strings of short merged bubbles where two halos overlapped. Fixed by dropping transition-shadow and the layered hairline shadow. The flash is now a sharp 2px ring that snaps on and off; pinned stays a 1px static ring. No shadow interpolation means no smearing. Spacing: Even with the halo gone, merged bubbles still read as too tight on the "?-?-?-?" pattern. Bumped baseline pb to pb-1.5 (6px) and merged-row pt to pt-3.5 (14px) — total ~20px between merged rows. Non-merged author-change rows use pt-6 (24px) for a clear ~30px break.
…adding Root cause of the "weird, non-uniform" spacing was the row's outer padding flipping between two values: pt-3.5 when isMerged, pt-6 when not. Because the author header lives INSIDE the inner column (above the bubble) only on non-merged rows, the bubble's position relative to the row's top kept shifting: merged row: [pt-3.5][bubble][pb-1.5] non-merged row: [pt-6][header+mb][bubble][pb-1.5] That meant the gap between successive BUBBLES varied depending on which side of the boundary you were on (merged→merged, merged→non-merged, non-merged→merged each produced a different bubble-to-bubble distance). Fix: every row now uses the same pt-2 / pb-1 — 12px between any two adjacent bubbles, period. The author header on non-merged rows still provides its own visual block-break by occupying space ABOVE the bubble inside the inner column, with a consistent mb-1 (4px) gap to the bubble below it. Two consecutive merged bubbles look exactly the same as a non-merged bubble followed by a merged one — what changes is what sits between them inside the row, not the row-to-row offset. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…esn't reject
Chat attachments are uploaded with Visibility=Private so the
ChatChannelFileAccessPolicy can gate reads to channel members. That
means FileAssetDto.publicUrl is null, and the composer was passing an
empty string as the message attachment Url — which the server's
MessageAttachment.Create rejects with ArgumentException, surfacing as
"SEND FAILED — RETRY?" on the composer.
After a successful upload, immediately call getFileDownloadUrl(id,
{inline: true}) when publicUrl is empty to mint a presigned read URL.
The resulting non-empty Url satisfies the server's validation AND lets
the recipient render the attachment without an extra round-trip.
Known limitation: the presigned URL expires (typically 1 hour). Old
chat attachments would 403 on re-render after the window passes. Proper
fix is a backend change — make MessageAttachment.Url nullable and
resolve via fileAssetId at read time — left as follow-up.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…istic temps
Two related bugs surfaced by attempting to send a file with no
accompanying text.
Send 400 "'Body' must not be empty":
- SendMessageCommandValidator unconditionally required Body. Now it
requires "Body OR at least one attachment" — Slack/Teams parity.
- SendMessageCommand.Body and SendMessageBody.Body switched to string?
so empty/missing bodies round-trip cleanly.
- Message.Create accepts null/empty body and stores null (column was
already nullable in EF config). Aggregate stays consistent because
the validator enforces the "must have content" invariant on input.
- SendMessageCommandHandler runs MentionParser against cmd.Body ??
string.Empty so an attachment-only send doesn't NRE on parse.
MarkChannelRead 500:
- The optimistic-send temp message has id "temp:xxx". Its momentary
presence at the head of the messages cache made chat-page's mark-read
effect POST that string to the server, which can't parse it as a
Guid and 500s. Skip when latestMessageId.startsWith("temp:") —
the real id arrives via realtime echo and the effect re-fires.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
24 new tests covering everything under Modules/Catalog/.../Features/v1/ Products/ that wasn't previously tested. Mirror of BrandsEndpointTests so the two halves of the catalog API have parity. Happy path: - CreateProduct returns 200 + persists all fields - SearchProducts includes a newly created product - SearchProducts filters by brandId - UpdateProduct persists changes + flips IsActive + bumps UpdatedAtUtc - ChangeProductPrice persists the new amount + currency - AdjustProductStock applies positive deltas - AdjustProductStock applies negative deltas in-range Soft-delete + restore: - DeleteProduct hides from search, returns 404 on GetById, appears in /products/trash with DeletedOnUtc + DeletedBy stamped - RestoreProduct brings the row back to GetById visibility, clears audit stamps - DeleteProduct → CreateWithSameSku succeeds (filtered unique index) Business rules: - AdjustProductStock rejects a negative delta that would push stock < 0 - CreateProduct returns 409 on duplicate live SKU - CreateProduct returns 400 on empty SKU - CreateProduct returns 400 on negative price - CreateProduct returns 400/404 on unknown brand - GetProductById returns 404 for unknown id - DeleteProduct returns 404 for unknown id - RestoreProduct returns 404 for unknown id Auth gating: - 401 on Create / Search / Delete / ListTrashedProducts without auth Idempotency: - CreateProduct without an Idempotency-Key header never gets the Idempotency-Replayed response header Uses FSH.Modules.Catalog.Contracts.Dtos.ProductDto directly (same as ProductImagesTests). PickBrandAndCategoryAsync picks the first seeded brand + category — keeps tests independent of seed data identity. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…s intact The new ProductsEndpointTests caught a real bug. DeleteProduct was returning 500 with: null value in column "PriceAmount" of relation "Products" violates not-null constraint Root cause: when EF marks an aggregate for deletion, every owned-type navigation cascades to EntityState.Deleted. The AuditableEntity interceptor flips the parent back to Modified to perform the soft delete, but the owned references stay marked for deletion — so the generated UPDATE includes "SET PriceAmount = NULL, PriceCurrency = NULL", which violates the NOT NULL columns. Brand soft-delete works because Brand has no owned types. The regression is specific to aggregates with owned value objects (Product → Money Price; future aggregates will hit the same). Fix: in the same interceptor, after restoring the parent's state, walk every owned reference whose TargetEntry is also Deleted and restore it to Unchanged. The owned values then survive the UPDATE unchanged, only IsDeleted / DeletedOnUtc / DeletedBy mutate. Also fixed in ProductsEndpointTests: - BrandDto fully qualified to Infrastructure.BrandDto (catalog DTO was ambiguous after referencing the contracts assembly directly). - UniqueSku helper upper-cases the prefix portion since Product.Create canonicalises the SKU to upper. Verified: 23/23 new product tests pass; full integration suite 282/285 (3 pre-existing skips, 0 failures). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CI builds with -warnaserror; the new DbMigrator project tripped CA1303, CA1308, CA1515, CA1812, CA1849, CA1873, CA2100, CA1031, S1186, S6618, S6667. Local builds didn't surface these because we don't pass the flag — fixing per-file rather than relaxing the analyzer set so the gap stays caught. - DbMigrator.csproj: NoWarn CA1303 (operator-facing console; not localised) - MigratorCommand: internal sealed record; OrdinalIgnoreCase verb match - PostgresMigratorLock: LoggerMessage source-gen, parameterised pg_advisory_lock/unlock SQL, CultureInfo.InvariantCulture for the TimeoutException message - Program: async Console writers, string.Create for interpolation, scoped CA1031 suppression around Main's broad catch - NoOpJobService: SuppressMessage CA1812 (DI-activated) - ProductImageValueGenerationNever migration: documented intentionally empty Up/Down Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Local builds previously stayed green even with analyzer warnings, so regressions surfaced only when CI ran `dotnet build … -warnaserror`. Flipping TreatWarningsAsErrors + CodeAnalysisTreatWarningsAsErrors to true in Directory.Build.props closes the gap — `dotnet build` (no flag) now fails on the same warnings CI does. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…only path Strips both startup-time migration hosted services and the two MultitenancyOptions flags that gated them. The API no longer applies schema changes on boot — FSH.Starter.DbMigrator (already production-grade, chained by Aspire AppHost) is the single deploy-time migration path. On-demand tenant provisioning via POST /tenants is preserved. Production-readiness teeth: TenantMigrationsHealthCheck now returns Unhealthy when any tenant has pending migrations, so /health/ready returns 503 and Kubernetes readiness probes pull the pod out of rotation until DbMigrator catches the schema up. DbMigrator hardening: fail-fast on empty connection string before host build (clean message, exit 1) and log current_user/current_database on connect so a misconfigured low-priv connection string is caught at boot instead of at the first DDL. CI: new migrator-smoke job publishes the migrator container locally and runs apply --catalog-only against an ephemeral Postgres — catches container-publish regressions before deploy. Regression: TenantMigrationsHealthCheckTests asserts the Unhealthy → Healthy transition against a real Postgres testcontainer plus the Unhealthy-on-probe-throw path. Implemented as a direct unit test (not through WebApplicationFactory) to avoid the Hangfire static state that would otherwise leak across the shared collection fixture. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`Message.Create` was relaxed in 9edaafe to accept null/empty/whitespace body so attachment-only messages can pass through the aggregate; the "must have content" invariant moved to SendMessageCommandValidator. The Create_Should_Reject_Empty_Body test still asserted the old throw behavior and has been failing since that commit (the 2026-05-14 audit ran only 6 of 8 suites and missed it). Rewritten as Create_Should_Normalize_Whitespace_Body_To_Null to lock in the current contract. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… 4.11 API
Patch bumps across the .NET 10 stack:
- ASP.NET Core / EF Core / Identity / Caching / Configuration / DI /
Hosting / Logging / Options / OpenAPI / SignalR.Client+Redis /
Mvc.Testing: 10.0.7 -> 10.0.8
- Http.Resilience / ServiceDiscovery / Caching.Hybrid: 10.5.0 -> 10.6.0
- Finbuckle.MultiTenant.*: 10.0.7 -> 10.0.8
- Scalar.AspNetCore: 2.14.7 -> 2.14.11
- AWSSDK.S3: 4.0.22.1 -> 4.0.23.1
- Spectre.Console: 0.55.0 -> 0.55.2
- Testcontainers.{PostgreSql,Redis,Minio}: 4.5.0 -> 4.11.0
Testcontainers 4.11 removes the `new XBuilder("image:tag")` constructor
overload; switched all three integration-test container builders to the
fluent `.WithImage("image:tag")` form.
Build green (0 warn / 0 err under TreatWarningsAsErrors). 598 unit tests
across 8 suites pass. Integration tests not exercised (Docker-gated).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Roslyn MCP + dotnet build + dotnet test audit across 45 projects / 42,314 LoC. 0 errors, 0 warnings, 0 circular dependencies, 539 unit tests green (the 6 suites the audit ran). All 768 raw anti-pattern findings and 21 dead-code candidates triaged: ~10 real design-choice items, the rest false positives or generated-code noise. Five optional recommendations, none blocking. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CONTRIBUTING: how to report bugs / vulns / feature requests, dev setup, PR workflow (target develop, Conventional Commits, no BuildingBlocks edits without discussion), pointer to CLAUDE.md for architecture rules. SECURITY: starter-kit support model (develop only — forks own their own fixes), private reporting via GitHub Security Advisories, 72h ack / 7d triage / ~90d disclosure window, scope, production hardening note. CODE_OF_CONDUCT.md intentionally deferred. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bumps transitive (vite, defu, postcss) and direct (astro, @astrojs/cloudflare) packages within existing caret ranges. Addresses high-severity vite path traversal, fs.deny bypass, WebSocket file read, and defu prototype pollution, plus moderate XSS in astro/postcss and low-severity SSRF in @astrojs/cloudflare. package.json unchanged; build verified (52 pages indexed). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…cs link - Catalog: 23 CategoriesEndpointTests covering CRUD, parent/child hierarchy with cycle detection, tree retrieval, soft-delete + restore, business rules, auth gating, and idempotency. - Notifications: 19 NotificationsEndpointTests covering the four inbox endpoints — auth gating, empty inbox, single-row lifecycle (incl. cross-user MarkRead safety and idempotent re-mark), bulk read, unreadOnly filter, newest-first ordering, paging. - Billing: 28 BillingEndpointTests covering Plans CRUD, Subscriptions assign/replace/lookup, the Invoice state machine (Draft → Issued → Paid/Void with rejection guards), and the Generate flow with per-period idempotency. - README: fix broken docs link to the new Astro Starlight site at docs/src/content/docs/ (resolves #1205). - docs/ISSUE_TRIAGE_2026-05-14.md: triage record for all 23 open issues — 18 closed with tailored comments, 5 kept open. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Switch from .WithImage("...") to constructor-with-image overload to
match commit bdfee3b — these three files were missed when the rest
of the test infra was migrated.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- DevDataSeeder: source SharedPassword from Seed:DemoPassword config instead of a C# literal; resolve lazily inside ExecuteAsync so missing config doesn't crash host startup. Adds Seed:DemoPassword=Password123! to appsettings.Development.json and the integration test factory's in-memory config (preserves existing seed behavior). - ConfigureJwtBearerOptions: add SanitizeForLog helper to strip control characters from Request.Method/Path before logging, defending against log-injection via attacker-controlled request data. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
SDK 10.0.300 errors CONTAINER2008 when both ContainerImageTag and ContainerImageTags are set. src/Directory.Build.props seeds the plural form globally (10.0.0-rc;latest), so the smoke step's singular -p override collides. Switch to the plural form to match the prod/dev publish steps and let -p override the default cleanly. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ainer target
The previous CONTAINER2008 fix (singular -> plural ContainerImageTag(s) on the
smoke step) wasn't enough — the real source of the collision is the obsolete
PublishProfile=DefaultContainer pattern. In SDK 10.0.300 it auto-derives
ContainerImageTag (singular) from \$(Version) = 10.0.0-rc.1, which then collides
with ContainerImageTags from Directory.Build.props. Hence the NETSDK1198 warning
("DefaultContainer profile not found") immediately followed by CONTAINER2008.
Switch to the modern /t:PublishContainer target everywhere (csprojs no longer
need PublishProfile, the API publish CI steps drop -p:PublishProfile=DefaultContainer).
Verified locally with -p:ContainerArchiveOutputPath — both DbMigrator (single
'smoke' tag) and API (two tags) publish cleanly, no warnings.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
#Architecture
scripts/openapi/generate-api-clients.ps1 -SpecUrl "<spec>"); Blazor consumes generated clients.