Input formatters are used to deserialize the request body to a model object (which is then passed into an action method). There are built-in input formatters for handling JSON and XML. You can add your own input formatter when you want to customize request body deserialization.
There are two scenarios where a custom InputFormatter would be useful:
- You want to receive a Content-Type that’s not supported by the built-in input formatters (such as the text/plain Content-Type).
- You want to deserialize JSON/XML differently than the settings / custom converters can handle.
To add an input formatter:
- Subclass InputFormatter (or TextInputFormatter if you need to deal with encodings besides UTF-8)
- Specify the Content-Type(s).
- Specify the parameter type(s).
- Deserialize the request body.
Here’s an example of implementing an input formatter that reads Content-Type “text/csv” for model parameters of type string[]:
using Microsoft.AspNetCore.Mvc.Formatters;
public class CsvStringInputFormatter : InputFormatter
{
public CsvStringInputFormatter()
{
//Which Content-Types this InputFormatter can handle
SupportedMediaTypes.Add("text/csv");
}
public override async Task<InputFormatterResult> ReadRequestBodyAsync(InputFormatterContext context)
{
using (var reader = new StreamReader(context.HttpContext.Request.Body))
{
string csv = await reader.ReadToEndAsync();
var values = csv.Split(",");
return InputFormatterResult.Success(values);
}
}
protected override bool CanReadType(Type type)
{
//Which action parameter types this InputFormatter can handle
return type == typeof(string[]);
}
}
Code language: C# (cs)
Note: Override CanRead() if you need to check more than just the Content-Type to decide if you can handle the request.
To make the framework use this, add it to the InputFormatters in the initialization code:
using Microsoft.Extensions.DependencyInjection;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers(o => o.InputFormatters.Add(new CsvStringInputFormatter()));
//rest of init code
Code language: C# (cs)
Now in the controller, add a parameter of the type that the custom input formatter handles (string[] in this case):
[HttpPost()]
[Consumes("text/csv")] //optional: this means only this content-type is allowed
public IActionResult Post([FromBody]string[] names)
{
return Ok($"Posted {names.Length} name(s)");
}
Code language: C# (cs)
Note: Adding [Consumes] is optional. It restricts the action to only accepting requests with this Content-Type.
To see it work, send a request with Postman and set the Content-Type (to text/csv in this example):
POST /Names
Content-Type: text/csv
Body:
a,b,c
Code language: plaintext (plaintext)
This request can be handled by CsvStringInputFormatter because it meets the conditions:
- Content-Type is text/csv.
- Parameter type is string[].
It outputs the following 200 (OK) response:
Posted 3 name(s)
Code language: plaintext (plaintext)
Return a good error
It returns a 500 error response if an exception is thrown from ReadRequestBodyAsync().
If you return just InputFormatterResult.Failure(), it’ll return a problem details error response (400 – Bad Request). This has a somewhat misleading error message like 400 (Bad Request) – “The names field is required”.
To return a better error message:
- Add error details to context.ModelState.
- Return InputFormatterResult.Failure().
Here’s an example:
public override async Task<InputFormatterResult> ReadRequestBodyAsync(InputFormatterContext context)
{
using (var reader = new StreamReader(context.HttpContext.Request.Body))
{
string csv = await reader.ReadToEndAsync();
var values = csv.Split(",");
if (values[0] == "bad")
{
context.ModelState.TryAddModelError("Body", "Invalid value in CSV!");
return InputFormatterResult.Failure();
}
return InputFormatterResult.Success(values);
}
}
Code language: C# (cs)
Now test it by sending a request with Postman that’ll result in an error. In this simple example, sending the word “bad” will cause it to return an error:
POST /Names
Content-Type: text/csv
Body:
bad,a,b,c
This returns the following 400 (Bad Request) response with the error details we added:
{
"type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
"title": "One or more validation errors occurred.",
"status": 400,
"traceId": "00-3f0ce093d0d2699af60cef301d97c1e4-1c406fad9d15c0b8-00",
"errors": {
"Body": [
"Invalid value in CSV!"
]
}
}
Code language: JSON / JSON with Comments (json)