ASP.NET Web API (.NET Framework) · REST Foundations

HTTP Methods

HTTP methods are the verbs of your API. Choosing the right one tells clients, caches and proxies exactly what a request will do.

Beginner 6 min Lesson 3 of 36Covers ASP.NET Web API 2.2 on .NET Framework 4.8.
Before you start:REST Principles
0/36 in ASP.NET Web API
On this page · 6 min · Beginner

Learning objectives#

  • Use GET, POST, PUT, PATCH and DELETE correctly
  • Explain safe and idempotent methods
  • Map methods to actions with [HttpGet], [HttpPost] and friends
  • Implement a partial update with PATCH

Explanation#

GET reads data and must never change it. POST creates a new resource (or triggers an action). PUT replaces a whole resource. PATCH changes only some fields. DELETE removes a resource.

Safe methods do not change anything (GET, HEAD). Idempotent methods give the same result if repeated: GET, PUT and DELETE are idempotent; POST is not — sending it twice creates two orders.

Web API 2 picks the HTTP method from attributes ([HttpGet], [HttpPost], [HttpPut], [HttpPatch], [HttpDelete]) or, without attributes, from the method name prefix (GetProduct, PostProduct).

PUT expects the complete object; missing fields become empty. PATCH expects only the fields to change. Web API 2 has no built-in JSON Patch, so a simple PATCH takes a DTO whose properties are nullable.

Successful DELETE and PUT commonly return 204 No Content; POST returns 201 Created.

Syntax#

Text Illustrative
Method   Purpose            Safe   Idempotent   Typical success
GET      read               yes    yes          200 OK
POST     create             no     no           201 Created
PUT      replace whole      no     yes          204 No Content / 200
PATCH    update part        no     no*          200 OK
DELETE   remove             no     yes          204 No Content

Code example: All five verbs on one resource#

C#ProductsController.cs Illustrative
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Web.Http;
using ShopApi.Models;


public class ProductPatch
{
    public string Name { get; set; }       // null = "do not change"
    public decimal? Price { get; set; }
}


[RoutePrefix("api/products")]
public class ProductsController : ApiController
{
    private static readonly Dictionary<int, Product> Store = new Dictionary<int, Product>
    {
        [1] = new Product { Id = 1, Name = "Pen", Price = 20m },
    };


    [HttpGet, Route("{id:int}", Name = "GetProduct")]
    public IHttpActionResult Get(int id)
    {
        Product product;
        if (!Store.TryGetValue(id, out product)) return NotFound();
        return Ok(product);
    }


    [HttpPost, Route("")]
    public IHttpActionResult Post(Product product)
    {
        product.Id = Store.Keys.Max() + 1;
        Store[product.Id] = product;
        return CreatedAtRoute("GetProduct", new { id = product.Id }, product);
    }


    [HttpPut, Route("{id:int}")]
    public IHttpActionResult Put(int id, Product product)
    {
        if (!Store.ContainsKey(id)) return NotFound();
        product.Id = id;
        Store[id] = product;                         // replaces everything
        return StatusCode(HttpStatusCode.NoContent);
    }


    [HttpPatch, Route("{id:int}")]
    public IHttpActionResult Patch(int id, ProductPatch changes)
    {
        Product product;
        if (!Store.TryGetValue(id, out product)) return NotFound();
        if (changes.Name != null) product.Name = changes.Name;
        if (changes.Price.HasValue) product.Price = changes.Price.Value;
        return Ok(product);
    }


    [HttpDelete, Route("{id:int}")]
    public IHttpActionResult Delete(int id)
    {
        if (!Store.Remove(id)) return NotFound();
        return StatusCode(HttpStatusCode.NoContent);
    }
}
Expected output
POST   /api/products {"Name":"Bag","Price":650}  → 201 Created, Location: /api/products/2
PATCH  /api/products/2 {"Price":599}             → 200 {"Id":2,"Name":"Bag","Price":599.0,...}
PUT    /api/products/2 {"Name":"Big Bag"}         → 204 (Price is now 0 — PUT replaces the whole product)
DELETE /api/products/2                            → 204
DELETE /api/products/2                            → 404 (already gone)

This example is illustrative. Copy or download it and run it in your own environment; the expected output is shown above.

Step-by-step explanation#

  1. Each action declares its verb and route with attributes.
  2. POST assigns a new id and answers 201 with a link built from the route named "GetProduct".
  3. PUT replaces the stored product, so any field the client leaves out is lost.
  4. PATCH only touches the fields that were sent (non-null).
  5. DELETE returns 204 the first time and 404 afterwards — the resource state is the same either way, which is what idempotent means.

Real-world use case#

A mobile app retries requests when the network drops. Because PUT and DELETE are idempotent, retries are safe; for POST /api/orders the API accepts an Idempotency-Key header so a retried order is not created twice.

Common mistakes#

Changing data in a GET

GET /api/orders/5/cancel can be triggered by crawlers, link previews or prefetching. Use POST or DELETE for changes.

Using PUT for partial updates

Sending only {"Price": 599} with PUT wipes the other fields. Use PATCH for partial changes.

Two actions with the same verb and route

Web API throws “Multiple actions were found that match the request”. Give each a distinct route or verb.

Best practices#

  • GET never changes state
  • POST → 201 with Location; DELETE → 204
  • Use nullable DTO properties for PATCH
  • Make POST safe to retry with idempotency keys where money is involved

Remember this#

GET read · POST create · PUT replace · PATCH change part · DELETE remove.

Idempotent = same result if repeated (GET, PUT, DELETE).

PUT without a field = that field is wiped.

Practice exercise#

Your task: Write a TodosController with GET api/todos, POST api/todos (201) and DELETE api/todos/{id} (204 or 404), storing todos in a static Dictionary<int, string>.

Need a hint?
Use CreatedAtRoute with a named GET route, and Dictionary.Remove to know whether the item existed.
Show solution
C# Illustrative
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Web.Http;


[RoutePrefix("api/todos")]
public class TodosController : ApiController
{
    private static readonly Dictionary<int, string> Todos = new Dictionary<int, string> { [1] = "Learn REST" };


    [HttpGet, Route("")]
    public IHttpActionResult GetAll() => Ok(Todos);


    [HttpGet, Route("{id:int}", Name = "GetTodo")]
    public IHttpActionResult Get(int id)
    {
        string text;
        return Todos.TryGetValue(id, out text) ? (IHttpActionResult)Ok(text) : NotFound();
    }


    [HttpPost, Route("")]
    public IHttpActionResult Post([FromBody] string text)
    {
        int id = Todos.Keys.DefaultIfEmpty(0).Max() + 1;
        Todos[id] = text;
        return CreatedAtRoute("GetTodo", new { id }, text);
    }


    [HttpDelete, Route("{id:int}")]
    public IHttpActionResult Delete(int id) =>
        Todos.Remove(id) ? (IHttpActionResult)StatusCode(HttpStatusCode.NoContent) : NotFound();
}

Mini quiz#

HTTP Methods quizMultiple choice Question 1 of 3

Which method should create a new order?

Score so far: 0/0

Want more? Practise all ASP.NET Web API MCQs · ASP.NET Web API interview questions

My notes#

Saved in this browser.

Progress is saved in this browser. Sign in to sync.