ASP.NET Core – Get posted form data in an API Controller

To get posted form data in an API Controller (using the [ApiController] attribute) in ASP.NET Core, use parameters with the [FromForm] attribute.

[Route("api/[controller]")]
[ApiController]
public class LocationController : ControllerBase
{
	[HttpPost]
	public IActionResult Post([FromForm] string location)
	{
		//process the form data
		
		return Ok(location);
	}
}
Code language: C# (cs)

Now send a request with form data to this endpoint to see it work. The request would look like this:

POST /api/weather HTTP/1.1
Content-Type: application/x-www-form-urlencoded

location=United+StatesCode language: plaintext (plaintext)

The form data is a string of key-value pairs (ex: location=United+States). The framework tries to map the form data to parameters by matching the form keys with parameter names (or model property names).

You can map multiple form fields as individual parameters, map to a model, and also read the form data directly if you want. In this article, I’ll show examples of each of these scenarios.

Map multiple form fields as individual parameters

Let’s say your form has two fields: Location and Temperature. You can map these fields as individual parameters:

[Route("api/[controller]")]
[ApiController]
public class WeatherController : ControllerBase
{
	[HttpPost]
	public IActionResult Post([FromForm] string location, [FromForm] int temperature)
	{
		//process the parameters
		
		return Ok($"{location} {temperature}");
	}
}
Code language: C# (cs)

Here’s what a request to this endpoint would look like:

POST /api/weather HTTP/1.1
Content-Type: application/x-www-form-urlencoded

location=United+States&temperature=64Code language: plaintext (plaintext)

Note: When there are multiple form fields, the key-value pairs are separated with an ampersand (&).

Mapping to a model

Instead of mapping form fields as individual parameters, you can map to a model. This is a good option because it does model validation.

[Route("api/[controller]")]
[ApiController]
public class WeatherController : ControllerBase
{
	[HttpPost]
	public IActionResult Post([FromForm] Weather weather)
	{
		SaveToDatabase(weather);

		return Ok();
	}
	
	//rest of class
}
Code language: C# (cs)

This maps the form data to the Weather class, which has the following definition:

using System.ComponentModel.DataAnnotations;

public class Weather
{
	[Required]
	public string Location { get; set; }

	public string Description { get; set; }

	[Required]
	[Range(-200, 200)]
	public int Temperature { get; set; }
}
Code language: C# (cs)

Requests to this endpoint would look like this:

POST /api/weather HTTP/1.1
Content-Type: application/x-www-form-urlencoded

location=United+States&temperature=64&description=cloudyCode language: plaintext (plaintext)

When mapping to a model, it matches form keys to model property names. As you can see, it does a case-insensitive comparison (location matches Weather.Location).

Mapping validation errors

When it maps form data to parameters, it performs validation and returns error response 400 – Bad Request if there’s a problem.

For example, if there’s a type mismatch, you’ll get an error response like this:

{
    "type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
    "title": "One or more validation errors occurred.",
    "status": 400,
    "traceId": "00-64ab197a56446c448095e9d3c3082c8b-a4255f612fce2e49-00",
    "errors": {
        "Temperature": [
            "The value 'text' is not valid for Temperature."
        ]
    }
}
Code language: JSON / JSON with Comments (json)

If you’re mapping to a model, it’ll also validate against the System.ComponentModel.DataAttributions attributes.

For example, Weather.Location has the [Required] attribute. When a required property is missing in the form data, you’ll get an error like this:

{
    "type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
    "title": "One or more validation errors occurred.",
    "status": 400,
    "traceId": "00-d37111fc22168f42b5cbe4684eea9359-7a98c6fa7d623b4e-00",
    "errors": {
        "Location": [
            "The Location field is required."
        ]
    }
}Code language: JSON / JSON with Comments (json)

Error response 415 – Media Type Unsupported

When you try to to map to a model and don’t include the [FromForm] attribute, like this:

[HttpPost]
public IActionResult Post(Weather weather)
Code language: C# (cs)

Then you’ll get error response 415 – Unsupported Media Type.

{
    "type": "https://tools.ietf.org/html/rfc7231#section-6.5.13",
    "title": "Unsupported Media Type",
    "status": 415,
    "traceId": "00-cac8e6fd75525e40b3e2b61e0bc2008a-725c6cba45bde44d-00"
}
Code language: JSON / JSON with Comments (json)

This can be fixed by adding the [FromForm] attribute:

[HttpPost]
public IActionResult Post([FromForm] Weather weather)
Code language: C# (cs)

Read form data without mapping

There may be scenarios where you want to read the form data without having the framework map it for you. The form data is stored in HttpContext.Request.Form. This has key/value pairs (like a dictionary), and you can get values by key. Here’s an example of looping through the form data:

[HttpPost]
public IActionResult Post()
{
	foreach(var key in HttpContext.Request.Form.Keys)
	{
		var val = HttpContext.Request.Form[key];

		//process the form data
	}
	
	return Ok();
}
Code language: C# (cs)

Leave a Comment