[HttpGet] and Friends

Bind controller actions to HTTP methods with [HttpGet], [HttpPost], [HttpPut], [HttpPatch], [HttpDelete] and [AcceptVerbs], and get 405s, HEAD and names right.

Signature
[HttpGet(string template, Name = "...", Order = 0)]  |  [AcceptVerbs("GET", "HEAD", Route = "...")]
  • Added in .NET ASP.NET Core 1.0

Parameters

ParameterTypeDescription
$templateoptionalstring?= nullRoute template appended to the controller prefix; a leading "/" ignores the prefix. Without it the action uses the controller route as is.
$Nameoptionalstring?= nullRoute name for link generation; unique across the application.
$Orderoptionalint= 0Tie-breaker between endpoints that match the same request; lower first.
$methods (AcceptVerbs only)params string[]HTTP methods the action accepts, for example "GET", "HEAD". AcceptVerbs takes its template from the Route property.

Return value

n/aRestricts the action to one HTTP method (or the listed methods for AcceptVerbs) and optionally adds a route template that is appended to the controller's [Route] prefix.

Errors

Two actions with the same method and an equally specific template throw AmbiguousMatchException at request time (HTTP 500). Duplicate route names fail at startup.

Changelog

  • PHP ASP.NET Core 3.0Method matching moved into endpoint routing: a request whose path matches but whose method does not gets 405 Method Not Allowed with an Allow header.

The HTTP method attributes all derive from HttpMethodAttribute. Each one does two jobs: it adds HTTP method metadata (IActionHttpMethodProvider) that endpoint routing matches on, and, when you pass a template, it adds a route like [Route] does. [HttpGet("{id:int}")] on an action in a [Route("api/orders")] controller is the endpoint GET api/orders/{id:int}.

Choosing the verb

AttributeTypical actionSafe / idempotentUsual success
[HttpGet]Read one or manyyes / yes200
[HttpPost]Create, or start a commandno / no201 with Location, or 202
[HttpPut]Replace the whole resourceno / yes200 or 204
[HttpPatch]Partial updateno / not necessarily200 or 204
[HttpDelete]Removeno / yes204
[AcceptVerbs("GET", "HEAD")]Several methods on one actiondepends

Idempotency matters because proxies, HTTP clients and resilience handlers retry. A retried PUT is harmless; a retried POST creates a second order unless you use an idempotency key.

Defaults worth knowing

  • When the path matches but the method does not, routing returns 405 with an Allow header listing what would have worked, not 404.
  • HEAD is not mapped to GET automatically: a GET-only action answers HEAD with 405. Use [AcceptVerbs("GET", "HEAD")] or [HttpHead] where monitors or CDNs send HEAD.
  • CORS preflight OPTIONS requests are answered by the CORS middleware; do not write [HttpOptions] actions for them.
  • An action with no method attribute at all, but a route from the class, accepts every method.

What fails in production

Two actions with the same verb and equivalent templates compile fine and start fine, then every matching request throws AmbiguousMatchException: a 500 that appears only on that URL. It usually comes from copy-pasting an action or overloading a method name. Integration tests that call every route catch it; so does a constraint that separates the templates. Overloads are not a dispatch mechanism in MVC, so give each action a distinct template.

Example

Try it yourself
Loading editor…
Press Run to execute the code.

When to use it

  • Answer HEAD /api/inventory/{sku}/stock with 200 or 404 so a CDN health probe can check existence without downloading a body
  • Use PUT for setting a product's absolute stock level so a retried request cannot double-count
  • Return 201 Created with a Location header built from the GetInvoice route name after POST /api/invoices

Frequently asked questions

Why do I get 405 instead of 404?

The path matched an endpoint but the HTTP method did not. Routing answers 405 Method Not Allowed and lists the accepted methods in the Allow header. A 404 means no template matched the path at all.

Does [HttpGet] also handle HEAD requests?

No. Endpoint routing matches the method exactly, so HEAD on a GET-only action returns 405. Use [AcceptVerbs("GET", "HEAD")] or add [HttpHead]; ASP.NET Core does not send a body for HEAD responses.

Should PATCH use JSON Patch?

Either works. A small partial-update record is simpler for most APIs; use JSON Patch (Microsoft.AspNetCore.JsonPatch.SystemTextJson) when clients need operations such as add, remove and test on arbitrary paths.

More examples

POST that returns 201 with a Location header

<code>Name = "GetInvoice"</code> on the GET action lets the POST build its <code>Location</code> header through link generation, so the URL stays correct when the route template changes. <code>CreatedAtAction(nameof(Get), ...)</code> does the same without a name when both actions are in one controller.

Example · csharp
using Microsoft.AspNetCore.Mvc;

namespace Shop.Api;

public record CreateInvoice(string CustomerId, decimal Amount, string Currency);
public record Invoice(int Id, string CustomerId, decimal Amount, string Currency);

[ApiController]
[Route("api/invoices")]
public class InvoicesController : ControllerBase
{
    private static int _nextId = 5000;

    [HttpGet("{id:int}", Name = "GetInvoice")]
    public ActionResult<Invoice> Get(int id) => new Invoice(id, "CUST-1042", 249.00m, "EUR");

    [HttpPost]
    public ActionResult<Invoice> Create(CreateInvoice request)
    {
        var invoice = new Invoice(Interlocked.Increment(ref _nextId), request.CustomerId, request.Amount, request.Currency);
        // Location: /api/invoices/5001, built from the route name, never by hand.
        return CreatedAtRoute("GetInvoice", new { id = invoice.Id }, invoice);
    }
}

The copy-paste AmbiguousMatchException

Parameter names do not distinguish templates: <code>{id:int}</code> and <code>{customerId:int}</code> are the same shape, so both actions match and routing throws <code>AmbiguousMatchException</code>. The app starts normally, which is why only a test that calls the URL finds it before users do.

Example · csharp
using Microsoft.AspNetCore.Mvc;

namespace Shop.Api;

[ApiController]
[Route("api/customers")]
public class CustomersController : ControllerBase
{
    [HttpGet("{id:int}")]
    public IActionResult Get(int id) => Ok(new { id });

    // Before (500 on every GET /api/customers/{n}):
    // [HttpGet("{customerId:int}")]
    // public IActionResult GetWithOrders(int customerId) => ...

    // After: a distinct template, or a query option on the original action.
    [HttpGet("{id:int}/with-orders")]
    public IActionResult GetWithOrders(int id) => Ok(new { id, orders = Array.Empty<object>() });
}

Exercising every verb from an .http file

An <code>.http</code> file next to the project (Visual Studio, Rider and VS Code REST Client all run it) documents each verb the controller accepts. Requests are separated by <code>###</code>, and a JSON body needs the <code>Content-Type</code> header or <code>[ApiController]</code> answers 415.

Example · http
@api = http://localhost:5080

GET {{api}}/api/inventory/KETTLE-BLUE

###
HEAD {{api}}/api/inventory/KETTLE-BLUE/stock

###
PUT {{api}}/api/inventory/KETTLE-BLUE
Content-Type: application/json

{ "quantity": 20 }

###
PATCH {{api}}/api/inventory/KETTLE-BLUE
Content-Type: application/json

{ "delta": -3 }

###
DELETE {{api}}/api/inventory/MUG-WHITE

Discussion

  • Be the first to comment on this lesson.