Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Build or maintain controller-based ASP.NET Core APIs when the project needs controller conventions, advanced model binding, validation extensions, OData, JsonPatch, or existing API patterns. USE FOR: working on controller-based APIs in ASP.NET Core; needing controller-specific extensibility or conventions; migrating or reviewing existing API controllers and filters. DO NOT USE FOR: unrelated stacks; generic tasks that do not need this specific guidance. INVOKES: inspect the repository context, e
.claude/skills/managedcode-web-api/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | 71% | 0% |
| case-02 | ✗→✓ | ▲ Improved | 38% | 0% |
| case-07 | ✗→✓ | ▲ Improved | 11% | 0% |
| case-09 | ✗→✓ | ▲ Improved | 39% | 0% |
| case-14 | ✗→✓ | ▲ Improved | 46% | 0% |
minimal-apis for new simple APIs instead of defaulting to controllers out of habit.dotnet/aspnetcore v10.0.11 is servicing and updates the OpenAPI stack to 2.7.5; controller-based API guidance still depends on conventions, advanced model binding, OData, JsonPatch, and existing filters. Re-run generated-document and client contract checks after upgrading.aspnetcore-10.0 overview keeps controller APIs alongside Minimal APIs rather than replacing them. Use the dedicated routing, OpenAPI, auth, and hosting pages before changing public API contracts.Use primary constructors (C# 12+) for dependency injection and keep controllers focused on HTTP concerns:
csharp[ApiController] [Route("api/[controller]")] public class OrdersController( IOrderService orderService, ILogger<OrdersController> logger) : ControllerBase { [HttpGet("{id:guid}")] [ProducesResponseType<OrderDto>(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task<IActionResult> GetById(Guid id, CancellationToken ct) { var order = await orderService.GetByIdAsync(id, ct); return order is null ? NotFound() : Ok(order); } [HttpPost] [ProducesResponseType<OrderDto>(StatusCodes.Status201Created)] [ProducesResponseType<ValidationProblemDetails>(StatusCodes.Status400BadRequest)] public async Task<IActionResult> Create(CreateOrderRequest request, CancellationToken ct) { var order = await orderService.CreateAsync(request, ct); return CreatedAtAction(nameof(GetById), new { id = order.Id }, order); } }
Explicitly declare binding sources for clarity:
csharp[HttpGet("{id:guid}")] public async Task<IActionResult> GetWithOptions( [FromRoute] Guid id, [FromQuery] bool includeDeleted = false, [FromHeader(Name = "X-Correlation-Id")] string? correlationId = null, CancellationToken ct = default) { // Route: id, Query: includeDeleted, Header: X-Correlation-Id }
Use record types with required members for request DTOs:
csharppublic record CreateProductRequest { public required string Name { get; init; } public required decimal Price { get; init; } public string? Description { get; init; } public IReadOnlyList<string> Tags { get; init; } = []; }
Prefer FluentValidation for complex validation rules:
csharppublic class CreateOrderRequestValidator : AbstractValidator<CreateOrderRequest> { public CreateOrderRequestValidator(IProductRepository products) { RuleFor(x => x.CustomerId) .NotEmpty() .WithMessage("Customer ID is required"); RuleFor(x => x.Items) .NotEmpty() .WithMessage("Order must contain at least one item"); RuleForEach(x => x.Items).ChildRules(item => { item.RuleFor(i => i.ProductId) .NotEmpty() .MustAsync(async (id, ct) => await products.ExistsAsync(id, ct)) .WithMessage("Product does not exist"); item.RuleFor(i => i.Quantity) .GreaterThan(0) .LessThanOrEqualTo(100); }); } }
Configure consistent Problem Details responses:
csharpbuilder.Services.Configure<ApiBehaviorOptions>(options => { options.InvalidModelStateResponseFactory = context => { var problemDetails = new ValidationProblemDetails(context.ModelState) { Type = "https://tools.ietf.org/html/rfc7231#section-6.5.1", Title = "One or more validation errors occurred.", Status = StatusCodes.Status400BadRequest, Instance = context.HttpContext.Request.Path }; return new BadRequestObjectResult(problemDetails); }; });
Configure URL path versioning:
csharpbuilder.Services.AddApiVersioning(options => { options.DefaultApiVersion = new ApiVersion(1, 0); options.AssumeDefaultVersionWhenUnspecified = true; options.ReportApiVersions = true; options.ApiVersionReader = new UrlSegmentApiVersionReader(); }) .AddApiExplorer(options => { options.GroupNameFormat = "'v'VVV"; options.SubstituteApiVersionInUrl = true; }); [ApiController] [Route("api/v{version:apiVersion}/products")] [ApiVersion("1.0")] public class ProductsV1Controller(IProductService productService) : ControllerBase { [HttpGet("{id}")] public async Task<IActionResult> Get(int id, CancellationToken ct) { var product = await productService.GetAsync(id, ct); return Ok(product); } }
Use global exception handlers for consistent error responses:
csharppublic class GlobalExceptionHandler( ILogger<GlobalExceptionHandler> logger) : IExceptionHandler { public async ValueTask<bool> TryHandleAsync( HttpContext httpContext, Exception exception, CancellationToken cancellationToken) { logger.LogError(exception, "Unhandled exception occurred"); var problemDetails = exception switch { ValidationException validationEx => new ProblemDetails { Status = StatusCodes.Status400BadRequest, Title = "Validation Error", Detail = validationEx.Message }, NotFoundException notFoundEx => new ProblemDetails { Status = StatusCodes.Status404NotFound, Title = "Resource Not Found", Detail = notFoundEx.Message }, _ => new ProblemDetails { Status = StatusCodes.Status500InternalServerError, Title = "Internal Server Error" } }; problemDetails.Extensions["traceId"] = httpContext.TraceIdentifier; httpContext.Response.StatusCode = problemDetails.Status ?? 500; await httpContext.Response.WriteAsJsonAsync(problemDetails, cancellationToken); return true; } }
Other measured skills in the registry, with their headline benchmark lift.