skills/api/controller-patterns/SKILL.md
Use when building controller-based REST APIs with action results, model binding, or MediatR integration.
npx skillsauth add faysilalshareef/dotnet-ai-kit controller-patternsInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
[ApiController] for automatic model validation and binding behaviorActionResult<T> with explicit [ProducesResponseType] attributesCancellationToken from every action method[ApiController]
[Route("api/[controller]")]
[Produces("application/json")]
public sealed class OrdersController(ISender sender) : ControllerBase
{
[HttpGet]
[ProducesResponseType(typeof(PagedList<OrderResponse>),
StatusCodes.Status200OK)]
public async Task<ActionResult<PagedList<OrderResponse>>> GetOrders(
[FromQuery] OrderFilter filter, CancellationToken ct)
{
var result = await sender.Send(new ListOrdersQuery(filter), ct);
return Ok(result);
}
[HttpGet("{id:guid}")]
[ProducesResponseType(typeof(OrderResponse), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<OrderResponse>> GetOrder(
Guid id, CancellationToken ct)
{
var result = await sender.Send(new GetOrderQuery(id), ct);
return result is not null ? Ok(result) : NotFound();
}
[HttpPost]
[ProducesResponseType(typeof(OrderResponse),
StatusCodes.Status201Created)]
[ProducesResponseType(typeof(ProblemDetails),
StatusCodes.Status400BadRequest)]
public async Task<ActionResult<OrderResponse>> CreateOrder(
CreateOrderRequest request, CancellationToken ct)
{
var result = await sender.Send(
new CreateOrderCommand(request.CustomerName, request.Items), ct);
return CreatedAtAction(
nameof(GetOrder), new { id = result.Id }, result);
}
[HttpPut("{id:guid}")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<IActionResult> UpdateOrder(
Guid id, UpdateOrderRequest request, CancellationToken ct)
{
var result = await sender.Send(
new UpdateOrderCommand(id, request.CustomerName), ct);
return result.Match<IActionResult>(
_ => NoContent(),
error => error.Type == ErrorType.NotFound
? NotFound() : BadRequest(error.ToProblemDetails()));
}
[HttpDelete("{id:guid}")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<IActionResult> DeleteOrder(
Guid id, CancellationToken ct)
{
var result = await sender.Send(new DeleteOrderCommand(id), ct);
return result.IsSuccess ? NoContent() : NotFound();
}
}
[ApiController]
[Route("api/orders/{orderId:guid}/items")]
[Produces("application/json")]
public sealed class OrderItemsController(ISender sender) : ControllerBase
{
[HttpGet]
public async Task<ActionResult<List<OrderItemResponse>>> GetItems(
Guid orderId, CancellationToken ct)
{
var result = await sender.Send(
new ListOrderItemsQuery(orderId), ct);
return Ok(result);
}
[HttpPost]
[ProducesResponseType(StatusCodes.Status201Created)]
public async Task<IActionResult> AddItem(
Guid orderId, AddOrderItemRequest request, CancellationToken ct)
{
var result = await sender.Send(
new AddOrderItemCommand(orderId, request.ProductId,
request.Quantity), ct);
return CreatedAtAction(nameof(GetItems), new { orderId }, result);
}
}
// Program.cs
builder.Services.AddControllers()
.AddJsonOptions(options =>
{
options.JsonSerializerOptions.Converters.Add(
new JsonStringEnumConverter());
options.JsonSerializerOptions.DefaultIgnoreCondition =
JsonIgnoreCondition.WhenWritingNull;
});
var app = builder.Build();
app.MapControllers();
builder.Services.AddProblemDetails(options =>
{
options.CustomizeProblemDetails = context =>
{
context.ProblemDetails.Instance =
context.HttpContext.Request.Path;
context.ProblemDetails.Extensions["traceId"] =
Activity.Current?.Id ??
context.HttpContext.TraceIdentifier;
};
});
// FromQuery — query string parameters
public async Task<IActionResult> Search(
[FromQuery] string? name,
[FromQuery] int page = 1) { }
// FromRoute — URL route parameters
[HttpGet("{id:guid}")]
public async Task<IActionResult> Get(
[FromRoute] Guid id) { }
// FromBody — request body (default for complex types with [ApiController])
[HttpPost]
public async Task<IActionResult> Create(
CreateOrderRequest request) { }
// FromHeader — custom headers
public async Task<IActionResult> Process(
[FromHeader(Name = "X-Correlation-Id")] string? correlationId) { }
CancellationToken parameter[ProducesResponseType] attributesISender/IMediator: ControllerBase or : Controller class inheritance[ApiController] attribute on classesControllers/ folderservices.AddControllers() in Program.cs[ProducesResponseType] attributes on actions[ApiController] to all API controllers for automatic validation[ProducesResponseType] to document response types for OpenAPICancellationToken parameter to all async action methodsCreatedAtAction for POST endpoints returning 201| Scenario | Recommendation |
|----------|---------------|
| Complex model binding | Controllers handle this well |
| Need attribute routing | Controllers with [Route] |
| Rapid prototyping | Minimal API may be faster |
| Legacy migration | Keep controllers, modernize patterns |
| OpenAPI generation | Both work, controllers have richer attributes |
data-ai
Use when about to claim work is complete, fixed, passing, or ready — before committing, creating PRs, or moving to the next task. Requires running verification commands and confirming output before making any success claims.
development
Use when encountering any bug, test failure, build error, or unexpected behavior — before proposing fixes or making changes.
development
Use when checkpointing, wrapping up, or handing off an AI-assisted development session.
development
Use when following the Specification-Driven Development lifecycle from plan through ship.