con un clic
add-feature
// 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.
// 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.
Add a domain entity/aggregate with EF configuration and a migration to an existing FSH module. Use when adding a new database-backed entity. Pairs with add-feature and create-migration.
Build a capability end-to-end — backend vertical slice (Contracts→handler→validator→endpoint) AND the React page wired to it. Use when delivering a user-facing feature across API + UI. Composes add-feature + add-react-page.
Publish a cross-module integration event via the Outbox and handle it idempotently in another module. Use when one module must react to something that happened in another. See .agents/rules/eventing.md.
Create a new module (bounded context) — runtime + Contracts projects, IModule, DbContext, permissions, migrations, and the four registration sites. Use when adding a distinct business domain. For a feature in an existing module, use add-feature.
Add a new permission end-to-end — server constant + endpoint gate, and (admin app) mirror it into the permissions catalog + route guard. Use when a new endpoint needs authorization. See modules/identity.md + frontend/admin.md.
Add a list+create page to a React app (clients/admin or clients/dashboard) — API module, page, lazy route, (admin) permission gate, Playwright test. Use when adding any frontend screen. See .agents/rules/frontend/.
| 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. |
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.
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
Mediator interfaces (using Mediator;). Records. A create command can return the raw Guid.
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.
Features/) — inject the DbContext, NOT a repositoryThere 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()).
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.
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.
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
}
{X}Module.MapEndpointsgroup.MapCreate{Entity}Endpoint(); // group = endpoints.MapGroup("api/v{version:apiVersion}/{x}") …
dotnet build src/FSH.Starter.slnx # 0 warnings (TreatWarningsAsErrors)
dotnet test src/Tests/{X}.Tests # + add a handler/validator test (see testing-guide)
using Mediator;), DTOs in Contracts/Dtos/public sealed, injects {X}DbContext (no repository), ValueTask<T> + .ConfigureAwait(false){Name}Validator existsinternal static …Map{Feature}Endpoint, .RequirePermission(...), .WithName/.WithSummary{X}Module.MapEndpoints