From 879802d25ca5b477df1feaf3c8dd81d8e29b2fa1 Mon Sep 17 00:00:00 2001 From: jarvis Date: Sun, 25 Jan 2026 13:19:56 +0000 Subject: [PATCH 1/4] feat: Make repo AI-ready with comprehensive guidelines - Rewrite CLAUDE.md with architectural philosophy and patterns - Add .cursorrules for Cursor IDE users - Add .github/copilot-instructions.md for GitHub Copilot AI assistants can now understand: - Modular monolith + vertical slice philosophy - Feature structure and patterns - Decision guides for where to put code - Critical rules and rationale --- .cursorrules | 35 ++++ .github/copilot-instructions.md | 49 +++++ CLAUDE.md | 352 +++++++++++++++----------------- 3 files changed, 243 insertions(+), 193 deletions(-) create mode 100644 .cursorrules create mode 100644 .github/copilot-instructions.md diff --git a/.cursorrules b/.cursorrules new file mode 100644 index 0000000000..93e07a90a9 --- /dev/null +++ b/.cursorrules @@ -0,0 +1,35 @@ +You are working on FullStackHero .NET Starter Kit - a modular monolith using vertical slice architecture. + +ARCHITECTURE: +- Features are self-contained in Modules/{Module}/Features/v1/{Feature}/ +- Each feature has: Command/Query, Handler, Validator, Endpoint +- Modules are in Modules/, framework in BuildingBlocks/, reference app in Playground/ +- DTOs go in Modules.{Name}.Contracts/ projects + +PATTERNS: +- Use Mediator library (NOT MediatR) - interfaces are ICommand, IQuery, ICommandHandler, IQueryHandler +- Handlers use primary constructors for DI +- Endpoints are static extension methods returning RouteHandlerBuilder +- Use .RequirePermission() for authorization +- Use [AsParameters] for query string binding +- All entities with tenant data implement IHasTenant (auto-filtered) + +RULES: +- Never return entities directly, always map to DTOs +- Every command needs an AbstractValidator +- Add .WithName() and .WithSummary() to all endpoints +- Build must pass with 0 warnings +- Don't modify BuildingBlocks without explicit approval + +COMMANDS: +- Build: dotnet build src/FSH.Framework.slnx +- Test: dotnet test src/FSH.Framework.slnx +- Run: dotnet run --project src/Playground/FSH.Playground.AppHost + +When adding a feature: +1. Create folder Modules/{Module}/Features/v1/{Name}/ +2. Add {Name}Command.cs or {Name}Query.cs +3. Add {Name}Handler.cs +4. Add {Name}Validator.cs (for commands) +5. Add {Name}Endpoint.cs +6. Wire in module's MapEndpoints() diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000000..f3c53d6fd6 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,49 @@ +# Copilot Instructions + +This is FullStackHero .NET Starter Kit - a modular monolith with vertical slice architecture. + +## Key Patterns + +**Feature Structure:** +``` +Modules/{Module}/Features/v1/{Feature}/ +├── {Feature}Command.cs # ICommand +├── {Feature}Handler.cs # ICommandHandler +├── {Feature}Validator.cs # AbstractValidator +└── {Feature}Endpoint.cs # Static RouteHandlerBuilder extension +``` + +**Use Mediator (not MediatR):** +```csharp +// ✓ Correct +public sealed record CreateUserCommand(string Email) : ICommand; +public sealed class CreateUserHandler : ICommandHandler + +// ✗ Wrong +public class CreateUserCommand : IRequest // This is MediatR +``` + +**Endpoints:** +```csharp +public static RouteHandlerBuilder MapCreateUserEndpoint(this IEndpointRouteBuilder e) => + e.MapPost("/", async (CreateUserCommand cmd, IMediator m, CancellationToken ct) => + TypedResults.Ok(await m.Send(cmd, ct))) + .WithName("CreateUser") + .WithSummary("Creates a new user") + .RequirePermission(IdentityPermissions.Users.Create); +``` + +## Rules + +1. DTOs in `Modules.{Module}.Contracts/` - never return entities +2. Every command needs a FluentValidation validator +3. Use `[AsParameters]` for query parameters +4. Use `.RequirePermission()` for auth +5. Build with 0 warnings: `dotnet build src/FSH.Framework.slnx` + +## Project Structure + +- `BuildingBlocks/` - Framework (don't modify without approval) +- `Modules/` - Feature modules (Identity, Multitenancy, Auditing) +- `Playground/` - Reference application +- `Tests/` - Architecture and unit tests diff --git a/CLAUDE.md b/CLAUDE.md index 65b54f181f..d8985ca29d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,239 +1,205 @@ # CLAUDE.md -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. +> AI assistant guidelines for FullStackHero .NET Starter Kit -## Build & Run Commands +## Philosophy -```bash -# Restore and build -dotnet restore src/FSH.Framework.slnx -dotnet build src/FSH.Framework.slnx - -# Run with Aspire (spins up Postgres + Redis via Docker) -dotnet run --project src/Playground/FSH.Playground.AppHost +This is a **modular monolith** using **vertical slice architecture**. Every decision optimizes for: -# Run API standalone (requires DB/Redis/JWT config in appsettings) -dotnet run --project src/Playground/Playground.Api +1. **Feature isolation** — Each feature is self-contained in one folder +2. **Module boundaries** — Modules are deployment-ready packages +3. **Explicit over implicit** — No magic, clear dependency flow +4. **Contract-first** — Public APIs via Contracts projects, internals stay internal -# Run all tests -dotnet test src/FSH.Framework.slnx +## Mental Model -# Run single test project -dotnet test src/Tests/Architecture.Tests +``` +┌─────────────────────────────────────────────────────────────┐ +│ Playground (Reference App) │ +│ └─ Wires modules together, runs the app │ +├─────────────────────────────────────────────────────────────┤ +│ Modules (Feature Packages) │ +│ ├─ Identity → Auth, users, roles, permissions │ +│ ├─ Multitenancy → Tenant isolation, per-tenant DBs │ +│ └─ Auditing → Audit trail, security logs │ +├─────────────────────────────────────────────────────────────┤ +│ BuildingBlocks (Framework) │ +│ └─ Core, Persistence, Web, Caching, Jobs, Events... │ +│ ⚠️ Changes here affect ALL modules │ +└─────────────────────────────────────────────────────────────┘ +``` -# Run specific test -dotnet test src/Tests/Architecture.Tests --filter "FullyQualifiedName~TestMethodName" +## The Vertical Slice -# Generate C# API client from OpenAPI spec (requires API running) -./scripts/openapi/generate-api-clients.ps1 -SpecUrl "https://localhost:7030/openapi/v1.json" +**One feature = one folder with everything it needs.** -# Check for OpenAPI drift (CI validation) -./scripts/openapi/check-openapi-drift.ps1 -SpecUrl "" ``` +Modules/Identity/Features/v1/CreateUser/ +├── CreateUserCommand.cs ← What (input) +├── CreateUserHandler.cs ← How (logic) +├── CreateUserValidator.cs ← Guard (validation) +└── CreateUserEndpoint.cs ← Where (HTTP binding) +``` + +**Why this works:** +- Change a feature? One folder to modify +- Delete a feature? Delete one folder +- Understand a feature? Read one folder +- Test a feature? Mock one handler + +## Decision Guide -## Architecture +### "Where do I put this?" -FullStackHero .NET 10 Starter Kit - multi-tenant SaaS framework using vertical slice architecture. +| You're building... | Put it in... | +|-------------------|--------------| +| New API endpoint | `Modules/{Module}/Features/v1/{Feature}/` | +| Shared DTO/contract | `Modules.{Module}.Contracts/` | +| Cross-cutting concern | `BuildingBlocks/` (needs approval) | +| New bounded context | New `Modules.{Name}/` project | +| Database migration | `Playground/Migrations.PostgreSQL/` | -### Repository Structure +### "Should I create a new module?" -- **src/BuildingBlocks/** - Reusable framework components (packaged as NuGets): Core (DDD primitives), Persistence (EF Core + specifications), Caching (Redis), Mailing, Jobs (Hangfire), Storage, Web (host wiring), Eventing -- **src/Modules/** - Feature modules (packaged as NuGets): Identity (JWT auth, users, roles), Multitenancy (Finbuckle), Auditing -- **src/Playground/** - Reference implementation using direct project references for development; includes Aspire AppHost, API, Blazor UI, PostgreSQL migrations -- **src/Tests/** - Architecture tests using NetArchTest.Rules, xUnit, Shouldly -- **scripts/openapi/** - NSwag-based C# client generation from OpenAPI spec; outputs to `Playground.Blazor/ApiClient/Generated.cs` -- **terraform/** - AWS infrastructure as code (modular) - - `modules/` - Reusable: network, ecs_cluster, ecs_service, rds_postgres, elasticache_redis, alb, s3_bucket - - `apps/playground/` - Playground deployment stack with `envs/{dev,staging,prod}/{region}/` - - `bootstrap/` - Initial AWS setup (S3 backend, etc.) +Ask: **Does this represent a separate bounded context?** +- Has its own domain entities? → New module +- Could be deployed independently? → New module +- Just a new feature in existing domain? → Add to existing module -### Module Pattern +## Code Patterns -Each module implements `IModule` with: -- `ConfigureServices(IHostApplicationBuilder)` - DI registration -- `MapEndpoints(IEndpointRouteBuilder)` - Minimal API endpoint mapping +### Command/Query (CQRS) -Feature structure within modules: +```csharp +// Command — changes state, returns result +public sealed record CreateUserCommand(string Email, string Name) : ICommand; + +// Query — reads state, no side effects +public sealed record GetUserQuery(Guid Id) : IQuery; ``` -Features/v1/{Feature}/ -├── {Feature}Command.cs (or Query) -├── {Feature}Handler.cs -├── {Feature}Validator.cs (FluentValidation) -└── {Feature}Endpoint.cs (static extension method on IEndpointRouteBuilder) + +### Handler + +```csharp +public sealed class CreateUserHandler( + IRepository repo, + ICurrentUser currentUser) : ICommandHandler +{ + public async ValueTask Handle(CreateUserCommand cmd, CancellationToken ct) + { + var user = User.Create(cmd.Email, cmd.Name, currentUser.TenantId); + await repo.AddAsync(user, ct); + return new UserCreatedResponse(user.Id); + } +} ``` -Contracts projects (`Modules.{Name}.Contracts/`) contain public DTOs shareable with clients. +### Endpoint -### Endpoint Pattern +```csharp +public static class CreateUserEndpoint +{ + public static RouteHandlerBuilder Map(this IEndpointRouteBuilder e) => + e.MapPost("/", async (CreateUserCommand cmd, IMediator m, CancellationToken ct) => + TypedResults.Created($"/users/{(await m.Send(cmd, ct)).Id}")) + .WithName(nameof(CreateUserCommand)) + .WithSummary("Create a new user") + .RequirePermission(IdentityPermissions.Users.Create); +} +``` + +### Validator -Endpoints are static extension methods returning `RouteHandlerBuilder`: ```csharp -public static RouteHandlerBuilder MapXxxEndpoint(this IEndpointRouteBuilder endpoint) +public sealed class CreateUserValidator : AbstractValidator { - return endpoint.MapPost("/path", async (..., IMediator mediator, CancellationToken ct) => + public CreateUserValidator() { - var result = await mediator.Send(command, ct); - return TypedResults.Ok(result); - }); + RuleFor(x => x.Email).NotEmpty().EmailAddress(); + RuleFor(x => x.Name).NotEmpty().MaximumLength(100); + } } ``` -### Platform Wiring +## Critical Rules -In `Program.cs`: -1. Register Mediator with command/query assemblies -2. Call `builder.AddHeroPlatform(...)` - enables auth, OpenAPI, caching, mailing, jobs, health, OTel -3. Call `builder.AddModules(moduleAssemblies)` to load modules -4. Call `app.UseHeroMultiTenantDatabases()` for tenant DB migrations -5. Call `app.UseHeroPlatform(p => p.MapModules = true)` to wire endpoints +| Rule | Rationale | +|------|-----------| +| Use `Mediator` not `MediatR` | Source-gen library, different interfaces (`ICommand`, `IQuery`) | +| Handlers use primary constructors | Clean DI, immutable dependencies | +| DTOs in Contracts project | Shareable with clients, no internal leakage | +| Entities implement `IHasTenant` | Auto-filtered by tenant, no manual filtering | +| Endpoints use `.RequirePermission()` | Centralized auth, auditable | +| Zero build warnings | CI enforces, no exceptions | -## Configuration +## Multi-Tenancy + +Every request has a tenant context. The framework handles: +- **Tenant resolution** — From header, subdomain, or path +- **Data isolation** — Queries auto-filter by `TenantId` +- **Per-tenant DBs** — Optional, configurable per tenant + +**Your code:** Just implement `IHasTenant` on entities. Done. + +## Commands + +```bash +# Build (must pass with 0 warnings) +dotnet build src/FSH.Framework.slnx -Key settings (appsettings or env vars): -- `DatabaseOptions:Provider` - postgres or mssql -- `DatabaseOptions:ConnectionString` - Primary database -- `CachingOptions:Redis` - Redis connection -- `JwtOptions:SigningKey` - Required in production +# Test (architecture tests enforce patterns) +dotnet test src/FSH.Framework.slnx -## Code Standards +# Run with infrastructure (Postgres + Redis via Aspire) +dotnet run --project src/Playground/FSH.Playground.AppHost -- .NET 10, C# latest, nullable enabled -- SonarAnalyzer.CSharp with code style enforced in build -- API versioning in URL path (`/api/v1/...`) -- Mediator library (not MediatR) for commands/queries -- FluentValidation for request validation -- **Zero warnings policy**: After making any code changes, always verify the build produces no warnings. Run `dotnet build src/FSH.Framework.slnx` and ensure "0 Warning(s)" in output. Fix any warnings before considering work complete. +# Run API only (needs manual DB/Redis setup) +dotnet run --project src/Playground/Playground.Api +``` -## Blazor UI Components +## Adding a Feature (Step by Step) -The framework provides reusable Blazor components in `BuildingBlocks/Blazor.UI/Components/` with consistent styling. +1. **Create folder:** `Modules/{Module}/Features/v1/{FeatureName}/` -### FshPageHeader Component +2. **Add Command/Query:** + ```csharp + public sealed record {Name}Command(...) : ICommand<{Result}>; + ``` -Use `FshPageHeader` for consistent page headers across Playground.Blazor: +3. **Add Handler:** + ```csharp + public sealed class {Name}Handler(...) : ICommandHandler<{Name}Command, {Result}> + ``` -```razor -@using FSH.Framework.Blazor.UI.Components.Page +4. **Add Validator** (commands only): + ```csharp + public sealed class {Name}Validator : AbstractValidator<{Name}Command> + ``` - - - - Action - - -``` +5. **Add Endpoint:** + ```csharp + public static RouteHandlerBuilder Map{Name}Endpoint(this IEndpointRouteBuilder e) => ... + ``` -**Parameters:** -- `Title` (required): Main page title -- `Description` (optional): Description text below title -- `DescriptionContent` (optional): RenderFragment for complex descriptions -- `ActionContent` (optional): RenderFragment for action buttons on the right -- `TitleTypo` (optional): Typography style (default: Typo.h5) -- `Class` (optional): Additional CSS classes -- `PageTitleSuffix` (optional): Suffix for browser tab title - -**Features:** -- Modern card design with MudPaper Elevation="2" -- Subtle gradient background with primary color accent -- Left border accent in primary color -- Dark mode support - -### FshUserProfile Component - -Modern user profile dropdown for app bars/navbars with avatar, user info, and menu: - -```razor -@using FSH.Framework.Blazor.UI.Components.User - - -``` +6. **Wire in Module:** Add `group.Map{Name}Endpoint();` in `MapEndpoints()` -**Parameters:** -- `UserName` (required): User's display name -- `UserEmail` (optional): User's email address -- `UserRole` (optional): User's role or title -- `AvatarUrl` (optional): URL to user's avatar (shows initials if not provided) -- `ShowUserName` (optional): Show username next to avatar (default: true, hidden on mobile) -- `ShowUserInfo` (optional): Show user info in menu header (default: true) -- `MenuItems` (optional): Custom RenderFragment for menu items (uses default Profile/Settings/Logout if not provided) -- `OnProfileClick` (optional): Callback for Profile menu item -- `OnSettingsClick` (optional): Callback for Settings menu item -- `OnLogoutClick` (optional): Callback for Logout menu item - -**Features:** -- Responsive design (hides username on mobile) -- Avatar with initials fallback -- Smooth hover animations and transitions -- Gradient menu header with user info -- Customizable menu items via RenderFragment -- Scoped CSS for isolated styling - -### FshStatCard Component - -Statistics card for displaying metrics with icon, value, label, and optional badge: - -```razor -@using FSH.Framework.Blazor.UI.Components.Cards - - -``` +7. **Test:** `dotnet build && dotnet test` -**Parameters:** -- `Icon` (required): MudBlazor icon to display -- `IconColor` (optional): Color theme for icon and accent (default: Primary) -- `Value` (required): Main metric value to display -- `Label` (required): Description of the metric -- `Badge` (optional): Small badge text below label -- `BadgeColor` (optional): Color for the badge (default: Primary) - -**Features:** -- Hover animation with lift effect (`translateY(-4px)`) and enhanced shadow -- Uses MudCard with Elevation="2" for consistent Material Design styling -- Scoped CSS with `::deep` for proper Blazor CSS isolation -- Consistent structure matching the original stats-card pattern used throughout the app - -### FSH Design Tokens - -The framework uses CSS custom properties for consistent styling across all components. These are defined in `fsh-theme.css`: - -```css -:root { - /* Border Radius */ - --fsh-radius: 10px; - --fsh-radius-sm: 8px; - --fsh-radius-lg: 16px; - --fsh-radius-xl: 20px; - --fsh-radius-full: 9999px; - - /* Shadows */ - --fsh-shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.08); - --fsh-shadow-md: 0 4px 12px rgba(0, 0, 0, 0.08); - --fsh-shadow-lg: 0 10px 30px rgba(0, 0, 0, 0.1); - - /* Card Styling */ - --fsh-card-bg: #ffffff; - --fsh-card-border: rgba(0, 0, 0, 0.08); - --fsh-card-shadow: var(--fsh-shadow-md); - - /* Text Colors */ - --fsh-text-primary: #1a1a2e; - --fsh-text-secondary: #64748b; - - /* Transitions */ - --fsh-transition: 0.3s cubic-bezier(0.4, 0, 0.2, 1); +## Configuration + +### Required (Production) +```json +{ + "DatabaseOptions": { "ConnectionString": "..." }, + "CachingOptions": { "Redis": "..." }, + "JwtOptions": { "SigningKey": "..." } } ``` -Dark mode overrides are automatically applied when `.mud-theme-dark` is present. Use these tokens in custom components to ensure consistent styling. +### The stack +- **.NET 10** with C# latest +- **PostgreSQL** (default) or SQL Server +- **Redis** for distributed caching +- **Hangfire** for background jobs +- **FluentValidation** for input validation +- **OpenTelemetry** for observability From b1263f91ed77692514a37b64ad3e84f0a2fd4002 Mon Sep 17 00:00:00 2001 From: jarvis Date: Sun, 25 Jan 2026 13:20:51 +0000 Subject: [PATCH 2/4] chore: Remove Cursor and Copilot files, keep Claude only --- .cursorrules | 35 ----------------------- .github/copilot-instructions.md | 49 --------------------------------- 2 files changed, 84 deletions(-) delete mode 100644 .cursorrules delete mode 100644 .github/copilot-instructions.md diff --git a/.cursorrules b/.cursorrules deleted file mode 100644 index 93e07a90a9..0000000000 --- a/.cursorrules +++ /dev/null @@ -1,35 +0,0 @@ -You are working on FullStackHero .NET Starter Kit - a modular monolith using vertical slice architecture. - -ARCHITECTURE: -- Features are self-contained in Modules/{Module}/Features/v1/{Feature}/ -- Each feature has: Command/Query, Handler, Validator, Endpoint -- Modules are in Modules/, framework in BuildingBlocks/, reference app in Playground/ -- DTOs go in Modules.{Name}.Contracts/ projects - -PATTERNS: -- Use Mediator library (NOT MediatR) - interfaces are ICommand, IQuery, ICommandHandler, IQueryHandler -- Handlers use primary constructors for DI -- Endpoints are static extension methods returning RouteHandlerBuilder -- Use .RequirePermission() for authorization -- Use [AsParameters] for query string binding -- All entities with tenant data implement IHasTenant (auto-filtered) - -RULES: -- Never return entities directly, always map to DTOs -- Every command needs an AbstractValidator -- Add .WithName() and .WithSummary() to all endpoints -- Build must pass with 0 warnings -- Don't modify BuildingBlocks without explicit approval - -COMMANDS: -- Build: dotnet build src/FSH.Framework.slnx -- Test: dotnet test src/FSH.Framework.slnx -- Run: dotnet run --project src/Playground/FSH.Playground.AppHost - -When adding a feature: -1. Create folder Modules/{Module}/Features/v1/{Name}/ -2. Add {Name}Command.cs or {Name}Query.cs -3. Add {Name}Handler.cs -4. Add {Name}Validator.cs (for commands) -5. Add {Name}Endpoint.cs -6. Wire in module's MapEndpoints() diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md deleted file mode 100644 index f3c53d6fd6..0000000000 --- a/.github/copilot-instructions.md +++ /dev/null @@ -1,49 +0,0 @@ -# Copilot Instructions - -This is FullStackHero .NET Starter Kit - a modular monolith with vertical slice architecture. - -## Key Patterns - -**Feature Structure:** -``` -Modules/{Module}/Features/v1/{Feature}/ -├── {Feature}Command.cs # ICommand -├── {Feature}Handler.cs # ICommandHandler -├── {Feature}Validator.cs # AbstractValidator -└── {Feature}Endpoint.cs # Static RouteHandlerBuilder extension -``` - -**Use Mediator (not MediatR):** -```csharp -// ✓ Correct -public sealed record CreateUserCommand(string Email) : ICommand; -public sealed class CreateUserHandler : ICommandHandler - -// ✗ Wrong -public class CreateUserCommand : IRequest // This is MediatR -``` - -**Endpoints:** -```csharp -public static RouteHandlerBuilder MapCreateUserEndpoint(this IEndpointRouteBuilder e) => - e.MapPost("/", async (CreateUserCommand cmd, IMediator m, CancellationToken ct) => - TypedResults.Ok(await m.Send(cmd, ct))) - .WithName("CreateUser") - .WithSummary("Creates a new user") - .RequirePermission(IdentityPermissions.Users.Create); -``` - -## Rules - -1. DTOs in `Modules.{Module}.Contracts/` - never return entities -2. Every command needs a FluentValidation validator -3. Use `[AsParameters]` for query parameters -4. Use `.RequirePermission()` for auth -5. Build with 0 warnings: `dotnet build src/FSH.Framework.slnx` - -## Project Structure - -- `BuildingBlocks/` - Framework (don't modify without approval) -- `Modules/` - Feature modules (Identity, Multitenancy, Auditing) -- `Playground/` - Reference application -- `Tests/` - Architecture and unit tests From 9b2191fee53ce174b070a5297b514da54c33bd85 Mon Sep 17 00:00:00 2001 From: jarvis Date: Sun, 25 Jan 2026 13:24:04 +0000 Subject: [PATCH 3/4] feat: Complete AI-ready setup with rules, skills, and agents MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Structure: - CLAUDE.md → Entry point with quick reference - .claude/rules.md → 12 hard constraints with rationale - .claude/skills.md → Step-by-step guides for common tasks - .claude/agents.md → AI behavior guidelines and decision framework This enables AI assistants to: - Understand architectural philosophy - Follow patterns consistently - Make correct decisions about code placement - Catch common mistakes before they happen --- .claude/agents.md | 107 +++++++++++++++++++ .claude/rules.md | 76 ++++++++++++++ .claude/skills.md | 257 ++++++++++++++++++++++++++++++++++++++++++++++ CLAUDE.md | 213 ++++++++++---------------------------- 4 files changed, 497 insertions(+), 156 deletions(-) create mode 100644 .claude/agents.md create mode 100644 .claude/rules.md create mode 100644 .claude/skills.md diff --git a/.claude/agents.md b/.claude/agents.md new file mode 100644 index 0000000000..112cc75bbc --- /dev/null +++ b/.claude/agents.md @@ -0,0 +1,107 @@ +# Agents + +How AI assistants should behave when working with this codebase. + +## Identity + +You are assisting with **FullStackHero .NET Starter Kit** — a production-ready, multi-tenant SaaS framework. + +Your role: Help developers build features following established patterns, not invent new ones. + +## Before You Start + +1. **Understand the request** — Is it a feature, fix, or question? +2. **Locate the context** — Which module? Which layer? +3. **Check existing patterns** — Find similar code to follow +4. **Verify constraints** — Review rules before implementing + +## Behavior + +### Do +- Follow existing patterns exactly — consistency over creativity +- Reference actual code in the repo as examples +- Ask clarifying questions before making assumptions +- Verify builds pass with 0 warnings after changes +- Keep changes minimal and focused + +### Don't +- Invent new architectural patterns +- Modify BuildingBlocks without explicit approval +- Skip validation on any command +- Return entities directly from endpoints +- Use MediatR patterns (this uses Mediator library) + +## Response Style + +### For "How do I..." questions +1. Point to existing similar code +2. Show the pattern with repo-specific examples +3. List the files they need to create/modify + +### For "Create a..." requests +1. Confirm the module and feature name +2. Generate all required files (Command, Handler, Validator, Endpoint) +3. Show where to wire it up +4. Include the build verification step + +### For "Fix..." requests +1. Understand the error/issue first +2. Check if it violates any rules +3. Propose minimal fix following existing patterns +4. Verify the fix doesn't break other things + +## Decision Framework + +### "Where does this go?" +``` +Is it a new API endpoint? + → Modules/{Module}/Features/v1/{Name}/ + +Is it a shared type for external use? + → Modules.{Module}.Contracts/ + +Is it a cross-cutting concern? + → BuildingBlocks/ (needs approval) + +Is it a new business domain? + → New Modules.{Name}/ project +``` + +### "Should I create a new module?" +``` +Does it have its own domain entities? → Yes = new module +Could it be deployed independently? → Yes = new module +Is it just a feature in existing domain? → No = existing module +``` + +### "Which pattern do I use?" +``` +Changing state? → Command + Handler + Validator + Endpoint +Reading data? → Query + Handler + Endpoint +Domain event? → Implement IDomainEvent, raise from entity +Background work? → Use Hangfire job +``` + +## Verification Checklist + +Before considering any task complete: + +- [ ] Code follows vertical slice structure +- [ ] DTOs are in Contracts project +- [ ] Command has a validator +- [ ] Endpoint has `.RequirePermission()` or `.AllowAnonymous()` +- [ ] Endpoint has `.WithName()` and `.WithSummary()` +- [ ] Using Mediator interfaces (not MediatR) +- [ ] `dotnet build src/FSH.Framework.slnx` shows 0 warnings +- [ ] No direct entity exposure in responses + +## Common Mistakes to Catch + +| If you see... | It's wrong because... | Fix | +|---------------|----------------------|-----| +| `IRequest` | That's MediatR | Use `ICommand` or `IQuery` | +| `IRequestHandler` | That's MediatR | Use `ICommandHandler` or `IQueryHandler` | +| Entity in response | Exposes internals | Create DTO in Contracts | +| No validator | Validation required | Add `AbstractValidator` | +| Manual tenant filter | Framework handles this | Implement `IHasTenant` | +| Code in BuildingBlocks | Affects all modules | Move to module or get approval | diff --git a/.claude/rules.md b/.claude/rules.md new file mode 100644 index 0000000000..fa4cb09115 --- /dev/null +++ b/.claude/rules.md @@ -0,0 +1,76 @@ +# Rules + +Hard constraints that must always be followed. + +## Architecture Rules + +1. **Vertical Slices Only** + - Every feature lives in `Modules/{Module}/Features/v1/{FeatureName}/` + - Never scatter feature code across multiple directories + - One folder = one complete feature + +2. **Module Boundaries** + - Modules only communicate through Contracts projects + - Never reference internal types across modules + - DTOs always in `Modules.{Name}.Contracts/` + +3. **BuildingBlocks Protection** + - Changes to `BuildingBlocks/` require explicit approval + - These affect ALL modules - changes ripple everywhere + - Prefer extension over modification + +## Code Rules + +4. **Mediator Not MediatR** + - Use `Mediator` source generator library + - Interfaces: `ICommand`, `IQuery`, `ICommandHandler`, `IQueryHandler` + - Never use `IRequest` or `IRequestHandler` (those are MediatR) + +5. **Validation Required** + - Every command must have an `AbstractValidator` + - No exceptions - validation is not optional + - Validators auto-register via FluentValidation + +6. **No Entity Exposure** + - Never return domain entities from endpoints + - Always map to DTOs from Contracts project + - Entities are internal implementation details + +7. **Explicit Authorization** + - Every endpoint needs `.RequirePermission()` or `.AllowAnonymous()` + - Use permission constants from `{Module}PermissionConstants` + - No implicit security + +8. **Zero Warnings** + - Build must pass with 0 warnings + - Run `dotnet build src/FSH.Framework.slnx` and verify + - CI enforces this - no exceptions + +## Naming Rules + +9. **Consistent Naming** + | Type | Pattern | + |------|---------| + | Commands | `{Action}{Entity}Command` | + | Queries | `Get{Entity}Query` or `Get{Entities}Query` | + | Handlers | `{CommandOrQuery}Handler` | + | Validators | `{Command}Validator` | + | Endpoints | `{CommandOrQuery}Endpoint` | + | DTOs | `{Entity}Dto`, `{Entity}Response`, `{Action}{Entity}Request` | + +10. **File = Type** + - One public type per file + - Filename matches type name exactly + - `CreateUserCommand.cs` contains `CreateUserCommand` + +## Multi-Tenancy Rules + +11. **Tenant Isolation** + - Entities with tenant data implement `IHasTenant` + - Framework auto-filters queries by tenant + - Never manually filter by TenantId in queries + +12. **No Hardcoded Tenants** + - Never hardcode tenant IDs + - Use `ICurrentUser.TenantId` for current tenant + - Tenant context comes from framework diff --git a/.claude/skills.md b/.claude/skills.md new file mode 100644 index 0000000000..20ec25126a --- /dev/null +++ b/.claude/skills.md @@ -0,0 +1,257 @@ +# Skills + +Step-by-step guides for common tasks. + +## Skill: Add a Feature + +**When:** Creating a new API endpoint with business logic. + +**Steps:** + +1. **Create the folder** + ``` + Modules/{Module}/Features/v1/{FeatureName}/ + ``` + +2. **Create Command or Query** + ```csharp + // For state changes + public sealed record CreateProductCommand( + string Name, + decimal Price) : ICommand; + + // For reads + public sealed record GetProductQuery(Guid Id) : IQuery; + ``` + +3. **Create Handler** + ```csharp + public sealed class CreateProductHandler( + IRepository repository, + ICurrentUser currentUser) : ICommandHandler + { + public async ValueTask Handle( + CreateProductCommand command, + CancellationToken ct) + { + var product = Product.Create(command.Name, command.Price, currentUser.TenantId); + await repository.AddAsync(product, ct); + return new CreateProductResponse(product.Id); + } + } + ``` + +4. **Create Validator** (commands only) + ```csharp + public sealed class CreateProductValidator : AbstractValidator + { + public CreateProductValidator() + { + RuleFor(x => x.Name).NotEmpty().MaximumLength(200); + RuleFor(x => x.Price).GreaterThan(0); + } + } + ``` + +5. **Create Endpoint** + ```csharp + public static class CreateProductEndpoint + { + public static RouteHandlerBuilder Map(this IEndpointRouteBuilder endpoints) => + endpoints.MapPost("/", async ( + CreateProductCommand command, + IMediator mediator, + CancellationToken ct) => TypedResults.Created( + $"/products/{(await mediator.Send(command, ct)).Id}")) + .WithName(nameof(CreateProductCommand)) + .WithSummary("Create a new product") + .RequirePermission(CatalogPermissions.Products.Create); + } + ``` + +6. **Wire in Module** + ```csharp + // In module's MapEndpoints method + var products = endpoints.MapGroup("/products").WithTags("Products"); + products.MapCreateProductEndpoint(); + ``` + +7. **Add DTO to Contracts** + ```csharp + // In Modules.{Module}.Contracts/ + public sealed record CreateProductResponse(Guid Id); + public sealed record ProductDto(Guid Id, string Name, decimal Price); + ``` + +8. **Verify** + ```bash + dotnet build src/FSH.Framework.slnx # Must show 0 warnings + dotnet test src/FSH.Framework.slnx + ``` + +--- + +## Skill: Add a Module + +**When:** Creating a new bounded context (new business domain). + +**Steps:** + +1. **Create projects** + ``` + src/Modules/{Name}/ + ├── Modules.{Name}/ + │ ├── Modules.{Name}.csproj + │ ├── {Name}Module.cs + │ ├── {Name}PermissionConstants.cs + │ └── Features/v1/ + └── Modules.{Name}.Contracts/ + └── Modules.{Name}.Contracts.csproj + ``` + +2. **Implement IModule** + ```csharp + public sealed class CatalogModule : IModule + { + public void ConfigureServices(IHostApplicationBuilder builder) + { + // Register services, DbContext, etc. + } + + public void MapEndpoints(IEndpointRouteBuilder endpoints) + { + var group = endpoints.MapGroup("/api/v1/catalog"); + // Map feature endpoints here + } + } + ``` + +3. **Add permission constants** + ```csharp + public static class CatalogPermissionConstants + { + public static class Products + { + public const string View = "Products.View"; + public const string Create = "Products.Create"; + public const string Update = "Products.Update"; + public const string Delete = "Products.Delete"; + } + } + ``` + +4. **Register in Program.cs** + ```csharp + var moduleAssemblies = new Assembly[] + { + typeof(IdentityModule).Assembly, + typeof(MultitenancyModule).Assembly, + typeof(AuditingModule).Assembly, + typeof(CatalogModule).Assembly, // Add new module + }; + ``` + +5. **Add Mediator assemblies** (if module has commands/queries) + ```csharp + builder.Services.AddMediator(o => + { + o.Assemblies = [ + // ... existing assemblies + typeof(CreateProductCommand).Assembly, + typeof(CreateProductHandler).Assembly, + ]; + }); + ``` + +--- + +## Skill: Add Entity with Multi-Tenancy + +**When:** Creating a domain entity that should be tenant-isolated. + +```csharp +public sealed class Product : AggregateRoot, IHasTenant, IAuditableEntity, ISoftDeletable +{ + public string Name { get; private set; } = null!; + public decimal Price { get; private set; } + + // IHasTenant + public string TenantId { get; private set; } = null!; + + // IAuditableEntity + public DateTimeOffset CreatedAt { get; set; } + public string? CreatedBy { get; set; } + public DateTimeOffset? LastModifiedAt { get; set; } + public string? LastModifiedBy { get; set; } + + // ISoftDeletable + public DateTimeOffset? DeletedAt { get; set; } + public string? DeletedBy { get; set; } + + private Product() { } // EF Core + + public static Product Create(string name, decimal price, string tenantId) + { + var product = new Product + { + Id = Guid.NewGuid(), + Name = name, + Price = price, + TenantId = tenantId + }; + product.AddDomainEvent(new ProductCreatedEvent(product.Id)); + return product; + } +} +``` + +--- + +## Skill: Query with Pagination + +```csharp +public sealed record GetProductsQuery( + string? Search, + int PageNumber = 1, + int PageSize = 10) : IQuery>; + +public sealed class GetProductsHandler( + IReadRepository repository) : IQueryHandler> +{ + public async ValueTask> Handle( + GetProductsQuery query, + CancellationToken ct) + { + var spec = new ProductSearchSpec(query.Search, query.PageNumber, query.PageSize); + return await repository.PaginatedListAsync(spec, ct); + } +} +``` + +--- + +## Skill: Run & Test + +```bash +# Build (must be 0 warnings) +dotnet build src/FSH.Framework.slnx + +# Run tests +dotnet test src/FSH.Framework.slnx + +# Run with Aspire (Postgres + Redis auto-provisioned) +dotnet run --project src/Playground/FSH.Playground.AppHost + +# Run API only (manual DB setup required) +dotnet run --project src/Playground/Playground.Api + +# Add migration +dotnet ef migrations add {Name} \ + --project src/Playground/Migrations.PostgreSQL \ + --startup-project src/Playground/Playground.Api + +# Apply migrations +dotnet ef database update \ + --project src/Playground/Migrations.PostgreSQL \ + --startup-project src/Playground/Playground.Api +``` diff --git a/CLAUDE.md b/CLAUDE.md index d8985ca29d..2102031df4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,115 +1,89 @@ # CLAUDE.md -> AI assistant guidelines for FullStackHero .NET Starter Kit +> FullStackHero .NET Starter Kit — AI Assistant Guidelines -## Philosophy +## Quick Reference -This is a **modular monolith** using **vertical slice architecture**. Every decision optimizes for: - -1. **Feature isolation** — Each feature is self-contained in one folder -2. **Module boundaries** — Modules are deployment-ready packages -3. **Explicit over implicit** — No magic, clear dependency flow -4. **Contract-first** — Public APIs via Contracts projects, internals stay internal +```bash +dotnet build src/FSH.Framework.slnx # Build (0 warnings required) +dotnet test src/FSH.Framework.slnx # Test +dotnet run --project src/Playground/FSH.Playground.AppHost # Run +``` -## Mental Model +## Project Structure ``` -┌─────────────────────────────────────────────────────────────┐ -│ Playground (Reference App) │ -│ └─ Wires modules together, runs the app │ -├─────────────────────────────────────────────────────────────┤ -│ Modules (Feature Packages) │ -│ ├─ Identity → Auth, users, roles, permissions │ -│ ├─ Multitenancy → Tenant isolation, per-tenant DBs │ -│ └─ Auditing → Audit trail, security logs │ -├─────────────────────────────────────────────────────────────┤ -│ BuildingBlocks (Framework) │ -│ └─ Core, Persistence, Web, Caching, Jobs, Events... │ -│ ⚠️ Changes here affect ALL modules │ -└─────────────────────────────────────────────────────────────┘ +src/ +├── BuildingBlocks/ # Framework core (⚠️ don't modify without approval) +├── Modules/ # Business modules — add features here +│ ├── Identity/ # Auth, users, roles, permissions +│ ├── Multitenancy/ # Tenant management +│ └── Auditing/ # Audit logging +├── Playground/ # Reference application +└── Tests/ # Architecture + unit tests ``` -## The Vertical Slice +## The Pattern -**One feature = one folder with everything it needs.** +Every feature = 4 files in one folder: ``` -Modules/Identity/Features/v1/CreateUser/ -├── CreateUserCommand.cs ← What (input) -├── CreateUserHandler.cs ← How (logic) -├── CreateUserValidator.cs ← Guard (validation) -└── CreateUserEndpoint.cs ← Where (HTTP binding) +Modules/{Module}/Features/v1/{Feature}/ +├── {Feature}Command.cs → ICommand +├── {Feature}Handler.cs → ICommandHandler +├── {Feature}Validator.cs → AbstractValidator +└── {Feature}Endpoint.cs → RouteHandlerBuilder extension ``` -**Why this works:** -- Change a feature? One folder to modify -- Delete a feature? Delete one folder -- Understand a feature? Read one folder -- Test a feature? Mock one handler - -## Decision Guide - -### "Where do I put this?" +## Essential Rules -| You're building... | Put it in... | -|-------------------|--------------| -| New API endpoint | `Modules/{Module}/Features/v1/{Feature}/` | -| Shared DTO/contract | `Modules.{Module}.Contracts/` | -| Cross-cutting concern | `BuildingBlocks/` (needs approval) | -| New bounded context | New `Modules.{Name}/` project | -| Database migration | `Playground/Migrations.PostgreSQL/` | +| Rule | Why | +|------|-----| +| Use `Mediator` not `MediatR` | Different library, different interfaces | +| DTOs in Contracts project | Keep internals internal | +| Every command needs validator | No unvalidated input | +| `.RequirePermission()` on endpoints | Explicit authorization | +| Zero build warnings | CI enforces this | -### "Should I create a new module?" +## Deep Dive -Ask: **Does this represent a separate bounded context?** -- Has its own domain entities? → New module -- Could be deployed independently? → New module -- Just a new feature in existing domain? → Add to existing module +| Topic | File | +|-------|------| +| All rules & constraints | [.claude/rules.md](.claude/rules.md) | +| Step-by-step guides | [.claude/skills.md](.claude/skills.md) | +| AI behavior guidelines | [.claude/agents.md](.claude/agents.md) | -## Code Patterns +--- -### Command/Query (CQRS) +## Quick Patterns +### Command + Handler ```csharp -// Command — changes state, returns result -public sealed record CreateUserCommand(string Email, string Name) : ICommand; +public sealed record CreateUserCommand(string Email, string Name) : ICommand; -// Query — reads state, no side effects -public sealed record GetUserQuery(Guid Id) : IQuery; -``` - -### Handler - -```csharp -public sealed class CreateUserHandler( - IRepository repo, - ICurrentUser currentUser) : ICommandHandler +public sealed class CreateUserHandler(IRepository repo) + : ICommandHandler { - public async ValueTask Handle(CreateUserCommand cmd, CancellationToken ct) + public async ValueTask Handle(CreateUserCommand cmd, CancellationToken ct) { - var user = User.Create(cmd.Email, cmd.Name, currentUser.TenantId); + var user = User.Create(cmd.Email, cmd.Name); await repo.AddAsync(user, ct); - return new UserCreatedResponse(user.Id); + return user.Id; } } ``` ### Endpoint - ```csharp -public static class CreateUserEndpoint -{ - public static RouteHandlerBuilder Map(this IEndpointRouteBuilder e) => - e.MapPost("/", async (CreateUserCommand cmd, IMediator m, CancellationToken ct) => - TypedResults.Created($"/users/{(await m.Send(cmd, ct)).Id}")) - .WithName(nameof(CreateUserCommand)) - .WithSummary("Create a new user") - .RequirePermission(IdentityPermissions.Users.Create); -} +public static RouteHandlerBuilder MapCreateUserEndpoint(this IEndpointRouteBuilder e) => + e.MapPost("/", async (CreateUserCommand cmd, IMediator m, CancellationToken ct) => + TypedResults.Created($"/users/{await m.Send(cmd, ct)}")) + .WithName("CreateUser") + .WithSummary("Create a new user") + .RequirePermission(IdentityPermissions.Users.Create); ``` ### Validator - ```csharp public sealed class CreateUserValidator : AbstractValidator { @@ -121,85 +95,12 @@ public sealed class CreateUserValidator : AbstractValidator } ``` -## Critical Rules - -| Rule | Rationale | -|------|-----------| -| Use `Mediator` not `MediatR` | Source-gen library, different interfaces (`ICommand`, `IQuery`) | -| Handlers use primary constructors | Clean DI, immutable dependencies | -| DTOs in Contracts project | Shareable with clients, no internal leakage | -| Entities implement `IHasTenant` | Auto-filtered by tenant, no manual filtering | -| Endpoints use `.RequirePermission()` | Centralized auth, auditable | -| Zero build warnings | CI enforces, no exceptions | - -## Multi-Tenancy - -Every request has a tenant context. The framework handles: -- **Tenant resolution** — From header, subdomain, or path -- **Data isolation** — Queries auto-filter by `TenantId` -- **Per-tenant DBs** — Optional, configurable per tenant - -**Your code:** Just implement `IHasTenant` on entities. Done. +--- -## Commands +## Configuration (Production) -```bash -# Build (must pass with 0 warnings) -dotnet build src/FSH.Framework.slnx - -# Test (architecture tests enforce patterns) -dotnet test src/FSH.Framework.slnx - -# Run with infrastructure (Postgres + Redis via Aspire) -dotnet run --project src/Playground/FSH.Playground.AppHost - -# Run API only (needs manual DB/Redis setup) -dotnet run --project src/Playground/Playground.Api ``` - -## Adding a Feature (Step by Step) - -1. **Create folder:** `Modules/{Module}/Features/v1/{FeatureName}/` - -2. **Add Command/Query:** - ```csharp - public sealed record {Name}Command(...) : ICommand<{Result}>; - ``` - -3. **Add Handler:** - ```csharp - public sealed class {Name}Handler(...) : ICommandHandler<{Name}Command, {Result}> - ``` - -4. **Add Validator** (commands only): - ```csharp - public sealed class {Name}Validator : AbstractValidator<{Name}Command> - ``` - -5. **Add Endpoint:** - ```csharp - public static RouteHandlerBuilder Map{Name}Endpoint(this IEndpointRouteBuilder e) => ... - ``` - -6. **Wire in Module:** Add `group.Map{Name}Endpoint();` in `MapEndpoints()` - -7. **Test:** `dotnet build && dotnet test` - -## Configuration - -### Required (Production) -```json -{ - "DatabaseOptions": { "ConnectionString": "..." }, - "CachingOptions": { "Redis": "..." }, - "JwtOptions": { "SigningKey": "..." } -} +DatabaseOptions:ConnectionString ← Required +CachingOptions:Redis ← Required +JwtOptions:SigningKey ← Required (256-bit) ``` - -### The stack -- **.NET 10** with C# latest -- **PostgreSQL** (default) or SQL Server -- **Redis** for distributed caching -- **Hangfire** for background jobs -- **FluentValidation** for input validation -- **OpenTelemetry** for observability From 324c92214a0d79068eea9bc87d7ee09fb7d55707 Mon Sep 17 00:00:00 2001 From: jarvis Date: Sun, 25 Jan 2026 13:51:17 +0000 Subject: [PATCH 4/4] feat: Add Claude Code agents, skills, and rules Following Claude Code official documentation structure: Skills (.claude/skills//SKILL.md): - add-feature: Create API endpoints with vertical slice pattern - add-module: Scaffold new bounded contexts - add-entity: Create domain entities with multi-tenancy - query-patterns: Pagination, filtering, specifications - testing-guide: Unit, integration, architecture tests - mediator-reference: Mediator vs MediatR (background knowledge) Subagents (.claude/agents/.md): - code-reviewer: Review PRs against FSH patterns (sonnet, read-only) - feature-scaffolder: Generate complete feature files - module-creator: Scaffold new modules - architecture-guard: Verify architecture (haiku, plan mode) - migration-helper: EF Core migrations Rules (.claude/rules/.md) - path-scoped: - buildingblocks-protection: Warns on BuildingBlocks changes - api-conventions: Endpoint requirements - testing-rules: Test conventions Removed old flat files (skills.md, agents.md, rules.md) Updated CLAUDE.md with new structure reference --- .claude/agents.md | 107 --------- .claude/agents/architecture-guard.md | 123 ++++++++++ .claude/agents/code-reviewer.md | 84 +++++++ .claude/agents/feature-scaffolder.md | 110 +++++++++ .claude/agents/migration-helper.md | 129 +++++++++++ .claude/agents/module-creator.md | 131 +++++++++++ .claude/rules.md | 76 ------ .claude/rules/api-conventions.md | 65 ++++++ .claude/rules/buildingblocks-protection.md | 36 +++ .claude/rules/testing-rules.md | 77 ++++++ .claude/skills.md | 257 --------------------- .claude/skills/add-entity/SKILL.md | 164 +++++++++++++ .claude/skills/add-feature/SKILL.md | 116 ++++++++++ .claude/skills/add-module/SKILL.md | 176 ++++++++++++++ .claude/skills/mediator-reference/SKILL.md | 132 +++++++++++ .claude/skills/query-patterns/SKILL.md | 187 +++++++++++++++ .claude/skills/testing-guide/SKILL.md | 223 ++++++++++++++++++ CLAUDE.md | 60 ++--- 18 files changed, 1787 insertions(+), 466 deletions(-) delete mode 100644 .claude/agents.md create mode 100644 .claude/agents/architecture-guard.md create mode 100644 .claude/agents/code-reviewer.md create mode 100644 .claude/agents/feature-scaffolder.md create mode 100644 .claude/agents/migration-helper.md create mode 100644 .claude/agents/module-creator.md delete mode 100644 .claude/rules.md create mode 100644 .claude/rules/api-conventions.md create mode 100644 .claude/rules/buildingblocks-protection.md create mode 100644 .claude/rules/testing-rules.md delete mode 100644 .claude/skills.md create mode 100644 .claude/skills/add-entity/SKILL.md create mode 100644 .claude/skills/add-feature/SKILL.md create mode 100644 .claude/skills/add-module/SKILL.md create mode 100644 .claude/skills/mediator-reference/SKILL.md create mode 100644 .claude/skills/query-patterns/SKILL.md create mode 100644 .claude/skills/testing-guide/SKILL.md diff --git a/.claude/agents.md b/.claude/agents.md deleted file mode 100644 index 112cc75bbc..0000000000 --- a/.claude/agents.md +++ /dev/null @@ -1,107 +0,0 @@ -# Agents - -How AI assistants should behave when working with this codebase. - -## Identity - -You are assisting with **FullStackHero .NET Starter Kit** — a production-ready, multi-tenant SaaS framework. - -Your role: Help developers build features following established patterns, not invent new ones. - -## Before You Start - -1. **Understand the request** — Is it a feature, fix, or question? -2. **Locate the context** — Which module? Which layer? -3. **Check existing patterns** — Find similar code to follow -4. **Verify constraints** — Review rules before implementing - -## Behavior - -### Do -- Follow existing patterns exactly — consistency over creativity -- Reference actual code in the repo as examples -- Ask clarifying questions before making assumptions -- Verify builds pass with 0 warnings after changes -- Keep changes minimal and focused - -### Don't -- Invent new architectural patterns -- Modify BuildingBlocks without explicit approval -- Skip validation on any command -- Return entities directly from endpoints -- Use MediatR patterns (this uses Mediator library) - -## Response Style - -### For "How do I..." questions -1. Point to existing similar code -2. Show the pattern with repo-specific examples -3. List the files they need to create/modify - -### For "Create a..." requests -1. Confirm the module and feature name -2. Generate all required files (Command, Handler, Validator, Endpoint) -3. Show where to wire it up -4. Include the build verification step - -### For "Fix..." requests -1. Understand the error/issue first -2. Check if it violates any rules -3. Propose minimal fix following existing patterns -4. Verify the fix doesn't break other things - -## Decision Framework - -### "Where does this go?" -``` -Is it a new API endpoint? - → Modules/{Module}/Features/v1/{Name}/ - -Is it a shared type for external use? - → Modules.{Module}.Contracts/ - -Is it a cross-cutting concern? - → BuildingBlocks/ (needs approval) - -Is it a new business domain? - → New Modules.{Name}/ project -``` - -### "Should I create a new module?" -``` -Does it have its own domain entities? → Yes = new module -Could it be deployed independently? → Yes = new module -Is it just a feature in existing domain? → No = existing module -``` - -### "Which pattern do I use?" -``` -Changing state? → Command + Handler + Validator + Endpoint -Reading data? → Query + Handler + Endpoint -Domain event? → Implement IDomainEvent, raise from entity -Background work? → Use Hangfire job -``` - -## Verification Checklist - -Before considering any task complete: - -- [ ] Code follows vertical slice structure -- [ ] DTOs are in Contracts project -- [ ] Command has a validator -- [ ] Endpoint has `.RequirePermission()` or `.AllowAnonymous()` -- [ ] Endpoint has `.WithName()` and `.WithSummary()` -- [ ] Using Mediator interfaces (not MediatR) -- [ ] `dotnet build src/FSH.Framework.slnx` shows 0 warnings -- [ ] No direct entity exposure in responses - -## Common Mistakes to Catch - -| If you see... | It's wrong because... | Fix | -|---------------|----------------------|-----| -| `IRequest` | That's MediatR | Use `ICommand` or `IQuery` | -| `IRequestHandler` | That's MediatR | Use `ICommandHandler` or `IQueryHandler` | -| Entity in response | Exposes internals | Create DTO in Contracts | -| No validator | Validation required | Add `AbstractValidator` | -| Manual tenant filter | Framework handles this | Implement `IHasTenant` | -| Code in BuildingBlocks | Affects all modules | Move to module or get approval | diff --git a/.claude/agents/architecture-guard.md b/.claude/agents/architecture-guard.md new file mode 100644 index 0000000000..1922d24196 --- /dev/null +++ b/.claude/agents/architecture-guard.md @@ -0,0 +1,123 @@ +--- +name: architecture-guard +description: Verify changes don't violate architecture rules. Run architecture tests, check module boundaries, verify BuildingBlocks aren't modified. Use before commits or PRs. +tools: Read, Grep, Glob, Bash +disallowedTools: Write, Edit +model: haiku +permissionMode: plan +--- + +You are an architecture guardian for FullStackHero .NET Starter Kit. Your job is to verify architectural integrity. + +## Verification Steps + +### 1. Check for BuildingBlocks Modifications + +```bash +git diff --name-only | grep -E "^src/BuildingBlocks/" +``` + +If any files listed: **STOP** - BuildingBlocks changes require explicit approval. + +### 2. Run Architecture Tests + +```bash +dotnet test src/Tests/Architecture.Tests --no-build +``` + +All tests must pass. + +### 3. Verify Build Has 0 Warnings + +```bash +dotnet build src/FSH.Framework.slnx 2>&1 | grep -E "warning|error" +``` + +Must show no warnings or errors. + +### 4. Check Module Boundaries + +Verify no cross-module internal dependencies: + +```bash +# Check if any module references another module's internal types +grep -r "using Modules\." src/Modules/ --include="*.cs" | grep -v "\.Contracts" +``` + +Should only show references to `.Contracts` namespaces. + +### 5. Verify Mediator Usage + +```bash +# Check for MediatR usage (should be empty) +grep -r "MediatR\|IRequest<\|IRequestHandler<" src/Modules/ --include="*.cs" +``` + +Must be empty - all should use Mediator interfaces. + +### 6. Check Validator Coverage + +For each command, verify a validator exists: + +```bash +# List commands +find src/Modules -name "*Command.cs" -type f + +# List validators +find src/Modules -name "*Validator.cs" -type f +``` + +Every command needs a corresponding validator. + +### 7. Check Endpoint Authorization + +```bash +# Find endpoints without authorization +grep -r "\.Map\(Get\|Post\|Put\|Delete\)" src/Modules/ --include="*.cs" -A 5 | \ +grep -v "RequirePermission\|AllowAnonymous" +``` + +Every endpoint must have explicit authorization. + +## Output Format + +``` +## Architecture Verification Report + +### BuildingBlocks +✅ No modifications | ⚠️ MODIFIED - Requires approval + +### Architecture Tests +✅ All passed | ❌ {count} failed + +### Build Warnings +✅ 0 warnings | ❌ {count} warnings + +### Module Boundaries +✅ Clean | ❌ Cross-module dependencies found + +### Mediator Usage +✅ Correct | ❌ MediatR interfaces detected + +### Validators +✅ All commands have validators | ❌ Missing: {list} + +### Authorization +✅ All endpoints authorized | ❌ Missing: {list} + +--- +**Overall:** ✅ PASS | ❌ FAIL - Fix issues before commit +``` + +## Quick Commands + +```bash +# Full verification +dotnet build src/FSH.Framework.slnx && dotnet test src/FSH.Framework.slnx + +# Architecture tests only +dotnet test src/Tests/Architecture.Tests + +# Check for common issues +git diff --name-only | xargs grep -l "IRequest<\|MediatR" +``` diff --git a/.claude/agents/code-reviewer.md b/.claude/agents/code-reviewer.md new file mode 100644 index 0000000000..66b7ae9f60 --- /dev/null +++ b/.claude/agents/code-reviewer.md @@ -0,0 +1,84 @@ +--- +name: code-reviewer +description: Review code changes against FSH patterns and conventions. Use proactively after any code modifications to catch violations before commit. +tools: Read, Grep, Glob, Bash +disallowedTools: Write, Edit +model: sonnet +--- + +You are a code reviewer for the FullStackHero .NET Starter Kit. Your job is to review code changes and ensure they follow FSH patterns. + +## Review Process + +1. Run `git diff` to see recent changes +2. Identify which files were modified +3. Check each change against the rules below +4. Report violations with specific file:line references + +## Critical Rules to Check + +### Architecture +- [ ] Features are in `Modules/{Module}/Features/v1/{Name}/` structure +- [ ] DTOs are in Contracts project, not internal +- [ ] No cross-module dependencies (modules only use Contracts) +- [ ] BuildingBlocks not modified without explicit approval + +### Mediator (NOT MediatR!) +- [ ] Commands use `ICommand` not `IRequest` +- [ ] Queries use `IQuery` not `IRequest` +- [ ] Handlers use `ICommandHandler` or `IQueryHandler` +- [ ] Handler methods return `ValueTask` not `Task` +- [ ] Using `Mediator` namespace, not `MediatR` + +### Validation +- [ ] Every command has a matching `AbstractValidator` +- [ ] Validators use FluentValidation rules + +### Endpoints +- [ ] Has `.RequirePermission()` or `.AllowAnonymous()` +- [ ] Has `.WithName()` matching the command/query name +- [ ] Has `.WithSummary()` with description +- [ ] Returns TypedResults, not raw objects + +### Entities +- [ ] Implements required interfaces (IHasTenant, IAuditableEntity, ISoftDeletable) +- [ ] Has private constructor for EF Core +- [ ] Uses factory method for creation +- [ ] Properties have `private set` +- [ ] Domain events raised for state changes + +### Naming +- [ ] Commands: `{Action}{Entity}Command` +- [ ] Queries: `Get{Entity}Query` or `Get{Entities}Query` +- [ ] Handlers: `{CommandOrQuery}Handler` +- [ ] Validators: `{Command}Validator` +- [ ] DTOs: `{Entity}Dto`, `{Entity}Response` + +## Output Format + +``` +## Code Review Summary + +### ✅ Passed +- [List what's correct] + +### ❌ Violations Found +1. **{Rule}** - {file}:{line} + - Issue: {description} + - Fix: {how to fix} + +### ⚠️ Warnings +- [Optional suggestions] + +### Build Verification +Run: `dotnet build src/FSH.Framework.slnx` +Expected: 0 warnings +``` + +## After Review + +Suggest running: +```bash +dotnet build src/FSH.Framework.slnx # Verify 0 warnings +dotnet test src/FSH.Framework.slnx # Run tests +``` diff --git a/.claude/agents/feature-scaffolder.md b/.claude/agents/feature-scaffolder.md new file mode 100644 index 0000000000..558bcd3041 --- /dev/null +++ b/.claude/agents/feature-scaffolder.md @@ -0,0 +1,110 @@ +--- +name: feature-scaffolder +description: Generate complete feature folders with Command, Handler, Validator, and Endpoint files. Use when creating new API endpoints or features. +tools: Read, Write, Glob, Grep, Bash +model: inherit +--- + +You are a feature scaffolder for FullStackHero .NET Starter Kit. Your job is to generate complete vertical slice features. + +## Required Information + +Before generating, confirm: +1. **Module name** - Which module? (e.g., Identity, Catalog) +2. **Feature name** - What action? (e.g., CreateProduct, GetUser) +3. **Entity name** - What entity? (e.g., Product, User) +4. **Operation type** - Command (state change) or Query (read)? +5. **Properties** - What fields does the command/query need? + +## Generation Process + +### Step 1: Create Feature Folder + +``` +src/Modules/{Module}/Features/v1/{FeatureName}/ +``` + +### Step 2: Generate Files + +For **Commands** (POST/PUT/DELETE), create 4 files: +1. `{Action}{Entity}Command.cs` +2. `{Action}{Entity}Handler.cs` +3. `{Action}{Entity}Validator.cs` +4. `{Action}{Entity}Endpoint.cs` + +For **Queries** (GET), create 3 files: +1. `Get{Entity}Query.cs` or `Get{Entities}Query.cs` +2. `Get{Entity}Handler.cs` +3. `Get{Entity}Endpoint.cs` + +### Step 3: Add DTOs to Contracts + +Create response/DTO types in: +``` +src/Modules/{Module}/Modules.{Module}.Contracts/ +``` + +### Step 4: Wire Endpoint + +Show where to add endpoint mapping in the module's `MapEndpoints` method. + +## Template: Command + +```csharp +// {Action}{Entity}Command.cs +public sealed record {Action}{Entity}Command( + {Properties}) : ICommand<{Action}{Entity}Response>; + +// {Action}{Entity}Handler.cs +public sealed class {Action}{Entity}Handler( + IRepository<{Entity}> repository, + ICurrentUser currentUser) : ICommandHandler<{Action}{Entity}Command, {Action}{Entity}Response> +{ + public async ValueTask<{Action}{Entity}Response> Handle( + {Action}{Entity}Command command, + CancellationToken ct) + { + // Implementation + } +} + +// {Action}{Entity}Validator.cs +public sealed class {Action}{Entity}Validator : AbstractValidator<{Action}{Entity}Command> +{ + public {Action}{Entity}Validator() + { + // Validation rules + } +} + +// {Action}{Entity}Endpoint.cs +public static class {Action}{Entity}Endpoint +{ + public static RouteHandlerBuilder Map(this IEndpointRouteBuilder endpoints) => + endpoints.Map{HttpMethod}("/", async ( + {Action}{Entity}Command command, + IMediator mediator, + CancellationToken ct) => TypedResults.{Result}(await mediator.Send(command, ct))) + .WithName(nameof({Action}{Entity}Command)) + .WithSummary("{Summary}") + .RequirePermission({Module}Permissions.{Entities}.{Action}); +} +``` + +## Checklist Before Completion + +- [ ] All files use `Mediator` interfaces (NOT MediatR) +- [ ] Handler returns `ValueTask` +- [ ] Validator exists for commands +- [ ] Endpoint has `.RequirePermission()` and `.WithName()` and `.WithSummary()` +- [ ] DTOs in Contracts project +- [ ] Shown where to wire endpoint in module + +## Verification + +After generation, run: +```bash +dotnet build src/FSH.Framework.slnx +``` + +Must show 0 warnings. diff --git a/.claude/agents/migration-helper.md b/.claude/agents/migration-helper.md new file mode 100644 index 0000000000..2a35e5a8da --- /dev/null +++ b/.claude/agents/migration-helper.md @@ -0,0 +1,129 @@ +--- +name: migration-helper +description: Handle EF Core migrations safely. Create, apply, and manage database migrations for FSH multi-tenant setup. Use when adding entities or changing database schema. +tools: Read, Write, Grep, Glob, Bash +model: inherit +--- + +You are a migration helper for FullStackHero .NET Starter Kit. Your job is to safely manage EF Core migrations. + +## Project Paths + +- **Migrations project:** `src/Playground/Migrations.PostgreSQL` +- **Startup project:** `src/Playground/Playground.Api` +- **DbContexts:** Each module has its own DbContext + +## Common Operations + +### Add Migration + +```bash +dotnet ef migrations add {MigrationName} \ + --project src/Playground/Migrations.PostgreSQL \ + --startup-project src/Playground/Playground.Api \ + --context {DbContextName} +``` + +**Context names:** +- `IdentityDbContext` - Identity module +- `MultitenancyDbContext` - Multitenancy module +- `AuditingDbContext` - Auditing module +- `{Module}DbContext` - Custom modules + +### Apply Migrations + +```bash +dotnet ef database update \ + --project src/Playground/Migrations.PostgreSQL \ + --startup-project src/Playground/Playground.Api \ + --context {DbContextName} +``` + +### List Migrations + +```bash +dotnet ef migrations list \ + --project src/Playground/Migrations.PostgreSQL \ + --startup-project src/Playground/Playground.Api \ + --context {DbContextName} +``` + +### Remove Last Migration + +```bash +dotnet ef migrations remove \ + --project src/Playground/Migrations.PostgreSQL \ + --startup-project src/Playground/Playground.Api \ + --context {DbContextName} +``` + +### Generate SQL Script + +```bash +dotnet ef migrations script \ + --project src/Playground/Migrations.PostgreSQL \ + --startup-project src/Playground/Playground.Api \ + --context {DbContextName} \ + --output migrations.sql +``` + +## Multi-Tenant Considerations + +FSH uses per-tenant databases. Migrations apply to: +1. **Host database** - Tenant registry, shared data +2. **Tenant databases** - Tenant-specific data + +The framework handles tenant database migrations automatically on startup via `UseHeroMultiTenantDatabases()`. + +## Migration Naming Conventions + +Use descriptive names: +- `Add{Entity}` - Adding new entity +- `Add{Property}To{Entity}` - Adding column +- `Remove{Property}From{Entity}` - Removing column +- `Create{Index}Index` - Adding index +- `Rename{Old}To{New}` - Renaming + +## Pre-Migration Checklist + +- [ ] Entity configuration exists (`IEntityTypeConfiguration`) +- [ ] Entity added to DbContext (`DbSet`) +- [ ] Build succeeds with 0 warnings +- [ ] Backup database if production + +## Post-Migration Checklist + +- [ ] Review generated migration file +- [ ] Check Up() and Down() methods are correct +- [ ] Test migration on development database +- [ ] Verify rollback works (Down method) + +## Troubleshooting + +### "No DbContext was found" +Specify context explicitly with `--context {Name}DbContext` + +### "Build failed" +Run `dotnet build src/FSH.Framework.slnx` first + +### "Pending migrations" +Apply pending migrations or remove them if not needed + +### "Migration already applied" +Check `__EFMigrationsHistory` table in database + +## Example: Adding a New Entity + +1. Create entity in `Domain/` folder +2. Create configuration (`IEntityTypeConfiguration`) +3. Add `DbSet` to DbContext +4. Build: `dotnet build src/FSH.Framework.slnx` +5. Add migration: + ```bash + dotnet ef migrations add Add{Entity} \ + --project src/Playground/Migrations.PostgreSQL \ + --startup-project src/Playground/Playground.Api \ + --context {Module}DbContext + ``` +6. Review migration file +7. Apply: `dotnet ef database update ...` diff --git a/.claude/agents/module-creator.md b/.claude/agents/module-creator.md new file mode 100644 index 0000000000..c9dab6c11a --- /dev/null +++ b/.claude/agents/module-creator.md @@ -0,0 +1,131 @@ +--- +name: module-creator +description: Create new modules (bounded contexts) with complete project structure, DbContext, permissions, and registration. Use when adding a new business domain. +tools: Read, Write, Glob, Grep, Bash +model: inherit +--- + +You are a module creator for FullStackHero .NET Starter Kit. Your job is to scaffold complete new modules. + +## When to Create a New Module + +Ask these questions: +- Does it have its own domain entities? → Yes = new module +- Could it be deployed independently? → Yes = new module +- Is it just a feature in an existing domain? → No = use existing module + +## Required Information + +Before generating, confirm: +1. **Module name** - PascalCase (e.g., Catalog, Inventory, Billing) +2. **Initial entities** - What domain entities? +3. **Permissions** - What operations need permissions? + +## Generation Process + +### Step 1: Create Project Structure + +``` +src/Modules/{Name}/ +├── Modules.{Name}/ +│ ├── Modules.{Name}.csproj +│ ├── {Name}Module.cs +│ ├── {Name}PermissionConstants.cs +│ ├── {Name}DbContext.cs +│ ├── Domain/ +│ └── Features/v1/ +└── Modules.{Name}.Contracts/ + ├── Modules.{Name}.Contracts.csproj + └── DTOs/ +``` + +### Step 2: Generate Core Files + +**Modules.{Name}.csproj:** +```xml + + + net10.0 + + + + + + + + +``` + +**{Name}Module.cs:** +```csharp +public sealed class {Name}Module : IModule +{ + public void ConfigureServices(IHostApplicationBuilder builder) + { + // DbContext, repositories, services + } + + public void MapEndpoints(IEndpointRouteBuilder endpoints) + { + var group = endpoints.MapGroup("/api/v1/{name}"); + // Map feature endpoints + } +} +``` + +**{Name}PermissionConstants.cs:** +```csharp +public static class {Name}PermissionConstants +{ + // Permission groups per entity +} +``` + +**{Name}DbContext.cs:** +```csharp +public sealed class {Name}DbContext : DbContext +{ + // Entity sets and configuration +} +``` + +### Step 3: Create Contracts Project + +**Modules.{Name}.Contracts.csproj:** +```xml + + + net10.0 + + +``` + +### Step 4: Register Module + +Show changes needed in: +1. `src/Playground/Playground.Api/Program.cs` - Add to moduleAssemblies +2. `src/Playground/Playground.Api/Playground.Api.csproj` - Add ProjectReference +3. Solution file - Add both projects + +### Step 5: Add to Solution + +```bash +dotnet sln src/FSH.Framework.slnx add src/Modules/{Name}/Modules.{Name}/Modules.{Name}.csproj +dotnet sln src/FSH.Framework.slnx add src/Modules/{Name}/Modules.{Name}.Contracts/Modules.{Name}.Contracts.csproj +``` + +## Checklist + +- [ ] Both projects created (main + contracts) +- [ ] IModule implemented +- [ ] Permission constants defined +- [ ] DbContext created with schema +- [ ] Registered in Program.cs +- [ ] Added to solution +- [ ] Referenced from Playground.Api + +## Verification + +```bash +dotnet build src/FSH.Framework.slnx # Must be 0 warnings +``` diff --git a/.claude/rules.md b/.claude/rules.md deleted file mode 100644 index fa4cb09115..0000000000 --- a/.claude/rules.md +++ /dev/null @@ -1,76 +0,0 @@ -# Rules - -Hard constraints that must always be followed. - -## Architecture Rules - -1. **Vertical Slices Only** - - Every feature lives in `Modules/{Module}/Features/v1/{FeatureName}/` - - Never scatter feature code across multiple directories - - One folder = one complete feature - -2. **Module Boundaries** - - Modules only communicate through Contracts projects - - Never reference internal types across modules - - DTOs always in `Modules.{Name}.Contracts/` - -3. **BuildingBlocks Protection** - - Changes to `BuildingBlocks/` require explicit approval - - These affect ALL modules - changes ripple everywhere - - Prefer extension over modification - -## Code Rules - -4. **Mediator Not MediatR** - - Use `Mediator` source generator library - - Interfaces: `ICommand`, `IQuery`, `ICommandHandler`, `IQueryHandler` - - Never use `IRequest` or `IRequestHandler` (those are MediatR) - -5. **Validation Required** - - Every command must have an `AbstractValidator` - - No exceptions - validation is not optional - - Validators auto-register via FluentValidation - -6. **No Entity Exposure** - - Never return domain entities from endpoints - - Always map to DTOs from Contracts project - - Entities are internal implementation details - -7. **Explicit Authorization** - - Every endpoint needs `.RequirePermission()` or `.AllowAnonymous()` - - Use permission constants from `{Module}PermissionConstants` - - No implicit security - -8. **Zero Warnings** - - Build must pass with 0 warnings - - Run `dotnet build src/FSH.Framework.slnx` and verify - - CI enforces this - no exceptions - -## Naming Rules - -9. **Consistent Naming** - | Type | Pattern | - |------|---------| - | Commands | `{Action}{Entity}Command` | - | Queries | `Get{Entity}Query` or `Get{Entities}Query` | - | Handlers | `{CommandOrQuery}Handler` | - | Validators | `{Command}Validator` | - | Endpoints | `{CommandOrQuery}Endpoint` | - | DTOs | `{Entity}Dto`, `{Entity}Response`, `{Action}{Entity}Request` | - -10. **File = Type** - - One public type per file - - Filename matches type name exactly - - `CreateUserCommand.cs` contains `CreateUserCommand` - -## Multi-Tenancy Rules - -11. **Tenant Isolation** - - Entities with tenant data implement `IHasTenant` - - Framework auto-filters queries by tenant - - Never manually filter by TenantId in queries - -12. **No Hardcoded Tenants** - - Never hardcode tenant IDs - - Use `ICurrentUser.TenantId` for current tenant - - Tenant context comes from framework diff --git a/.claude/rules/api-conventions.md b/.claude/rules/api-conventions.md new file mode 100644 index 0000000000..11eedf5d7d --- /dev/null +++ b/.claude/rules/api-conventions.md @@ -0,0 +1,65 @@ +--- +paths: + - "src/Modules/**/Features/**/*" + - "src/Modules/**/*Endpoint*.cs" +--- + +# API Conventions + +Rules for API endpoints in FSH. + +## Endpoint Requirements + +Every endpoint MUST have: + +```csharp +endpoints.MapPost("/", handler) + .WithName(nameof(CommandOrQuery)) // Required: Unique name + .WithSummary("Description") // Required: OpenAPI description + .RequirePermission(Permission) // Required: Or .AllowAnonymous() +``` + +## HTTP Method Mapping + +| Operation | Method | Return | +|-----------|--------|--------| +| Create | `MapPost` | `TypedResults.Created(...)` | +| Read single | `MapGet` | `TypedResults.Ok(...)` | +| Read list | `MapGet` | `TypedResults.Ok(...)` | +| Update | `MapPut` | `TypedResults.Ok(...)` or `NoContent()` | +| Delete | `MapDelete` | `TypedResults.NoContent()` | + +## Route Patterns + +``` +/api/v1/{module}/{entities} # Collection +/api/v1/{module}/{entities}/{id} # Single item +/api/v1/{module}/{entities}/{id}/sub # Sub-resource +``` + +## Response Types + +Always use `TypedResults`: +- `TypedResults.Ok(data)` +- `TypedResults.Created($"/path/{id}", data)` +- `TypedResults.NoContent()` +- `TypedResults.NotFound()` +- `TypedResults.BadRequest(errors)` + +Never return raw objects or use `Results.Ok()`. + +## Permission Format + +```csharp +.RequirePermission({Module}Permissions.{Entity}.{Action}) +``` + +Actions: `View`, `Create`, `Update`, `Delete` + +## Query Parameters + +Use `[AsParameters]` for complex queries: + +```csharp +endpoints.MapGet("/", async ([AsParameters] GetProductsQuery query, ...) => ...) +``` diff --git a/.claude/rules/buildingblocks-protection.md b/.claude/rules/buildingblocks-protection.md new file mode 100644 index 0000000000..4abfe8b3a3 --- /dev/null +++ b/.claude/rules/buildingblocks-protection.md @@ -0,0 +1,36 @@ +--- +paths: + - "src/BuildingBlocks/**/*" +--- + +# ⚠️ BuildingBlocks Protection + +**STOP. You are modifying BuildingBlocks.** + +Changes to BuildingBlocks affect ALL modules across the entire framework. These are core abstractions that many projects depend on. + +## Before Proceeding + +1. **Confirm explicit approval** - Has the user specifically approved this change? +2. **Consider alternatives** - Can this be done in the module instead? +3. **Assess impact** - What modules will this affect? + +## If Approved + +- Make minimal, focused changes +- Ensure backward compatibility +- Update all affected modules +- Run full test suite: `dotnet test src/FSH.Framework.slnx` +- Document the change + +## Alternatives to Consider + +| Instead of... | Consider... | +|---------------|-------------| +| Modifying Core | Extension method in module | +| Changing Persistence | Custom repository in module | +| Updating Web | Module-specific middleware | + +## If Not Approved + +Do not proceed. Suggest alternatives that don't require BuildingBlocks modifications. diff --git a/.claude/rules/testing-rules.md b/.claude/rules/testing-rules.md new file mode 100644 index 0000000000..c7018746e7 --- /dev/null +++ b/.claude/rules/testing-rules.md @@ -0,0 +1,77 @@ +--- +paths: + - "src/Tests/**/*" +--- + +# Testing Rules + +Rules for tests in FSH. + +## Test Organization + +``` +src/Tests/ +├── Architecture.Tests/ # Layering enforcement (mandatory) +├── {Module}.Tests/ # Module-specific tests +└── Generic.Tests/ # Shared utilities +``` + +## Naming Conventions + +| Type | Pattern | +|------|---------| +| Test class | `{ClassUnderTest}Tests` | +| Test method | `{Method}_{Scenario}_{ExpectedResult}` | +| Test file | Same as class name | + +## Test Structure + +Always use Arrange-Act-Assert: + +```csharp +[Fact] +public async Task Handle_ValidCommand_ReturnsId() +{ + // Arrange + var command = new CreateProductCommand("Test", 10m); + + // Act + var result = await _handler.Handle(command, CancellationToken.None); + + // Assert + result.Id.Should().NotBeEmpty(); +} +``` + +## Required Tests + +### For Handlers +- Happy path with valid input +- Edge cases (empty, null, boundary values) +- Repository interactions verified + +### For Validators +- Each validation rule has a test +- Valid input passes +- Invalid input fails with correct property + +### For Entities +- Factory method creates valid entity +- Invalid input throws appropriate exception +- Domain events raised correctly + +## Libraries + +- **xUnit** - Test framework +- **FluentAssertions** - `.Should()` assertions +- **Moq** - `Mock` for dependencies + +## Architecture Tests + +Architecture tests in `Architecture.Tests/` are mandatory and enforce: +- Module boundary isolation +- No cross-module internal dependencies +- Handlers/validators are sealed +- Contracts don't depend on implementations + +These run on every build and PR. diff --git a/.claude/skills.md b/.claude/skills.md deleted file mode 100644 index 20ec25126a..0000000000 --- a/.claude/skills.md +++ /dev/null @@ -1,257 +0,0 @@ -# Skills - -Step-by-step guides for common tasks. - -## Skill: Add a Feature - -**When:** Creating a new API endpoint with business logic. - -**Steps:** - -1. **Create the folder** - ``` - Modules/{Module}/Features/v1/{FeatureName}/ - ``` - -2. **Create Command or Query** - ```csharp - // For state changes - public sealed record CreateProductCommand( - string Name, - decimal Price) : ICommand; - - // For reads - public sealed record GetProductQuery(Guid Id) : IQuery; - ``` - -3. **Create Handler** - ```csharp - public sealed class CreateProductHandler( - IRepository repository, - ICurrentUser currentUser) : ICommandHandler - { - public async ValueTask Handle( - CreateProductCommand command, - CancellationToken ct) - { - var product = Product.Create(command.Name, command.Price, currentUser.TenantId); - await repository.AddAsync(product, ct); - return new CreateProductResponse(product.Id); - } - } - ``` - -4. **Create Validator** (commands only) - ```csharp - public sealed class CreateProductValidator : AbstractValidator - { - public CreateProductValidator() - { - RuleFor(x => x.Name).NotEmpty().MaximumLength(200); - RuleFor(x => x.Price).GreaterThan(0); - } - } - ``` - -5. **Create Endpoint** - ```csharp - public static class CreateProductEndpoint - { - public static RouteHandlerBuilder Map(this IEndpointRouteBuilder endpoints) => - endpoints.MapPost("/", async ( - CreateProductCommand command, - IMediator mediator, - CancellationToken ct) => TypedResults.Created( - $"/products/{(await mediator.Send(command, ct)).Id}")) - .WithName(nameof(CreateProductCommand)) - .WithSummary("Create a new product") - .RequirePermission(CatalogPermissions.Products.Create); - } - ``` - -6. **Wire in Module** - ```csharp - // In module's MapEndpoints method - var products = endpoints.MapGroup("/products").WithTags("Products"); - products.MapCreateProductEndpoint(); - ``` - -7. **Add DTO to Contracts** - ```csharp - // In Modules.{Module}.Contracts/ - public sealed record CreateProductResponse(Guid Id); - public sealed record ProductDto(Guid Id, string Name, decimal Price); - ``` - -8. **Verify** - ```bash - dotnet build src/FSH.Framework.slnx # Must show 0 warnings - dotnet test src/FSH.Framework.slnx - ``` - ---- - -## Skill: Add a Module - -**When:** Creating a new bounded context (new business domain). - -**Steps:** - -1. **Create projects** - ``` - src/Modules/{Name}/ - ├── Modules.{Name}/ - │ ├── Modules.{Name}.csproj - │ ├── {Name}Module.cs - │ ├── {Name}PermissionConstants.cs - │ └── Features/v1/ - └── Modules.{Name}.Contracts/ - └── Modules.{Name}.Contracts.csproj - ``` - -2. **Implement IModule** - ```csharp - public sealed class CatalogModule : IModule - { - public void ConfigureServices(IHostApplicationBuilder builder) - { - // Register services, DbContext, etc. - } - - public void MapEndpoints(IEndpointRouteBuilder endpoints) - { - var group = endpoints.MapGroup("/api/v1/catalog"); - // Map feature endpoints here - } - } - ``` - -3. **Add permission constants** - ```csharp - public static class CatalogPermissionConstants - { - public static class Products - { - public const string View = "Products.View"; - public const string Create = "Products.Create"; - public const string Update = "Products.Update"; - public const string Delete = "Products.Delete"; - } - } - ``` - -4. **Register in Program.cs** - ```csharp - var moduleAssemblies = new Assembly[] - { - typeof(IdentityModule).Assembly, - typeof(MultitenancyModule).Assembly, - typeof(AuditingModule).Assembly, - typeof(CatalogModule).Assembly, // Add new module - }; - ``` - -5. **Add Mediator assemblies** (if module has commands/queries) - ```csharp - builder.Services.AddMediator(o => - { - o.Assemblies = [ - // ... existing assemblies - typeof(CreateProductCommand).Assembly, - typeof(CreateProductHandler).Assembly, - ]; - }); - ``` - ---- - -## Skill: Add Entity with Multi-Tenancy - -**When:** Creating a domain entity that should be tenant-isolated. - -```csharp -public sealed class Product : AggregateRoot, IHasTenant, IAuditableEntity, ISoftDeletable -{ - public string Name { get; private set; } = null!; - public decimal Price { get; private set; } - - // IHasTenant - public string TenantId { get; private set; } = null!; - - // IAuditableEntity - public DateTimeOffset CreatedAt { get; set; } - public string? CreatedBy { get; set; } - public DateTimeOffset? LastModifiedAt { get; set; } - public string? LastModifiedBy { get; set; } - - // ISoftDeletable - public DateTimeOffset? DeletedAt { get; set; } - public string? DeletedBy { get; set; } - - private Product() { } // EF Core - - public static Product Create(string name, decimal price, string tenantId) - { - var product = new Product - { - Id = Guid.NewGuid(), - Name = name, - Price = price, - TenantId = tenantId - }; - product.AddDomainEvent(new ProductCreatedEvent(product.Id)); - return product; - } -} -``` - ---- - -## Skill: Query with Pagination - -```csharp -public sealed record GetProductsQuery( - string? Search, - int PageNumber = 1, - int PageSize = 10) : IQuery>; - -public sealed class GetProductsHandler( - IReadRepository repository) : IQueryHandler> -{ - public async ValueTask> Handle( - GetProductsQuery query, - CancellationToken ct) - { - var spec = new ProductSearchSpec(query.Search, query.PageNumber, query.PageSize); - return await repository.PaginatedListAsync(spec, ct); - } -} -``` - ---- - -## Skill: Run & Test - -```bash -# Build (must be 0 warnings) -dotnet build src/FSH.Framework.slnx - -# Run tests -dotnet test src/FSH.Framework.slnx - -# Run with Aspire (Postgres + Redis auto-provisioned) -dotnet run --project src/Playground/FSH.Playground.AppHost - -# Run API only (manual DB setup required) -dotnet run --project src/Playground/Playground.Api - -# Add migration -dotnet ef migrations add {Name} \ - --project src/Playground/Migrations.PostgreSQL \ - --startup-project src/Playground/Playground.Api - -# Apply migrations -dotnet ef database update \ - --project src/Playground/Migrations.PostgreSQL \ - --startup-project src/Playground/Playground.Api -``` diff --git a/.claude/skills/add-entity/SKILL.md b/.claude/skills/add-entity/SKILL.md new file mode 100644 index 0000000000..1967ab97cf --- /dev/null +++ b/.claude/skills/add-entity/SKILL.md @@ -0,0 +1,164 @@ +--- +name: add-entity +description: Create a domain entity with multi-tenancy, auditing, soft-delete, and domain events. Use when adding new database entities to a module. +argument-hint: [ModuleName] [EntityName] +--- + +# Add Entity + +Create a domain entity following FSH patterns with full multi-tenancy support. + +## Entity Template + +```csharp +public sealed class {Entity} : AggregateRoot, IHasTenant, IAuditableEntity, ISoftDeletable +{ + // Domain properties + public string Name { get; private set; } = null!; + public decimal Price { get; private set; } + public string? Description { get; private set; } + + // IHasTenant - automatic tenant isolation + public string TenantId { get; private set; } = null!; + + // IAuditableEntity - automatic audit trails + public DateTimeOffset CreatedAt { get; set; } + public string? CreatedBy { get; set; } + public DateTimeOffset? LastModifiedAt { get; set; } + public string? LastModifiedBy { get; set; } + + // ISoftDeletable - automatic soft deletes + public DateTimeOffset? DeletedAt { get; set; } + public string? DeletedBy { get; set; } + + // Private constructor for EF Core + private {Entity}() { } + + // Factory method - the only way to create + public static {Entity} Create(string name, decimal price, string tenantId) + { + ArgumentException.ThrowIfNullOrWhiteSpace(name); + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(price); + + var entity = new {Entity} + { + Id = Guid.NewGuid(), + Name = name, + Price = price, + TenantId = tenantId + }; + + entity.AddDomainEvent(new {Entity}CreatedEvent(entity.Id)); + return entity; + } + + // Domain methods for state changes + public void UpdateDetails(string name, decimal price, string? description) + { + ArgumentException.ThrowIfNullOrWhiteSpace(name); + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(price); + + Name = name; + Price = price; + Description = description; + + AddDomainEvent(new {Entity}UpdatedEvent(Id)); + } +} +``` + +## Domain Events + +```csharp +public sealed record {Entity}CreatedEvent(Guid {Entity}Id) : IDomainEvent; +public sealed record {Entity}UpdatedEvent(Guid {Entity}Id) : IDomainEvent; +public sealed record {Entity}DeletedEvent(Guid {Entity}Id) : IDomainEvent; +``` + +## EF Core Configuration + +```csharp +public sealed class {Entity}Configuration : IEntityTypeConfiguration<{Entity}> +{ + public void Configure(EntityTypeBuilder<{Entity}> builder) + { + builder.ToTable("{entities}"); + + builder.HasKey(x => x.Id); + + builder.Property(x => x.Name) + .IsRequired() + .HasMaxLength(200); + + builder.Property(x => x.Price) + .HasPrecision(18, 2); + + builder.Property(x => x.TenantId) + .IsRequired() + .HasMaxLength(64); + + builder.HasIndex(x => x.TenantId); + + // Global query filter for tenant isolation + builder.HasQueryFilter(x => x.DeletedAt == null); + } +} +``` + +## Register in DbContext + +```csharp +public sealed class {Module}DbContext : DbContext +{ + public DbSet<{Entity}> {Entities} => Set<{Entity}>(); + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.HasDefaultSchema("{module}"); + modelBuilder.ApplyConfigurationsFromAssembly(typeof({Module}DbContext).Assembly); + } +} +``` + +## Add Migration + +```bash +dotnet ef migrations add Add{Entity} \ + --project src/Playground/Migrations.PostgreSQL \ + --startup-project src/Playground/Playground.Api + +dotnet ef database update \ + --project src/Playground/Migrations.PostgreSQL \ + --startup-project src/Playground/Playground.Api +``` + +## Interfaces Reference + +| Interface | Purpose | Auto-Handled | +|-----------|---------|--------------| +| `IHasTenant` | Tenant isolation | Query filtering | +| `IAuditableEntity` | Created/Modified tracking | SaveChanges interceptor | +| `ISoftDeletable` | Soft delete support | Delete interceptor | +| `AggregateRoot` | Domain events support | Event dispatcher | + +## Key Rules + +1. **Private constructor** - EF Core needs it, but users use factory methods +2. **Factory methods** - All creation goes through `Create()` static method +3. **Domain methods** - State changes through methods, not property setters +4. **Domain events** - Raise events for significant state changes +5. **Validation in methods** - Validate in factory/domain methods, not entity +6. **No public setters** - Properties are `private set` + +## Checklist + +- [ ] Implements `AggregateRoot` +- [ ] Implements `IHasTenant` for tenant isolation +- [ ] Implements `IAuditableEntity` for audit trails +- [ ] Implements `ISoftDeletable` for soft deletes +- [ ] Has private constructor +- [ ] Has static factory method +- [ ] Domain events raised for state changes +- [ ] EF configuration created +- [ ] Added to DbContext +- [ ] Migration created diff --git a/.claude/skills/add-feature/SKILL.md b/.claude/skills/add-feature/SKILL.md new file mode 100644 index 0000000000..810a08b8e6 --- /dev/null +++ b/.claude/skills/add-feature/SKILL.md @@ -0,0 +1,116 @@ +--- +name: add-feature +description: Create a new API endpoint with Command, Handler, Validator, and Endpoint following FSH vertical slice architecture. Use when adding any new feature, API endpoint, or business operation. +argument-hint: [ModuleName] [FeatureName] +--- + +# Add Feature + +Create a complete vertical slice feature with all required files. + +## File Structure + +``` +src/Modules/{Module}/Features/v1/{FeatureName}/ +├── {Action}{Entity}Command.cs # or Get{Entity}Query.cs +├── {Action}{Entity}Handler.cs +├── {Action}{Entity}Validator.cs # Commands only +└── {Action}{Entity}Endpoint.cs +``` + +## Step 1: Create Command or Query + +**For state changes (POST/PUT/DELETE):** +```csharp +public sealed record Create{Entity}Command( + string Name, + decimal Price) : ICommand; +``` + +**For reads (GET):** +```csharp +public sealed record Get{Entity}Query(Guid Id) : IQuery<{Entity}Dto>; +``` + +## Step 2: Create Handler + +```csharp +public sealed class Create{Entity}Handler( + IRepository<{Entity}> repository, + ICurrentUser currentUser) : ICommandHandler +{ + public async ValueTask Handle( + Create{Entity}Command command, + CancellationToken ct) + { + var entity = {Entity}.Create(command.Name, command.Price, currentUser.TenantId); + await repository.AddAsync(entity, ct); + return new Create{Entity}Response(entity.Id); + } +} +``` + +## Step 3: Create Validator (Commands Only) + +```csharp +public sealed class Create{Entity}Validator : AbstractValidator +{ + public Create{Entity}Validator() + { + RuleFor(x => x.Name).NotEmpty().MaximumLength(200); + RuleFor(x => x.Price).GreaterThan(0); + } +} +``` + +## Step 4: Create Endpoint + +```csharp +public static class Create{Entity}Endpoint +{ + public static RouteHandlerBuilder Map(this IEndpointRouteBuilder endpoints) => + endpoints.MapPost("/", async ( + Create{Entity}Command command, + IMediator mediator, + CancellationToken ct) => TypedResults.Created( + $"/{entities}/{(await mediator.Send(command, ct)).Id}")) + .WithName(nameof(Create{Entity}Command)) + .WithSummary("Create a new {entity}") + .RequirePermission({Module}Permissions.{Entities}.Create); +} +``` + +## Step 5: Add DTOs to Contracts + +In `src/Modules/{Module}/Modules.{Module}.Contracts/`: + +```csharp +public sealed record Create{Entity}Response(Guid Id); +public sealed record {Entity}Dto(Guid Id, string Name, decimal Price); +``` + +## Step 6: Wire Endpoint in Module + +In `{Module}Module.cs` MapEndpoints method: + +```csharp +var entities = endpoints.MapGroup("/{entities}").WithTags("{Entities}"); +entities.Map{Action}{Entity}Endpoint(); +``` + +## Step 7: Verify + +```bash +dotnet build src/FSH.Framework.slnx # Must be 0 warnings +dotnet test src/FSH.Framework.slnx +``` + +## Checklist + +- [ ] Command/Query uses `ICommand` or `IQuery` (NOT MediatR's IRequest) +- [ ] Handler uses `ICommandHandler` or `IQueryHandler` +- [ ] Validator exists for commands +- [ ] Endpoint has `.RequirePermission()` or `.AllowAnonymous()` +- [ ] Endpoint has `.WithName()` and `.WithSummary()` +- [ ] DTOs in Contracts project, not internal +- [ ] Build passes with 0 warnings diff --git a/.claude/skills/add-module/SKILL.md b/.claude/skills/add-module/SKILL.md new file mode 100644 index 0000000000..6042436058 --- /dev/null +++ b/.claude/skills/add-module/SKILL.md @@ -0,0 +1,176 @@ +--- +name: add-module +description: Create a new module (bounded context) with proper project structure, permissions, DbContext, and registration. Use when adding a new business domain that needs its own entities and endpoints. +argument-hint: [ModuleName] +--- + +# Add Module + +Create a new bounded context with full project structure. + +## When to Create a New Module + +- Has its own domain entities +- Could be deployed independently +- Represents a distinct business domain + +If it's just a feature in an existing domain, use `add-feature` instead. + +## Project Structure + +``` +src/Modules/{Name}/ +├── Modules.{Name}/ +│ ├── Modules.{Name}.csproj +│ ├── {Name}Module.cs +│ ├── {Name}PermissionConstants.cs +│ ├── {Name}DbContext.cs +│ ├── Domain/ +│ │ └── {Entity}.cs +│ └── Features/v1/ +│ └── {Feature}/ +└── Modules.{Name}.Contracts/ + ├── Modules.{Name}.Contracts.csproj + └── DTOs/ +``` + +## Step 1: Create Projects + +### Main Module Project +`src/Modules/{Name}/Modules.{Name}/Modules.{Name}.csproj`: +```xml + + + net10.0 + + + + + + + + +``` + +### Contracts Project +`src/Modules/{Name}/Modules.{Name}.Contracts/Modules.{Name}.Contracts.csproj`: +```xml + + + net10.0 + + +``` + +## Step 2: Implement IModule + +```csharp +public sealed class {Name}Module : IModule +{ + public void ConfigureServices(IHostApplicationBuilder builder) + { + // Register DbContext + builder.Services.AddDbContext<{Name}DbContext>((sp, options) => + { + var dbOptions = sp.GetRequiredService>().Value; + options.UseNpgsql(dbOptions.ConnectionString); + }); + + // Register repositories + builder.Services.AddScoped(typeof(IRepository<>), typeof(Repository<>)); + builder.Services.AddScoped(typeof(IReadRepository<>), typeof(Repository<>)); + } + + public void MapEndpoints(IEndpointRouteBuilder endpoints) + { + var group = endpoints.MapGroup("/api/v1/{name}"); + // Map feature endpoints here + } +} +``` + +## Step 3: Add Permission Constants + +```csharp +public static class {Name}PermissionConstants +{ + public static class {Entities} + { + public const string View = "{Entities}.View"; + public const string Create = "{Entities}.Create"; + public const string Update = "{Entities}.Update"; + public const string Delete = "{Entities}.Delete"; + } +} +``` + +## Step 4: Create DbContext + +```csharp +public sealed class {Name}DbContext : DbContext +{ + public {Name}DbContext(DbContextOptions<{Name}DbContext> options) : base(options) { } + + public DbSet<{Entity}> {Entities} => Set<{Entity}>(); + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.HasDefaultSchema("{name}"); + modelBuilder.ApplyConfigurationsFromAssembly(typeof({Name}DbContext).Assembly); + } +} +``` + +## Step 5: Register in Program.cs + +```csharp +// Add to moduleAssemblies array +var moduleAssemblies = new Assembly[] +{ + typeof(IdentityModule).Assembly, + typeof(MultitenancyModule).Assembly, + typeof(AuditingModule).Assembly, + typeof({Name}Module).Assembly, // Add here +}; + +// Add Mediator assemblies if module has commands/queries +builder.Services.AddMediator(o => +{ + o.Assemblies = [ + // ... existing + typeof({Name}Module).Assembly, + ]; +}); +``` + +## Step 6: Add to Solution + +```bash +dotnet sln src/FSH.Framework.slnx add src/Modules/{Name}/Modules.{Name}/Modules.{Name}.csproj +dotnet sln src/FSH.Framework.slnx add src/Modules/{Name}/Modules.{Name}.Contracts/Modules.{Name}.Contracts.csproj +``` + +## Step 7: Reference from API + +In `src/Playground/Playground.Api/Playground.Api.csproj`: +```xml + +``` + +## Step 8: Verify + +```bash +dotnet build src/FSH.Framework.slnx # Must be 0 warnings +dotnet test src/FSH.Framework.slnx +``` + +## Checklist + +- [ ] Both projects created (main + contracts) +- [ ] IModule implemented with ConfigureServices and MapEndpoints +- [ ] Permission constants defined +- [ ] DbContext created with proper schema +- [ ] Registered in Program.cs moduleAssemblies +- [ ] Added to solution file +- [ ] Referenced from Playground.Api +- [ ] Build passes with 0 warnings diff --git a/.claude/skills/mediator-reference/SKILL.md b/.claude/skills/mediator-reference/SKILL.md new file mode 100644 index 0000000000..2b106eef71 --- /dev/null +++ b/.claude/skills/mediator-reference/SKILL.md @@ -0,0 +1,132 @@ +--- +name: mediator-reference +description: Mediator library patterns and interfaces for FSH. This project uses the Mediator source generator, NOT MediatR. Reference when implementing commands, queries, and handlers. +user-invocable: false +--- + +# Mediator Reference + +⚠️ **FSH uses the `Mediator` source generator library, NOT `MediatR`.** + +These are different libraries with different interfaces. Using MediatR interfaces will cause build errors. + +## Interface Comparison + +| Purpose | ✅ Mediator (Use This) | ❌ MediatR (Don't Use) | +|---------|------------------------|------------------------| +| Command | `ICommand` | `IRequest` | +| Query | `IQuery` | `IRequest` | +| Command Handler | `ICommandHandler` | `IRequestHandler` | +| Query Handler | `IQueryHandler` | `IRequestHandler` | +| Notification | `INotification` | `INotification` | +| Notification Handler | `INotificationHandler` | `INotificationHandler` | + +## Command Pattern + +```csharp +// ✅ Correct - Mediator +public sealed record CreateUserCommand(string Email, string Name) : ICommand; + +public sealed class CreateUserHandler : ICommandHandler +{ + public async ValueTask Handle(CreateUserCommand command, CancellationToken ct) + { + // Implementation + } +} + +// ❌ Wrong - MediatR +public sealed record CreateUserCommand(string Email, string Name) : IRequest; + +public sealed class CreateUserHandler : IRequestHandler +{ + public async Task Handle(CreateUserCommand request, CancellationToken ct) + { + // This won't work! + } +} +``` + +## Query Pattern + +```csharp +// ✅ Correct - Mediator +public sealed record GetUserQuery(Guid Id) : IQuery; + +public sealed class GetUserHandler : IQueryHandler +{ + public async ValueTask Handle(GetUserQuery query, CancellationToken ct) + { + // Implementation + } +} +``` + +## Key Differences + +| Aspect | Mediator | MediatR | +|--------|----------|---------| +| Return type | `ValueTask` | `Task` | +| Parameter name | `command` / `query` | `request` | +| Registration | Source generated | Runtime reflection | +| Performance | Faster (compile-time) | Slower (runtime) | + +## Sending Commands/Queries + +```csharp +// In endpoint or controller +public class MyEndpoint +{ + public static async Task Handle( + CreateUserCommand command, + IMediator mediator, // Same interface name + CancellationToken ct) + { + var result = await mediator.Send(command, ct); + return TypedResults.Created($"/users/{result}"); + } +} +``` + +## Registration + +```csharp +// In Program.cs +builder.Services.AddMediator(options => +{ + options.Assemblies = + [ + typeof(IdentityModule).Assembly, + typeof(MultitenancyModule).Assembly, + // Add your module assemblies here + ]; +}); +``` + +## Common Errors + +### Error: `IRequest` not found +**Cause:** Using MediatR interface +**Fix:** Change to `ICommand` or `IQuery` + +### Error: `IRequestHandler` not found +**Cause:** Using MediatR interface +**Fix:** Change to `ICommandHandler` or `IQueryHandler` + +### Error: Handler not found at runtime +**Cause:** Assembly not registered in AddMediator +**Fix:** Add assembly to `options.Assemblies` array + +### Error: `Task` vs `ValueTask` +**Cause:** Using MediatR return type +**Fix:** Change handler return type to `ValueTask` + +## Namespaces + +```csharp +// ✅ Correct +using Mediator; + +// ❌ Wrong +using MediatR; +``` diff --git a/.claude/skills/query-patterns/SKILL.md b/.claude/skills/query-patterns/SKILL.md new file mode 100644 index 0000000000..fb5f310d15 --- /dev/null +++ b/.claude/skills/query-patterns/SKILL.md @@ -0,0 +1,187 @@ +--- +name: query-patterns +description: Query patterns including pagination, search, filtering, and specifications for FSH. Use when implementing GET endpoints that return lists or need filtering. +--- + +# Query Patterns + +Reference for implementing queries with pagination, search, and filtering. + +## Basic Paginated Query + +```csharp +// Query +public sealed record Get{Entities}Query( + string? Search, + int PageNumber = 1, + int PageSize = 10) : IQuery>; + +// Handler +public sealed class Get{Entities}Handler( + IReadRepository<{Entity}> repository) : IQueryHandler> +{ + public async ValueTask> Handle( + Get{Entities}Query query, + CancellationToken ct) + { + var spec = new {Entity}SearchSpec(query.Search, query.PageNumber, query.PageSize); + return await repository.PaginatedListAsync(spec, ct); + } +} +``` + +## Specification Pattern + +```csharp +public sealed class {Entity}SearchSpec : EntitiesByPaginationFilterSpec<{Entity}, {Entity}Dto> +{ + public {Entity}SearchSpec(string? search, int pageNumber, int pageSize) + : base(new PaginationFilter(pageNumber, pageSize)) + { + Query + .OrderByDescending(x => x.CreatedAt) + .Where(x => string.IsNullOrEmpty(search) || + x.Name.Contains(search) || + x.Description!.Contains(search)); + } +} +``` + +## Get Single Entity + +```csharp +// Query +public sealed record Get{Entity}Query(Guid Id) : IQuery<{Entity}Dto>; + +// Handler +public sealed class Get{Entity}Handler( + IReadRepository<{Entity}> repository) : IQueryHandler +{ + public async ValueTask<{Entity}Dto> Handle(Get{Entity}Query query, CancellationToken ct) + { + var spec = new {Entity}ByIdSpec(query.Id); + var entity = await repository.FirstOrDefaultAsync(spec, ct); + + return entity ?? throw new NotFoundException($"{Entity} {query.Id} not found"); + } +} + +// Specification +public sealed class {Entity}ByIdSpec : Specification<{Entity}, {Entity}Dto>, ISingleResultSpecification<{Entity}> +{ + public {Entity}ByIdSpec(Guid id) + { + Query.Where(x => x.Id == id); + } +} +``` + +## Advanced Filtering + +```csharp +public sealed record Get{Entities}Query( + string? Search, + Guid? CategoryId, + decimal? MinPrice, + decimal? MaxPrice, + DateTimeOffset? CreatedAfter, + bool? IsActive, + string? SortBy, + bool SortDescending = false, + int PageNumber = 1, + int PageSize = 10) : IQuery>; + +public sealed class {Entity}FilterSpec : EntitiesByPaginationFilterSpec<{Entity}, {Entity}Dto> +{ + public {Entity}FilterSpec(Get{Entities}Query query) + : base(new PaginationFilter(query.PageNumber, query.PageSize)) + { + Query + // Search + .Where(x => string.IsNullOrEmpty(query.Search) || + x.Name.Contains(query.Search)) + + // Filters + .Where(x => !query.CategoryId.HasValue || + x.CategoryId == query.CategoryId) + .Where(x => !query.MinPrice.HasValue || + x.Price >= query.MinPrice) + .Where(x => !query.MaxPrice.HasValue || + x.Price <= query.MaxPrice) + .Where(x => !query.CreatedAfter.HasValue || + x.CreatedAt >= query.CreatedAfter) + .Where(x => !query.IsActive.HasValue || + x.IsActive == query.IsActive); + + // Dynamic sorting + ApplySorting(query.SortBy, query.SortDescending); + } + + private void ApplySorting(string? sortBy, bool descending) + { + switch (sortBy?.ToLowerInvariant()) + { + case "name": + if (descending) Query.OrderByDescending(x => x.Name); + else Query.OrderBy(x => x.Name); + break; + case "price": + if (descending) Query.OrderByDescending(x => x.Price); + else Query.OrderBy(x => x.Price); + break; + default: + Query.OrderByDescending(x => x.CreatedAt); + break; + } + } +} +``` + +## Endpoint Patterns + +### List Endpoint +```csharp +public static RouteHandlerBuilder MapGet{Entities}Endpoint(this IEndpointRouteBuilder endpoints) => + endpoints.MapGet("/", async ( + [AsParameters] Get{Entities}Query query, + IMediator mediator, + CancellationToken ct) => TypedResults.Ok(await mediator.Send(query, ct))) + .WithName(nameof(Get{Entities}Query)) + .WithSummary("Get paginated list of {entities}") + .RequirePermission({Module}Permissions.{Entities}.View); +``` + +### Single Entity Endpoint +```csharp +public static RouteHandlerBuilder MapGet{Entity}Endpoint(this IEndpointRouteBuilder endpoints) => + endpoints.MapGet("/{id:guid}", async ( + Guid id, + IMediator mediator, + CancellationToken ct) => TypedResults.Ok(await mediator.Send(new Get{Entity}Query(id), ct))) + .WithName(nameof(Get{Entity}Query)) + .WithSummary("Get {entity} by ID") + .RequirePermission({Module}Permissions.{Entities}.View); +``` + +## Response Types + +```csharp +// In Contracts project +public sealed record {Entity}Dto( + Guid Id, + string Name, + decimal Price, + string? Description, + DateTimeOffset CreatedAt); + +// PagedList is from BuildingBlocks +// Returns: Items, PageNumber, PageSize, TotalCount, TotalPages +``` + +## Key Points + +1. **Use specifications** - Don't write raw LINQ in handlers +2. **Tenant filtering is automatic** - Framework handles `IHasTenant` +3. **Soft delete filtering is automatic** - DeletedAt != null filtered out +4. **Use `[AsParameters]`** - For query parameters in endpoints +5. **Project to DTOs** - Never return entities directly diff --git a/.claude/skills/testing-guide/SKILL.md b/.claude/skills/testing-guide/SKILL.md new file mode 100644 index 0000000000..de2fe6e9b3 --- /dev/null +++ b/.claude/skills/testing-guide/SKILL.md @@ -0,0 +1,223 @@ +--- +name: testing-guide +description: Write unit tests, integration tests, and architecture tests for FSH features. Use when adding tests or understanding the testing strategy. +--- + +# Testing Guide + +FSH uses a layered testing strategy with architecture tests as guardrails. + +## Test Project Structure + +``` +src/Tests/ +├── Architecture.Tests/ # Enforces layering rules +├── Generic.Tests/ # Shared test utilities +├── Identity.Tests/ # Identity module tests +├── Multitenancy.Tests/ # Multitenancy module tests +└── Auditing.Tests/ # Auditing module tests +``` + +## Architecture Tests + +Architecture tests enforce module boundaries and layering. They run on every build. + +```csharp +public class ArchitectureTests +{ + [Fact] + public void Modules_ShouldNot_DependOnOtherModules() + { + var result = Types.InAssembly(typeof(IdentityModule).Assembly) + .ShouldNot() + .HaveDependencyOn("Modules.Multitenancy") + .GetResult(); + + result.IsSuccessful.Should().BeTrue(); + } + + [Fact] + public void Contracts_ShouldNot_DependOnImplementation() + { + var result = Types.InAssembly(typeof(UserDto).Assembly) + .ShouldNot() + .HaveDependencyOn("Modules.Identity") + .GetResult(); + + result.IsSuccessful.Should().BeTrue(); + } + + [Fact] + public void Handlers_ShouldBe_Sealed() + { + var result = Types.InAssembly(typeof(IdentityModule).Assembly) + .That() + .ImplementInterface(typeof(ICommandHandler<,>)) + .Or() + .ImplementInterface(typeof(IQueryHandler<,>)) + .Should() + .BeSealed() + .GetResult(); + + result.IsSuccessful.Should().BeTrue(); + } +} +``` + +## Unit Test Patterns + +### Handler Tests + +```csharp +public class Create{Entity}HandlerTests +{ + private readonly Mock> _repositoryMock; + private readonly Mock _currentUserMock; + private readonly Create{Entity}Handler _handler; + + public Create{Entity}HandlerTests() + { + _repositoryMock = new Mock>(); + _currentUserMock = new Mock(); + _currentUserMock.Setup(x => x.TenantId).Returns("test-tenant"); + + _handler = new Create{Entity}Handler( + _repositoryMock.Object, + _currentUserMock.Object); + } + + [Fact] + public async Task Handle_ValidCommand_Returns{Entity}Id() + { + // Arrange + var command = new Create{Entity}Command("Test", 99.99m); + _repositoryMock + .Setup(x => x.AddAsync(It.IsAny<{Entity}>(), It.IsAny())) + .Returns(Task.CompletedTask); + + // Act + var result = await _handler.Handle(command, CancellationToken.None); + + // Assert + result.Id.Should().NotBeEmpty(); + _repositoryMock.Verify(x => x.AddAsync( + It.Is<{Entity}>(e => e.Name == "Test" && e.Price == 99.99m), + It.IsAny()), Times.Once); + } +} +``` + +### Validator Tests + +```csharp +public class Create{Entity}ValidatorTests +{ + private readonly Create{Entity}Validator _validator = new(); + + [Fact] + public void Validate_EmptyName_Fails() + { + var command = new Create{Entity}Command("", 99.99m); + var result = _validator.Validate(command); + + result.IsValid.Should().BeFalse(); + result.Errors.Should().Contain(e => e.PropertyName == "Name"); + } + + [Fact] + public void Validate_NegativePrice_Fails() + { + var command = new Create{Entity}Command("Test", -1m); + var result = _validator.Validate(command); + + result.IsValid.Should().BeFalse(); + result.Errors.Should().Contain(e => e.PropertyName == "Price"); + } + + [Theory] + [InlineData("Valid Name", 10)] + [InlineData("Another", 0.01)] + public void Validate_ValidCommand_Passes(string name, decimal price) + { + var command = new Create{Entity}Command(name, price); + var result = _validator.Validate(command); + + result.IsValid.Should().BeTrue(); + } +} +``` + +### Entity Tests + +```csharp +public class {Entity}Tests +{ + [Fact] + public void Create_ValidInput_Creates{Entity}WithEvent() + { + var entity = {Entity}.Create("Test", 99.99m, "tenant-1"); + + entity.Id.Should().NotBeEmpty(); + entity.Name.Should().Be("Test"); + entity.Price.Should().Be(99.99m); + entity.TenantId.Should().Be("tenant-1"); + entity.DomainEvents.Should().ContainSingle(e => e is {Entity}CreatedEvent); + } + + [Fact] + public void Create_EmptyName_ThrowsArgumentException() + { + var act = () => {Entity}.Create("", 99.99m, "tenant-1"); + + act.Should().Throw(); + } + + [Fact] + public void UpdateDetails_ValidInput_UpdatesAndRaisesEvent() + { + var entity = {Entity}.Create("Original", 50m, "tenant-1"); + entity.ClearDomainEvents(); + + entity.UpdateDetails("Updated", 75m, "New description"); + + entity.Name.Should().Be("Updated"); + entity.Price.Should().Be(75m); + entity.Description.Should().Be("New description"); + entity.DomainEvents.Should().ContainSingle(e => e is {Entity}UpdatedEvent); + } +} +``` + +## Running Tests + +```bash +# Run all tests +dotnet test src/FSH.Framework.slnx + +# Run specific test project +dotnet test src/Tests/Architecture.Tests + +# Run with coverage +dotnet test src/FSH.Framework.slnx --collect:"XPlat Code Coverage" + +# Run specific test +dotnet test --filter "FullyQualifiedName~Create{Entity}HandlerTests" +``` + +## Test Conventions + +| Convention | Example | +|------------|---------| +| Test class name | `{ClassUnderTest}Tests` | +| Test method name | `{Method}_{Scenario}_{ExpectedResult}` | +| Arrange-Act-Assert | Always use this structure | +| One assertion concept | Multiple asserts OK if same concept | + +## Key Rules + +1. **Architecture tests are mandatory** - They enforce module boundaries +2. **Validators need tests** - Cover edge cases +3. **Handlers need tests** - Mock dependencies +4. **Entities need tests** - Test factory methods and domain logic +5. **Use FluentAssertions** - `.Should()` syntax +6. **Use Moq for mocking** - `Mock` pattern diff --git a/CLAUDE.md b/CLAUDE.md index 2102031df4..6f0eb527c6 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -7,7 +7,7 @@ ```bash dotnet build src/FSH.Framework.slnx # Build (0 warnings required) dotnet test src/FSH.Framework.slnx # Test -dotnet run --project src/Playground/FSH.Playground.AppHost # Run +dotnet run --project src/Playground/FSH.Playground.AppHost # Run with Aspire ``` ## Project Structure @@ -25,48 +25,60 @@ src/ ## The Pattern -Every feature = 4 files in one folder: +Every feature = vertical slice in one folder: ``` Modules/{Module}/Features/v1/{Feature}/ -├── {Feature}Command.cs → ICommand -├── {Feature}Handler.cs → ICommandHandler -├── {Feature}Validator.cs → AbstractValidator -└── {Feature}Endpoint.cs → RouteHandlerBuilder extension +├── {Action}{Entity}Command.cs # ICommand (NOT IRequest!) +├── {Action}{Entity}Handler.cs # ICommandHandler returns ValueTask +├── {Action}{Entity}Validator.cs # AbstractValidator +└── {Action}{Entity}Endpoint.cs # MapPost/Get/Put/Delete ``` -## Essential Rules +## Critical Rules | Rule | Why | |------|-----| | Use `Mediator` not `MediatR` | Different library, different interfaces | +| `ICommand` / `IQuery` | NOT `IRequest` | +| `ValueTask` return type | NOT `Task` | | DTOs in Contracts project | Keep internals internal | | Every command needs validator | No unvalidated input | | `.RequirePermission()` on endpoints | Explicit authorization | | Zero build warnings | CI enforces this | -## Deep Dive +## Available Skills -| Topic | File | -|-------|------| -| All rules & constraints | [.claude/rules.md](.claude/rules.md) | -| Step-by-step guides | [.claude/skills.md](.claude/skills.md) | -| AI behavior guidelines | [.claude/agents.md](.claude/agents.md) | +| Skill | When to Use | +|-------|-------------| +| `/add-feature` | Creating new API endpoints | +| `/add-module` | Creating new bounded contexts | +| `/add-entity` | Adding domain entities | +| `/query-patterns` | Implementing GET with pagination/filtering | +| `/testing-guide` | Writing tests | ---- +## Available Agents + +| Agent | Purpose | +|-------|---------| +| `code-reviewer` | Review changes against FSH patterns | +| `feature-scaffolder` | Generate complete feature files | +| `module-creator` | Scaffold new modules | +| `architecture-guard` | Verify architectural integrity | +| `migration-helper` | Handle EF Core migrations | ## Quick Patterns ### Command + Handler ```csharp -public sealed record CreateUserCommand(string Email, string Name) : ICommand; +public sealed record CreateUserCommand(string Email) : ICommand; public sealed class CreateUserHandler(IRepository repo) : ICommandHandler { public async ValueTask Handle(CreateUserCommand cmd, CancellationToken ct) { - var user = User.Create(cmd.Email, cmd.Name); + var user = User.Create(cmd.Email); await repo.AddAsync(user, ct); return user.Id; } @@ -75,10 +87,10 @@ public sealed class CreateUserHandler(IRepository repo) ### Endpoint ```csharp -public static RouteHandlerBuilder MapCreateUserEndpoint(this IEndpointRouteBuilder e) => +public static RouteHandlerBuilder Map(this IEndpointRouteBuilder e) => e.MapPost("/", async (CreateUserCommand cmd, IMediator m, CancellationToken ct) => TypedResults.Created($"/users/{await m.Send(cmd, ct)}")) - .WithName("CreateUser") + .WithName(nameof(CreateUserCommand)) .WithSummary("Create a new user") .RequirePermission(IdentityPermissions.Users.Create); ``` @@ -90,17 +102,13 @@ public sealed class CreateUserValidator : AbstractValidator public CreateUserValidator() { RuleFor(x => x.Email).NotEmpty().EmailAddress(); - RuleFor(x => x.Name).NotEmpty().MaximumLength(100); } } ``` ---- - -## Configuration (Production) +## Before Committing -``` -DatabaseOptions:ConnectionString ← Required -CachingOptions:Redis ← Required -JwtOptions:SigningKey ← Required (256-bit) +```bash +dotnet build src/FSH.Framework.slnx # Must be 0 warnings +dotnet test src/FSH.Framework.slnx # All tests pass ```