Bug
Simple project (bindstringrepro.zip, but also below).
dotnet runapplication/json, and add valid JSON in the body, e.g. { } or { "Value": "Some value" }public class Program
{
public static void Main(string[] args) => CreateWebHostBuilder(args).Build().Run();
public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.UseUrls("http://localhost:5100")
.ConfigureServices(services => services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Latest))
.Configure(app => app.UseMvc());
}
[Route("api/[controller]")]
public class ValuesController : ControllerBase
{
// GET api/values
[HttpPost]
public ActionResult<string> Post([FromBody] string payload)
{
return payload;
}
}
An MVC controller with a single string parameter, decorated with [FromBody]. POSTing a JSON body to the API with an application/json Content-Type triggers the JsonInputFormatter, but throws a JsonReaderException, even if the JSON is completely valid (e.g. {}).
As far as I can tell, binding a string to the body should be possible. @RickStrahl explicitly mentions it in a post from last year, so I'm guessing this is a change in 2.x?
Logs compressed for readibility
info: Microsoft.AspNetCore.Hosting.Internal.WebHost[1]
Request starting HTTP/1.1 POST http://localhost:5100/api/values application/json 2
dbug: Microsoft.AspNetCore.HostFiltering.HostFilteringMiddleware[0]
Wildcard detected, all requests with hosts will be allowed.
dbug: Microsoft.AspNetCore.Routing.Tree.TreeRouter[1]
Request successfully matched the route with name '(null)' and template 'api/Values'.
info: Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker[1]
Route matched with {action = "Post", controller = "Values"}. Executing action bindstringrepro.Controllers.ValuesController.Post (bindstringrepro)
dbug: Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker[1]
Execution plan of authorization filters (in the following order): None
dbug: Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker[1]
Execution plan of resource filters (in the following order): Microsoft.AspNetCore.Mvc.ViewFeatures.Internal.SaveTempDataFilter
dbug: Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker[1]
Execution plan of action filters (in the following order): Microsoft.AspNetCore.Mvc.ModelBinding.UnsupportedContentTypeFilter
dbug: Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker[1]
Execution plan of exception filters (in the following order): None
dbug: Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker[1]
Execution plan of result filters (in the following order): Microsoft.AspNetCore.Mvc.ViewFeatures.Internal.SaveTempDataFilter
dbug: Microsoft.AspNetCore.Mvc.ModelBinding.ParameterBinder[22]
Attempting to bind parameter 'payload' of type 'System.String' ...
dbug: Microsoft.AspNetCore.Mvc.ModelBinding.Binders.BodyModelBinder[44]
Attempting to bind parameter 'payload' of type 'System.String' using the name '' in request data ...
dbug: Microsoft.AspNetCore.Mvc.ModelBinding.Binders.BodyModelBinder[2]
Rejected input formatter 'Microsoft.AspNetCore.Mvc.Formatters.JsonPatchInputFormatter' for content type 'application/json'.
dbug: Microsoft.AspNetCore.Mvc.ModelBinding.Binders.BodyModelBinder[1]
Selected input formatter 'Microsoft.AspNetCore.Mvc.Formatters.JsonInputFormatter' for content type 'application/json'.
dbug: Microsoft.AspNetCore.Server.Kestrel[25]
Connection id "0HLH8G3GLI442", Request id "0HLH8G3GLI442:00000001": started reading request body.
dbug: Microsoft.AspNetCore.Server.Kestrel[26]
Connection id "0HLH8G3GLI442", Request id "0HLH8G3GLI442:00000001": done reading request body.
dbug: Microsoft.AspNetCore.Mvc.Formatters.JsonInputFormatter[1]
JSON input formatter threw an exception.
Newtonsoft.Json.JsonReaderException: Unexpected character encountered while parsing value: {. Path '', line 1, position 1.
at Newtonsoft.Json.JsonTextReader.ReadStringValue(ReadType readType)
at Newtonsoft.Json.JsonTextReader.ReadAsString()
at Newtonsoft.Json.JsonReader.ReadForType(JsonContract contract, Boolean hasConverter)
at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.Deserialize(JsonReader reader, Type objectType, Boolean checkAdditionalContent)
dbug: Microsoft.AspNetCore.Mvc.ModelBinding.Binders.BodyModelBinder[45]
Done attempting to bind parameter 'payload' of type 'System.String'.
dbug: Microsoft.AspNetCore.Mvc.ModelBinding.ParameterBinder[23]
Done attempting to bind parameter 'payload' of type 'System.String'.
dbug: Microsoft.AspNetCore.Mvc.ModelBinding.ParameterBinder[26]
Attempting to validate the bound parameter 'payload' of type 'System.String' ...
dbug: Microsoft.AspNetCore.Mvc.ModelBinding.ParameterBinder[27]
Done attempting to validate the bound parameter 'payload' of type 'System.String'.
info: Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker[1]
Executing action method bindstringrepro.Controllers.ValuesController.Post (bindstringrepro) with arguments () - Validation state: Invalid
info: Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker[2]
Executed action method bindstringrepro.Controllers.ValuesController.Post (bindstringrepro), returned result Microsoft.AspNetCore.Mvc.ObjectResult in 0.1929ms.
dbug: Microsoft.AspNetCore.Mvc.Infrastructure.DefaultOutputFormatterSelector[11]
List of registered output formatters, in the following order: Microsoft.AspNetCore.Mvc.Formatters.HttpNoContentOutputFormatter, Microsoft.AspNetCore.Mvc.Formatters.StringOutputFormatter, Microsoft.AspNetCore.Mvc.Formatters.StreamOutputFormatter, Microsoft.AspNetCore.Mvc.Formatters.JsonOutputFormatter
dbug: Microsoft.AspNetCore.Mvc.Infrastructure.DefaultOutputFormatterSelector[4]
No information found on request to perform content negotiation.
dbug: Microsoft.AspNetCore.Mvc.Infrastructure.DefaultOutputFormatterSelector[8]
Attempting to select an output formatter without using a content type as no explicit content types were specified for the response.
dbug: Microsoft.AspNetCore.Mvc.Infrastructure.DefaultOutputFormatterSelector[10]
Attempting to select the first formatter in the output formatters list which can write the result.
dbug: Microsoft.AspNetCore.Mvc.Infrastructure.DefaultOutputFormatterSelector[2]
Selected output formatter 'Microsoft.AspNetCore.Mvc.Formatters.HttpNoContentOutputFormatter' and content type '' to write the response.
info: Microsoft.AspNetCore.Mvc.Infrastructure.ObjectResultExecutor[1]
Executing ObjectResult, writing value of type 'null'.
info: Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker[2]
Executed action bindstringrepro.Controllers.ValuesController.Post (bindstringrepro) in 121.5176ms
dbug: Microsoft.AspNetCore.Server.Kestrel[9]
Connection id "0HLH8G3GLI442" completed keep alive response.
info: Microsoft.AspNetCore.Hosting.Internal.WebHost[2]
Request finished in 267.6543ms 204
Microsoft.AspNetCore.Mvc or Microsoft.AspNetCore.App or Microsoft.AspNetCore.All:Microsoft.NETCore.App 2.1.4
His example is a JSON encoded string - search for the following, on the linked blog post.
This works to retrieve the JSON string as a plain string. Note that the string sent is not a raw string, but rather a JSON string as it includes the wrapping quotes:
If you want to get at the raw body content, I'd suggest just reading it yourself, or if you reallly really want something that's a parameter, implement something like: https://github.com/aspnet/Mvc/blob/release/2.2/test/WebSites/FormatterWebSite/StringInputFormatter.cs
Thanks for the quick reply @rynowak 馃憤
His example is a JSON encoded string search
Hmmm, so it is, fair point. I read that as meaning it had to be JSON, not _only_ a JSON string.
I can work around the issue for my example. The easiest way for me was just to bind a JObject and call ToString() in the method. Horrible performance-wise, but this is a rarely-called non-performance critical endpoint, so it'll do for now.
It still seems a little odd to me that it doesn't work though. I guess it's a rare use case. This was an app ported from old-school ASP.NET, so apparently there's a difference in behaviour there.
I guess it's a rare use case.
Can I ask a few questions for my own education? I've always been a little mystified when these topics come up.
Sure
- What do you ultimately need the body for?
It's being deconstructed into various domain objects. We don't have control over the incoming document (it's an external system posting to ours), so we need to be quite flexible in how we deconstruct it (I believe - I didn't write it)
Why is it important that you bind it to a parameter?
It's not _important_, it's just convenient. Wherever possible I don't want to have to mess with the underlying request. MVC should handle that for me 馃檪
Why do you want to body as a string as opposed to a stream?
Because existing APIs need a string
Makes sense! If you really like the idea of it being a parameter, I'd suggest using the StringInputFormatter I linked.
If you want to do something more first class, you also have the option to write your own model binder - that sounds like a better fit. Then you could do the translation into your domain models in there (if it helps).
Closing this since it sounds like there's no action needed from us.
One reason for parameters vs streams is documentation. Parameters self document the api vs hidden black box with stream. This is true both for just looking at the code or if you use doc tools to generate api docs.