CtrlK
BlogDocsLog inGet started
Tessl Logo

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.

74

Quality

93%

Does it follow best practices?

Run evals on this skill

Adds up to 20 points to the overall score

View guide

SecuritybySnyk

Passed

No findings from the security scan

SKILL.md
Quality
Evals
Security

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.

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()).

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)

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)

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

group.MapCreate{Entity}Endpoint();   // group = endpoints.MapGroup("api/v{version:apiVersion}/{x}") …

Step 6 — Verify

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
Repository
fullstackhero/dotnet-starter-kit
Last updated
First committed

Is this your skill?

If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.