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 65b54f181f..2102031df4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,239 +1,106 @@ # CLAUDE.md -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. +> FullStackHero .NET Starter Kit — AI Assistant Guidelines -## Build & Run Commands +## Quick Reference ```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 - -# Run API standalone (requires DB/Redis/JWT config in appsettings) -dotnet run --project src/Playground/Playground.Api +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 +``` -# Run all tests -dotnet test src/FSH.Framework.slnx +## Project Structure -# Run single test project -dotnet test src/Tests/Architecture.Tests +``` +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 +``` -# Run specific test -dotnet test src/Tests/Architecture.Tests --filter "FullyQualifiedName~TestMethodName" +## The Pattern -# Generate C# API client from OpenAPI spec (requires API running) -./scripts/openapi/generate-api-clients.ps1 -SpecUrl "https://localhost:7030/openapi/v1.json" +Every feature = 4 files in one folder: -# Check for OpenAPI drift (CI validation) -./scripts/openapi/check-openapi-drift.ps1 -SpecUrl "" +``` +Modules/{Module}/Features/v1/{Feature}/ +├── {Feature}Command.cs → ICommand +├── {Feature}Handler.cs → ICommandHandler +├── {Feature}Validator.cs → AbstractValidator +└── {Feature}Endpoint.cs → RouteHandlerBuilder extension ``` -## Architecture +## Essential Rules -FullStackHero .NET 10 Starter Kit - multi-tenant SaaS framework using vertical slice architecture. +| 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 | -### Repository Structure +## Deep Dive -- **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.) +| 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) | -### Module Pattern +--- -Each module implements `IModule` with: -- `ConfigureServices(IHostApplicationBuilder)` - DI registration -- `MapEndpoints(IEndpointRouteBuilder)` - Minimal API endpoint mapping +## Quick Patterns -Feature structure within modules: -``` -Features/v1/{Feature}/ -├── {Feature}Command.cs (or Query) -├── {Feature}Handler.cs -├── {Feature}Validator.cs (FluentValidation) -└── {Feature}Endpoint.cs (static extension method on IEndpointRouteBuilder) -``` +### Command + Handler +```csharp +public sealed record CreateUserCommand(string Email, string Name) : ICommand; -Contracts projects (`Modules.{Name}.Contracts/`) contain public DTOs shareable with clients. +public sealed class CreateUserHandler(IRepository repo) + : ICommandHandler +{ + public async ValueTask Handle(CreateUserCommand cmd, CancellationToken ct) + { + var user = User.Create(cmd.Email, cmd.Name); + await repo.AddAsync(user, ct); + return user.Id; + } +} +``` -### Endpoint Pattern +### Endpoint +```csharp +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); +``` -Endpoints are static extension methods returning `RouteHandlerBuilder`: +### Validator ```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 - -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 +--- -## Configuration +## Configuration (Production) -Key settings (appsettings or env vars): -- `DatabaseOptions:Provider` - postgres or mssql -- `DatabaseOptions:ConnectionString` - Primary database -- `CachingOptions:Redis` - Redis connection -- `JwtOptions:SigningKey` - Required in production - -## Code Standards - -- .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. - -## Blazor UI Components - -The framework provides reusable Blazor components in `BuildingBlocks/Blazor.UI/Components/` with consistent styling. - -### FshPageHeader Component - -Use `FshPageHeader` for consistent page headers across Playground.Blazor: - -```razor -@using FSH.Framework.Blazor.UI.Components.Page - - - - - Action - - ``` - -**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 - - -``` - -**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 - - +DatabaseOptions:ConnectionString ← Required +CachingOptions:Redis ← Required +JwtOptions:SigningKey ← Required (256-bit) ``` - -**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); -} -``` - -Dark mode overrides are automatically applied when `.mud-theme-dark` is present. Use these tokens in custom components to ensure consistent styling.