$npx -y skills add managedcode/dotnet-skills --skill web-apiBuild 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 ex
| 1 | # ASP.NET Core Web API |
| 2 | |
| 3 | ## Trigger On |
| 4 | |
| 5 | - working on controller-based APIs in ASP.NET Core |
| 6 | - needing controller-specific extensibility or conventions |
| 7 | - migrating or reviewing existing API controllers and filters |
| 8 | |
| 9 | ## Workflow |
| 10 | |
| 11 | 1. Use controllers when the API needs controller-centric features, not simply because older templates did so. |
| 12 | 2. Keep controllers thin: map HTTP concerns to application services or handlers, and avoid embedding data access and business rules directly in actions. |
| 13 | 3. Use clear DTO boundaries, explicit validation, and predictable HTTP status behavior. |
| 14 | 4. Review authentication and authorization at both controller and endpoint levels so the API surface is not accidentally inconsistent. |
| 15 | 5. Keep OpenAPI generation, versioning, and error contract behavior deliberate rather than incidental. |
| 16 | 6. Use `minimal-apis` for new simple APIs instead of defaulting to controllers out of habit. |
| 17 | |
| 18 | ## Current Upstream Notes |
| 19 | |
| 20 | - `dotnet/aspnetcore` `v10.0.10` is servicing. It fixes nullable `DescriptionAttribute` handling and duplicate generic-property/reference IDs in OpenAPI generation; controller-based API guidance still depends on conventions, advanced model binding, OData, JsonPatch, and existing filters. |
| 21 | - The July 2026 `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. |
| 22 | |
| 23 | ## Deliver |
| 24 | |
| 25 | - controller APIs with explicit contracts and policies |
| 26 | - reduced controller bloat |
| 27 | - tests or smoke checks for critical API behavior |
| 28 | |
| 29 | ## Validate |
| 30 | |
| 31 | - controller features are actually justified |
| 32 | - actions do not hide business logic and persistence details |
| 33 | - HTTP semantics stay predictable across endpoints |
| 34 | |
| 35 | ## Controller Structure |
| 36 | |
| 37 | Use primary constructors (C# 12+) for dependency injection and keep controllers focused on HTTP concerns: |
| 38 | |
| 39 | ```csharp |
| 40 | [ApiController] |
| 41 | [Route("api/[controller]")] |
| 42 | public class OrdersController( |
| 43 | IOrderService orderService, |
| 44 | ILogger<OrdersController> logger) : ControllerBase |
| 45 | { |
| 46 | [HttpGet("{id:guid}")] |
| 47 | [ProducesResponseType<OrderDto>(StatusCodes.Status200OK)] |
| 48 | [ProducesResponseType(StatusCodes.Status404NotFound)] |
| 49 | public async Task<IActionResult> GetById(Guid id, CancellationToken ct) |
| 50 | { |
| 51 | var order = await orderService.GetByIdAsync(id, ct); |
| 52 | return order is null ? NotFound() : Ok(order); |
| 53 | } |
| 54 | |
| 55 | [HttpPost] |
| 56 | [ProducesResponseType<OrderDto>(StatusCodes.Status201Created)] |
| 57 | [ProducesResponseType<ValidationProblemDetails>(StatusCodes.Status400BadRequest)] |
| 58 | public async Task<IActionResult> Create(CreateOrderRequest request, CancellationToken ct) |
| 59 | { |
| 60 | var order = await orderService.CreateAsync(request, ct); |
| 61 | return CreatedAtAction(nameof(GetById), new { id = order.Id }, order); |
| 62 | } |
| 63 | } |
| 64 | ``` |
| 65 | |
| 66 | ## Model Binding |
| 67 | |
| 68 | Explicitly declare binding sources for clarity: |
| 69 | |
| 70 | ```csharp |
| 71 | [HttpGet("{id:guid}")] |
| 72 | public async Task<IActionResult> GetWithOptions( |
| 73 | [FromRoute] Guid id, |
| 74 | [FromQuery] bool includeDeleted = false, |
| 75 | [FromHeader(Name = "X-Correlation-Id")] string? correlationId = null, |
| 76 | CancellationToken ct = default) |
| 77 | { |
| 78 | // Route: id, Query: includeDeleted, Header: X-Correlation-Id |
| 79 | } |
| 80 | ``` |
| 81 | |
| 82 | Use record types with required members for request DTOs: |
| 83 | |
| 84 | ```csharp |
| 85 | public record CreateProductRequest |
| 86 | { |
| 87 | public required string Name { get; init; } |
| 88 | public required decimal Price { get; init; } |
| 89 | public string? Description { get; init; } |
| 90 | public IReadOnlyList<string> Tags { get; init; } = []; |
| 91 | } |
| 92 | ``` |
| 93 | |
| 94 | ## Validation |
| 95 | |
| 96 | Prefer FluentValidation for complex validation rules: |
| 97 | |
| 98 | ```csharp |
| 99 | public class CreateOrderRequestValidator : AbstractValidator<CreateOrderRequest> |
| 100 | { |
| 101 | public CreateOrderRequestValidator(IProductRepository products) |
| 102 | { |
| 103 | RuleFor(x => x.CustomerId) |
| 104 | .NotEmpty() |
| 105 | .WithMessage("Customer ID is required"); |
| 106 | |
| 107 | RuleFor(x => x.Items) |
| 108 | .NotEmpty() |
| 109 | .WithMessage("Order must contain at least one item"); |
| 110 | |
| 111 | RuleForEach(x => x.Items).ChildRules(item => |
| 112 | { |
| 113 | item.RuleFor(i => i.ProductId) |
| 114 | .NotEmpty() |
| 115 | .MustAsync(async (id, ct) => await products.ExistsAsync(id, ct)) |
| 116 | .WithMessage |