Stats
Actions
Tags
Help us improve
Share bugs, ideas, or general feedback.
From dotnet-ai-kit
Use when building controller-based REST APIs with action results, model binding, or MediatR integration.
npx claudepluginhub faysilalshareef/dotnet-ai-kitHow this skill is triggered — by the user, by Claude, or both
Slash command
/dotnet-ai-kit:controller-patternsWhen to use
When creating or modifying controller-based API endpoints with MediatR integration
The summary Claude sees in its skill listing — used to decide when to auto-load this skill
- Use `[ApiController]` for automatic model validation and binding behavior
Guides technical evaluation of code review feedback: read fully, restate for understanding, verify against codebase, respond with reasoning or pushback before implementing.
Share bugs, ideas, or general feedback.
[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 |