ASP.NET Web API (.NET Framework) · Building APIs

Request and Response Models

Model binding turns an HTTP request into C# parameters; formatters turn your return value back into JSON. Knowing the rules saves hours of “why is my parameter null?”.

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

Learning objectives#

  • Know where Web API 2 binds each parameter from
  • Use [FromUri] for query objects and [FromBody] for request bodies
  • Read request headers
  • Configure JSON output: camelCase, no XML, ignore nulls

Explanation#

Simple types (int, string, bool, decimal, DateTime, Guid) are read from the URL — route values and the query string — by default.

Complex types (classes) are read from the request body by default, using the Content-Type header to choose a formatter (JSON or XML).

Override with attributes: [FromUri] makes a class bind from the query string (great for search filters); [FromBody] makes a simple type bind from the body. Only ONE parameter may come from the body.

Web API 2 serialises with Json.NET. By default property names stay PascalCase and the XML formatter is enabled. Most teams remove XML and switch to camelCase in WebApiConfig.

Read headers through Request.Headers (an HttpRequestHeaders). Use TryGetValues, because a missing header is normal.

Syntax#

C# Illustrative
public IHttpActionResult Get(int id)                          // id from the route
public IHttpActionResult Search([FromUri] ProductQuery query) // class from the query string
public IHttpActionResult Post(CreateProductRequest request)   // class from the JSON body
public IHttpActionResult Rename(int id, [FromBody] string name)

Code example: Binding inputs and shaping JSON#

C#ProductsController.cs Illustrative
using System.Collections.Generic;
using System.Linq;
using System.Web.Http;
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;


// App_Start/WebApiConfig.cs
public static class WebApiConfig
{
    public static void Register(HttpConfiguration config)
    {
        config.MapHttpAttributeRoutes();


        config.Formatters.Remove(config.Formatters.XmlFormatter);      // JSON only
        var json = config.Formatters.JsonFormatter.SerializerSettings;
        json.ContractResolver = new CamelCasePropertyNamesContractResolver();
        json.NullValueHandling = NullValueHandling.Ignore;
        json.DateTimeZoneHandling = DateTimeZoneHandling.Utc;
    }
}


public class ProductQuery                 // bound from ?category=Books&maxPrice=500
{
    public string Category { get; set; }
    public decimal? MaxPrice { get; set; }
}


public class CreateProductRequest         // bound from the JSON body
{
    public string Name { get; set; }
    public decimal Price { get; set; }
}


public class ProductResponse
{
    public int Id { get; set; }
    public string Name { get; set; }
    public decimal Price { get; set; }
    public string Badge { get; set; }     // null → left out of the JSON
}


[RoutePrefix("api/products")]
public class ProductsController : ApiController
{
    [HttpGet, Route("")]
    public IHttpActionResult Search([FromUri] ProductQuery query)
    {
        return Ok(new { query.Category, query.MaxPrice });
    }


    [HttpPost, Route("")]
    public IHttpActionResult Create(CreateProductRequest request)
    {
        var response = new ProductResponse { Id = 7, Name = request.Name, Price = request.Price };
        return Created("api/products/7", response);
    }


    [HttpPut, Route("{id:int}/name")]
    public IHttpActionResult Rename(int id, [FromBody] string newName)
    {
        return Ok(new { id, newName });
    }


    [HttpGet, Route("language")]
    public IHttpActionResult Language()
    {
        IEnumerable<string> values;
        var language = Request.Headers.TryGetValues("Accept-Language", out values) ? values.First() : "en";
        return Ok(new { language });
    }
}
Expected output
GET  /api/products?category=Books&maxPrice=500
     → 200 {"category":"Books","maxPrice":500.0}

POST /api/products   Content-Type: application/json   {"name":"Pen","price":20}
     → 201 {"id":7,"name":"Pen","price":20.0}          (badge is null, so it is omitted)

PUT  /api/products/7/name   Content-Type: application/json   "Gel Pen"
     → 200 {"id":7,"newName":"Gel Pen"}

GET  /api/products/language   Accept-Language: hi-IN
     → 200 {"language":"hi-IN"}

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. WebApiConfig removes the XML formatter and sets camelCase names, so C# Name becomes JSON name.
  2. [FromUri] ProductQuery collects several query-string values into one object.
  3. CreateProductRequest is a class, so it is read from the JSON body automatically.
  4. [FromBody] string reads a raw JSON string — the body must be "Gel Pen" in quotes.
  5. NullValueHandling.Ignore drops null properties such as badge from the response.
  6. Request.Headers.TryGetValues reads an optional header safely.

Real-world use case#

A React front end expects camelCase JSON. One change in WebApiConfig makes every endpoint match, instead of decorating hundreds of properties with [JsonProperty("name")].

Common mistakes#

Parameter is always null

Usually the Content-Type header is missing or wrong (send application/json), or the JSON does not match the class property names/types.

Two complex parameters from the body

Web API 2 can read the body only once. Combine them into one request class.

Sending a plain string to [FromBody] string

The JSON formatter expects "text" with quotes. Unquoted text binds to null.

Best practices#

  • Use one request class per POST/PUT action
  • Use [FromUri] classes for filters and paging
  • Configure JSON globally once
  • Return response classes, not anonymous objects, for documented endpoints

Remember this#

Simple types → URL. Classes → body.

[FromUri] class = query-string object. Only one [FromBody].

camelCase: CamelCasePropertyNamesContractResolver.

Practice exercise#

Your task: Write an action GET api/reports/sales that binds a [FromUri] SalesQuery with From and To (DateTime) and Region (string, optional), and returns them back. What query string calls it?

Need a hint?
DateTime values bind from ISO strings like 2026-01-01.
Show solution
C# Illustrative
using System;
using System.Web.Http;


public class SalesQuery
{
    public DateTime From { get; set; }
    public DateTime To { get; set; }
    public string Region { get; set; }
}


public class ReportsController : ApiController
{
    // GET api/reports/sales?from=2026-01-01&to=2026-03-31&region=South
    [HttpGet, Route("api/reports/sales")]
    public IHttpActionResult Sales([FromUri] SalesQuery query)
    {
        return Ok(query);
    }
}

Mini quiz#

Request and Response Models quizMultiple choice Question 1 of 3

By default, where does Web API 2 bind a complex type parameter from?

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.