.agents/rules/api-conventions.md
Read before adding endpoints, commands/queries, validators, or error handling.
Static extension methods on IEndpointRouteBuilder, returning RouteHandlerBuilder. The handler delegates to Mediator. Gate with .RequirePermission(...).
public static class RegisterUserEndpoint
{
internal static RouteHandlerBuilder MapRegisterUserEndpoint(this IEndpointRouteBuilder endpoints) =>
endpoints.MapPost("/register", (RegisterUserCommand command,
IMediator mediator, CancellationToken cancellationToken) =>
mediator.Send(command, cancellationToken))
.WithName("RegisterUser")
.WithSummary("Register user")
.RequirePermission(IdentityPermissionConstants.Users.Create);
}
CancellationToken to mediator.Send. ASP.NET injects it.MapEndpoints(). Endpoints group under api/v{version:apiVersion}/{module}.TypedResults / .Produces<T>(...) for accurate OpenAPI. Add .WithIdempotency() on POSTs that must be replay-safe.Modules.{Name}.Contracts — implement ICommand<TResponse> / IQuery<TResponse>. Records preferred.Modules.{Name}/Features/ — public sealed, implement ICommandHandler<T,TResponse> / IQueryHandler<T,TResponse>, return ValueTask<T>, .ConfigureAwait(false) on awaits.IPagedQuery (PageNumber, PageSize, Sort) and return PagedResponse<T>.FluentValidation, auto-registered by ModuleLoader. Name {Command}Validator. Live in the same feature folder.
Architecture.Tests (HandlerValidatorPairingTests). A handler legitimately without rules can be added to that test's known-missing allowlist, but prefer writing the validator.ValidationBehavior<,> Mediator pipeline before the handler.Throw framework exception types; the global handler converts to RFC 9457 ProblemDetails:
| Throw | HTTP |
|---|---|
NotFoundException | 404 |
ForbiddenException | 403 |
UnauthorizedException | 401 |
CustomException(msg, errors?, HttpStatusCode) | as specified (default 400) |
Don't catch broadly to swallow. Background loops may catch (Exception) to stay alive, but must log with context and exclude OperationCanceledException (filtered catch or a preceding catch (OperationCanceledException)).
Constants in Shared/Identity/*Permissions.cs (e.g. IdentityPermissionConstants). Apply with .RequirePermission(...) on the endpoint. RequiredPermissionAttribute implements IRequiredPermissionMetadata — never let a duplicate of that interface appear; it silently disables all .RequirePermission() gates.
Use Specification<T> (src/BuildingBlocks/Persistence/Specifications/) for query composition. Default AsNoTracking = true — see database.md for when tracking is required instead.
Modules.{Name}.Contracts/v1/{Area}/{Feature}/.Modules.{Name}/Features/v1/{Area}/{Feature}/.MapEndpoints().Tests/{Name}.Tests/ (+ integration test if it touches DB/IO).