--- name: add-feature description: Add a vertical-slice feature (command/query + handler + validator + endpoint) to an existing FSH module. Use when adding an API endpoint or business operation to a module that already exists. argument-hint: "[ModuleName] [Area] [FeatureName]" --- # Add Feature A feature is a vertical slice **split across two projects**: the request/response types live in the module's `.Contracts` project (public API); the handler, validator, and endpoint live in the runtime project. Full conventions: `.agents/rules/api-conventions.md`. ## Layout (real) ``` src/Modules/{X}/Modules.{X}.Contracts/v1/{Area}/{Feature}Command.cs # ICommand/IQuery src/Modules/{X}/Modules.{X}.Contracts/Dtos/{Entity}Dto.cs # response DTOs (if any) src/Modules/{X}/Modules.{X}/Features/v1/{Area}/{Feature}/ ├── {Feature}CommandHandler.cs # public sealed, injects the DbContext directly ├── {Feature}CommandValidator.cs # required for commands + paginated queries └── {Feature}Endpoint.cs # internal static extension ``` ## Step 1 — Command/Query (Contracts project) `Mediator` interfaces (`using Mediator;`). Records. A create command can return the raw `Guid`. ```csharp namespace FSH.Modules.{X}.Contracts.v1.{Area}; public sealed record Create{Entity}Command(string Name, decimal PriceAmount, string PriceCurrency) : ICommand; ``` Read/list DTOs go in `Modules.{X}.Contracts/Dtos/`. Paginated queries return `PagedResponse` (`FSH.Framework.Shared.Persistence`) — see `query-patterns`. ## Step 2 — Handler (runtime `Features/`) — inject the DbContext, NOT a repository There is **no generic `IRepository`**. Inject the module's `{X}DbContext`. `public sealed`, primary ctor, `ValueTask`, `.ConfigureAwait(false)`, guard first. Tenant/audit fields are auto-stamped — only inject `ICurrentUser` if you need the acting user (`GetUserId()` / `GetTenant()`). ```csharp public sealed class Create{Entity}CommandHandler(CatalogDbContext dbContext) : ICommandHandler { public async ValueTask Handle(Create{Entity}Command command, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(command); var entity = {Entity}.Create(command.Name, new Money(command.PriceAmount, command.PriceCurrency)); dbContext.{Entities}.Add(entity); await dbContext.SaveChangesAsync(cancellationToken).ConfigureAwait(false); return entity.Id; } } ``` Throw `NotFoundException` / `CustomException(msg, errors, HttpStatusCode)` (`FSH.Framework.Core.Exceptions`) — the global handler maps them to ProblemDetails. ## Step 3 — Validator (required; same folder) ```csharp public sealed class Create{Entity}CommandValidator : AbstractValidator { public Create{Entity}CommandValidator() { RuleFor(x => x.Name).NotEmpty().MaximumLength(200); RuleFor(x => x.PriceCurrency).NotEmpty().Length(3); } } ``` `Architecture.Tests` fails the build if a command/paginated-query handler has no `{Name}Validator`. ## Step 4 — Endpoint (same folder) ```csharp public static class Create{Entity}Endpoint { internal static RouteHandlerBuilder MapCreate{Entity}Endpoint(this IEndpointRouteBuilder endpoints) => endpoints.MapPost("/{entities}", async (Create{Entity}Command command, IMediator mediator, CancellationToken ct) => Results.Ok(await mediator.Send(command, ct))) .WithName("Create{Entity}") .WithSummary("Create a {entity}") .RequirePermission({X}Permissions.{Entities}.Create) .WithIdempotency(); // on replay-safe POSTs } ``` ## Step 5 — Wire it in `{X}Module.MapEndpoints` ```csharp group.MapCreate{Entity}Endpoint(); // group = endpoints.MapGroup("api/v{version:apiVersion}/{x}") … ``` ## Step 6 — Verify ```bash dotnet build src/FSH.Starter.slnx # 0 warnings (TreatWarningsAsErrors) dotnet test src/Tests/{X}.Tests # + add a handler/validator test (see testing-guide) ``` ## Checklist - [ ] Command/Query in the **Contracts** project (`using Mediator;`), DTOs in `Contracts/Dtos/` - [ ] Handler `public sealed`, injects `{X}DbContext` (no repository), `ValueTask` + `.ConfigureAwait(false)` - [ ] `{Name}Validator` exists - [ ] Endpoint `internal static …Map{Feature}Endpoint`, `.RequirePermission(...)`, `.WithName/.WithSummary` - [ ] Wired in `{X}Module.MapEndpoints` - [ ] Build 0 warnings; test added