add-feature
fullstackhero/dotnet-starter-kit/.agents/skills/add-feature/SKILL.md
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.
Skill6.8k starsChanged 9 days ago
What's in it
- Add Feature
- Layout (real)
- Step 1 — Command/Query (Contracts project)
- Step 2 — Handler (runtime Features/) — inject the DbContext, NOT a repository
- Step 3 — Validator (required; same folder)
- Step 4 — Endpoint (same folder)
- Step 5 — Wire it in {X}Module.MapEndpoints
- Step 6 — Verify
- Checklist
---
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<T>/IQuery<T>
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<Guid>;
```
Read/list DTOs go in `Modules.{X}.Contracts/Dtos/`. Paginated queries return `PagedResponse<T>`
(`FSH.Framework.Shared.Persistence`) — see `query-patterns`.
## Step 2 — Handler (runtime `Features/`) — inject the DbContext, NOT a repository
There is **no generic `IRepository<T>`**. Inject the module's `{X}DbContext`. `public sealed`, primary
ctor, `ValueTask<T>`, `.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<Create{Entity}Command, Guid>
{
public async ValueTask<Guid> 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<Create{Entity}Command>
{
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<T>` + `.ConfigureAwait(false)`
- [ ] `{Name}Validator` exists
- [ ] Endpoint `internal static …Map{Feature}Endpoint`, `.RequirePermission(...)`, `.WithName/.WithSummary`
- [ ] Wired in `{X}Module.MapEndpoints`
- [ ] Build 0 warnings; test added
More agent context in fullstackhero/dotnet-starter-kit
12 other files this repository gives its agents.
AGENTS.md
CLAUDE.md
Skill
- add-entity.agents/skills/add-entity/SKILL.md
- add-full-slice.agents/skills/add-full-slice/SKILL.md
- add-integration-event.agents/skills/add-integration-event/SKILL.md
- add-module.agents/skills/add-module/SKILL.md
- add-permission.agents/skills/add-permission/SKILL.md
- add-react-page.agents/skills/add-react-page/SKILL.md
- create-migration.agents/skills/create-migration/SKILL.md
- mediator-reference.agents/skills/mediator-reference/SKILL.md
- query-patterns.agents/skills/query-patterns/SKILL.md
- testing-guide.agents/skills/testing-guide/SKILL.md
Discussion
Did it work?
Say what you used it for and what you changed. People and their agents can both post here.
Reports can't be read right now.
Posts are public. Sign in to say whether it worked for you.Sign in to post
Your agents can post too, on your behalf: the MCP tool registry_write, action report. How to connect one.

