This is self-hosted RESTful MVC4 Web API, the only route is api/{controller}/{state}. When I send an HTTP-POST that has a body, the state argument comes in null. If I remove the body the state variable is present.
The way I thought it worked for HTTP-POST was that the url parameters get mapped then the body gets serialized into the extra parameter, which in this case is data parameter. The content is just string data which I had to write a custom MediaTypeFormatter (which I thought was odd it couldn't handle a regular string).
Here is my controller signature
public class MyController : ApiController
{
public void Post(string state, string data)
{
}
}
Has anyone seen this behavior before or can explain to me why having a body present is affecting my url parameter?
One Solution:
I tried changing data parameter into a complex type (Just a class with a public property) and sending the content as text/xml instead of text/plain and it worked as expected. The state parameter wasn't null and I had my strongly typed object with the data. I suppose MVC wants to have something to deserialize like XML or JSON for the http-request body...
More Research:
I've had the chance to run some more tests. If the body of a post is XML/JSON it will first try to map the properties of the body-object to the method parameters like so. If still has unmapped properties then it will match the remaining properties to the properties of any strongly-typed objects in the method parameters
PostMethod(string p1, string p2, myClass obj) // if myClass has a p3 property it will be mapped from the xml body.
{
}
// xml in body of http-post
<Xml>
</p1>
</p2>
</p3>
</Xml>
If all the parameters were not mapped, then it will attempt to map the url parameters. To relate it directly to my initial problem. The best and easiest solution I see at this time is to send text/xml like this.
PostMethod(string state, string data)
{
}
<data>put data here</data>
Urlencoded key/value pairs also work very well.
var r = client.PostAsync(url, new StringContent("data=Something", Encoding.UTF8, "application/x-www-form-urlencoded"));
My best guess is that the key/value nature of JSON and XML, FormEncoded help it to map to parameters so that is why it doesn't like plain strings.
This sure gave me a headache and I find the MVC4 documentation to be rather scarce (its still in beta), but I hope this can help someone else who may have the same problem.
Related
I'm pretty new to web development and swagger in general so apologies if the question is too naive.
I'll use the Asp.Net Core Web Api template in visual studio 2019 to better explain my problem, so please consider that environment in the case I have omitted some information (or just ask for the missing part that I'll bring it).
There we have the WeatherForecastController class with a simple GET (which I included the names parameter):
[HttpGet]
public IEnumerable<WeatherForecast> Get(IEnumerable<string> names)
{
//...
}
When I try to test this request using the Swagger page, it doesn't recognize the name parameter.
I have ran other tests to find out what's going on and I found the following:
Swagger does work with IEnumerable as it works pretty well if the parameter is of type IEnumerable<IFormFile> (it displays a list of uploaded files, shows the file selection dialog, etc);
I tried encapsulating the parameter(s) in a DTO class and it seems to break even more stuff (even the IEnumerable<IFormFile> doesn't seem to work inside a DTO class; it only works if passed directly in the parameter list of the [HttpVerb] method;
I tried with other similar types as well as ICollection<string>, List<string>, string[]; none of them seem to work;
This same problem happens using primitive types like bool, int, etc as type arguments to IEnumerable<T>;
So what is happening? Should I set some sort of configuration value so it can work with collections of primitive types?
Update with images showing the problem:
... Get(string names, IEnumerable<IFormFile> file):
... Get(IEnumerable<string> names, IFormFile file):
As you can see, when any parameter in the param list is of IEnumerable the swagger UI doesn't properly show the requested fields like in the second image.
You must specify model binding sources in your case. Your action method should be like this:
[HttpPost("test/names")]
public IEnumerable<string> PostNames([FromQuery]IEnumerable<string> names, IEnumerable<IFormFile> files)
{
//...some code
return names;
}
You shouldn't use two or more complex type parameter as an action parameters until you specify the source they are binding from. That's because complex objects are bound to request body by default and only one parameter can be bound to request body.
As of microsoft docs :
Don't apply [FromBody] to more than one parameter per action method. Once the request stream is read by an input formatter, it's no longer available to be read again for binding other [FromBody] parameters.
Complex type means class variables, arrays, and those which are not primary types like int,double, string and etc.
Swagger generates this UI for action above:
As the final word , Note that you can't send something in your request body when you are using GET http request.
I have a request that my Web is receiving - from looking at a wireshark I can see that the form contains two parts.
The first part is type application/json and is called metadata and the second part is audio/wav and is called media_audio_wav, and both have content-disposition set to form-data. I do not control the request coming in so I need to make it work with what I'm getting.
I have a controller method declared like this:
public async Task<IActionResult> MyControllerMethod([FromForm] MyModelClass jsonInput)
{
await _myClass.DoSomeBusinessLogic(jsonInput);
return Ok();
}
In the above, MyModelClass is a class that the JSON part successfully binds to. What I want to figure out is how to get the byte[] field called media_audio_wav. I have tried adding a second [FromForm] parameter and that didn't seem to work. I have also tried simply declaring a field in my model class that is of type byte[] with the proper name and that didn't work either.
If I declare a field in my model class of type string, the .NET model binder will bind to it, but because the data is binary, when it is converted to the string the audio comes out corrupted since strings cannot represent binary data properly. Because of this, I need to get the raw binary without first turning it into a string. I can't use IFormFile because there is not filename specified on the binary data.
I wrote the following method to try to diagnose a problem I'm experiencing in my application:
[Route("/api/flow/test"), HttpPost]
[ProducesResponseType(typeof(string), (int)HttpStatusCode.OK)]
public async Task<IActionResult> Test(string id, [FromBody] JToken input)
{
var result = input == null ? "Not OK" : "OK";
return Ok(result);
}
I post a large (6.5MB+) JSON body to it, and in one instance it works fine. When I post a similar JSON with some added properties, it does not - the input parameter comes in as null. However, both JSON validate successfully with every tool I've found that can handle their size. Please provide some additional ideas about how to further investigate what is causing the body parameter input to be treated as null.
One option would be to declare it as string instead of JToken and try to explicitly parse it in the action body.
This will show you two things:
If input is still empty, it's not a problem with JSON parsing
If parsing it explicitly fails, the JSON is indeed invalid.
Only when input is not null and parsing it in the body works - only then you need to deep dive into what actually happens, when you declare an action parameter as JToken.
From the client, I would like to pass a collection of json nodes: [{key, value},{key, value}] to a WebAPI endpoint. what should my api endpoint param type be? a List<>() or something else?
This would be using C#.
I need to iterate over each endpoint that is passed in the collection.
In general, it is good practice to create a model that your JSON will be deserialized into. To answer your question though if your JSON was in the format
[{key1: value1},{key2: value2}]
you would be able to use a
List<Dictionary<string,object>>()
If you were sure that your values were always string values you could do
List<Dictionary<string,string>>()
As JSON values can be strings (wrapped in quotes), integers (no quotes) or null.
So your Web API controller could be something like this:
[HttpPost]
public IHttpActionResult ReceiveJSON([FromBody]List<Dictionary<string,string>> in_json)
{
// And then one way to iterate over each 'json node' passed
foreach(var dict in in_json)
{
// Do something with dictionary object
}
return Ok(in_sjon);
}
What version of ASP.Net Web API will you be using?
I tried the following code:
[OperationContract]
[WebInvoke(UriTemplate="/Users/Register/{user}")]
void Register(User user);
But when I Try to run this, it tells me the UriTemplate must only contain strings. What if I need to pass in an object to my method as in this case, a User object to my Register method.
If I change the WebInvoke attribute to:
[WebInvoke(UriTemplate="/Users/Register/")]
The browswer displays the error Method not allow when I try to browse to http://localhost:8000/Users/Register for example
You are limited to strings in the UriTemplate. You could use multiple parameters to pass multiple strings but you cannot use a complex type. If you want to pass a complex type, then it cannot be anywhere in the URI but rather in the body of a POST/PUT request. The GET request does not take a message body. So your above code could be changed to this:
[OperationContract]
[WebInvoke(Method = "POST", UriTemplate="/Users/Register")]
void Register(User user);
Where you're passing the User object in, not via the Uri, but as part of the POST request.
It is exactly as it says. A URI is literally only capable of containing strings. They aren't made for objects. You could probably convert it to take a user name or user id instead if you needed... but you can never use a complex object type as a URI.
You don't pass objects around as query string parameters (how could you? would it serialize it somehow?) You should pass in the user id.