skills/api/controllers/SKILL.md
Use when creating RESTful API controllers with MediatR dispatch and ProblemDetails error responses.
npx skillsauth add faysilalshareef/dotnet-ai-kit controllersInstall 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.
namespace {Company}.{Domain}.Api.Controllers;
[ApiController]
[Route("api/v1/[controller]")]
[Produces("application/json")]
public abstract class BaseController : ControllerBase
{
private ISender? _mediator;
protected ISender Mediator =>
_mediator ??= HttpContext.RequestServices.GetRequiredService<ISender>();
}
namespace {Company}.{Domain}.Api.Controllers;
[Authorize]
public sealed class OrdersController : BaseController
{
/// <summary>Get paginated orders.</summary>
[HttpGet]
[ProducesResponseType(typeof(PaginatedList<OrderOutput>), 200)]
public async Task<IActionResult> GetAll(
[FromQuery] int page = 1,
[FromQuery] int pageSize = 20,
[FromQuery] string? search = null,
CancellationToken ct = default)
{
var query = new GetOrdersQuery(page, pageSize, search);
var result = await Mediator.Send(query, ct);
// C-Q4 fix: Problem(string? detail, ...) overload prefers explicit
// named arguments so the status code is unambiguous (otherwise the
// detail is interpreted as a path/URI). Status 400 is the canonical
// mapping for a failed Result<T> in this skill.
return result.IsSuccess
? Ok(result.Value)
: Problem(detail: result.Error, statusCode: StatusCodes.Status400BadRequest);
}
/// <summary>Get order by ID.</summary>
[HttpGet("{id:guid}")]
[ProducesResponseType(typeof(OrderOutput), 200)]
[ProducesResponseType(404)]
public async Task<IActionResult> GetById(Guid id, CancellationToken ct)
{
var result = await Mediator.Send(new GetOrderByIdQuery(id), ct);
return result.IsSuccess
? Ok(result.Value)
: NotFound();
}
/// <summary>Create a new order.</summary>
[HttpPost]
[ProducesResponseType(typeof(Guid), 201)]
[ProducesResponseType(typeof(ValidationProblemDetails), 400)]
public async Task<IActionResult> Create(
[FromBody] CreateOrderCommand command, CancellationToken ct)
{
var result = await Mediator.Send(command, ct);
return result.IsSuccess
? CreatedAtAction(nameof(GetById), new { id = result.Value }, result.Value)
: BadRequest(result.Error);
}
/// <summary>Update an existing order.</summary>
[HttpPut("{id:guid}")]
[ProducesResponseType(204)]
[ProducesResponseType(404)]
public async Task<IActionResult> Update(
Guid id, [FromBody] UpdateOrderCommand command, CancellationToken ct)
{
if (id != command.Id)
return BadRequest("Route ID does not match body ID.");
var result = await Mediator.Send(command, ct);
return result.IsSuccess ? NoContent() : NotFound();
}
/// <summary>Delete an order.</summary>
[HttpDelete("{id:guid}")]
[ProducesResponseType(204)]
[ProducesResponseType(404)]
public async Task<IActionResult> Delete(Guid id, CancellationToken ct)
{
var result = await Mediator.Send(new DeleteOrderCommand(id), ct);
return result.IsSuccess ? NoContent() : NotFound();
}
}
namespace {Company}.{Domain}.Api.Middleware;
public sealed class GlobalExceptionHandler(ILogger<GlobalExceptionHandler> logger)
: IExceptionHandler
{
public async ValueTask<bool> TryHandleAsync(
HttpContext context, Exception exception, CancellationToken ct)
{
logger.LogError(exception, "Unhandled exception");
var problem = exception switch
{
ValidationException ve => new ProblemDetails
{
Status = 400, Title = "Validation Failed",
Detail = string.Join("; ", ve.Errors.Select(e => e.ErrorMessage))
},
NotFoundException => new ProblemDetails
{
Status = 404, Title = "Not Found"
},
_ => new ProblemDetails
{
Status = 500, Title = "Internal Server Error"
}
};
context.Response.StatusCode = problem.Status!.Value;
await context.Response.WriteAsJsonAsync(problem, ct);
return true;
}
}
// Program.cs
builder.Services.AddControllers();
builder.Services.AddExceptionHandler<GlobalExceptionHandler>();
builder.Services.AddProblemDetails();
var app = builder.Build();
app.UseExceptionHandler();
app.MapControllers();
| Anti-Pattern | Correct Approach | |---|---| | Business logic in controllers | Delegate to MediatR handlers | | Returning domain entities directly | Map to output DTOs | | Manual model validation | Use FluentValidation + pipeline behavior | | Catching exceptions per action | Use global exception handler |
grep -r "ControllerBase\|ApiController" --include="*.cs"
grep -r "IMediator\|ISender" --include="*.cs"
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.