0

Using asp.net web api v2, I have a working POST method that I am able to POST a custom type from a different application, and by using JSONConvert I am able to deserialize it and use it in my POST method body.

However, the parameter to my POST must be of type "object" or else the parameter is not found (null).

Why does this happen? I would ideally have the custom type as the parameter type, so that my API Documentation can populate with the proper request info, since it auto-generates the API docs based on the parameter type used (don't see a way to override that -- would be great if that was possible).

See my code below -- if "incomingInformation" is of type "RemoteFileInfo" rather than type "object", a null exception is thrown when I try to .toString() it.

[Route("api/xx/uploadfiletoalfresco/")]
    [HttpPost()]
    public ResultStruct UploadFileToAlfresco(object incomingInformation)
    {
        JObject deserializedJObject = (JObject)JsonConvert.DeserializeObject(incomingInformation.ToString());
        SA.Services.RemoteFileInfo convertedRemoteFileInfo = deserializedJObject.ToObject<SA.Services.RemoteFileInfo>();
...

Here is my sample code on the sending application (vb.net) - the content type is set as application/json and is serialized before sending

Dim req As WebRequest = WebRequest.Create(_restEndpointURL & "/uploadfiletoalfresco/")
    req.ContentType = "application/json"
    req.Method = "POST"
    Using sw As New StreamWriter(req.GetRequestStream())
        Dim ser As New JavaScriptSerializer
        Dim serJSON = ser.Serialize(JsonConvert.SerializeObject(remoteFileInfo))
        sw.Write(serJSON)
        sw.Flush()
        sw.Close()
    End Using

Below is my remoteFileInfo type, it is declared this way on both the receiving app and sending app. It is converted to JSON string before sending by the method JsonConvert.SerializeObject

Partial Public Class RemoteFileInfo
    Public CategoryID As Integer
    Public FileName As String
    Public Length As Long
    Public Note As String
    Public SFSubmissionID As String
    Public SourceInstance As String
    Public Subject As String
    Public UserID As Integer
    Public Visibility As Boolean
    Public riskID As Integer
    Public fileByteArray As Byte()
End Class

Receiving app definition:

 public class RemoteFileInfo
{

    public int CategoryID;
    public string FileName;
    public long Length;
    public string Note;
    public string SFSubmissionID;
    public string SourceInstance;
    public string Subject;
    public int UserID;
    public bool Visibility;
    public int riskID;
    public Byte[] fileByteArray;
}

Sample JSON from the sending application:

"{"CategoryID":2,"FileName":"Scrum postponed until this afternoon .msg","Length":62976,"Note":"asdf","SFSubmissionID":"006E000000OuYxP","SourceInstance":"Addin","Subject":"Scrum postponed until this afternoon ","UserID":0,"Visibility":true,"riskID":0,"fileByteArray":"VERY LONG STRING"}"

Full JSON from fiddler:

POST http://yyyy/api/xxx/uploadfiletoalfresco/ HTTP/1.1
Content-Type: application/json
Host: yyyyy
Content-Length: 84273
Expect: 100-continue
Connection: Keep-Alive

"{\"CategoryID\":2,\"FileName\":\"Scrum postponed until this afternoon .msg\",\"Length\":62976,\"Note\":\"asdf\",\"SFSubmissionID\":\"006E000000OuYxP\",\"SourceInstance\":\"Addin\",\"Subject\":\"Scrum postponed until this afternoon \",\"UserID\":0,\"Visibility\":true,\"riskID\":0,\"fileByteArray\":\"VERY LONG STRING - user edited this is not how it looks in fiddler!\"}"
nismonster
  • 61
  • 9
  • That likely has to do with an incorrect ContentType. See http://stackoverflow.com/questions/20226169/how-to-pass-json-post-data-to-web-api-method-as-object for an example. – B2K Feb 24 '15 at 22:23
  • This isn't quite what you were asking for but you can put `JObject _incomingInformation = incomingInformation as JObject;` This will identify _incomingInformation as typeof(JObject) with the values passed through on the incomingInformation parameter. – CalebB Feb 24 '15 at 22:26
  • @B2K Please see edited post -- I do have the application/json type as concluded in your linked post. Any other things you see that could be causing this? – nismonster Feb 24 '15 at 22:28
  • You haven't given enough information to provide a definitive answer. Where is the json data that you are posting and the definition of RemoteFileInfo? – B2K Feb 24 '15 at 22:38
  • @B2K Definition of RemoteFileInfo has been added. I can paste a sample raw json if that will help – nismonster Feb 24 '15 at 22:45
  • The json string is enclosed in quotes. I'm assuming that is how you are getting it. If so, that not a valid json body, and it is not a valid javascript string because none of the internal quotes are escaped. – B2K Feb 24 '15 at 23:22
  • Thanks B2K, that was actually a paste from my Watch window in Visual Studio -- I went ahead and pasted the full JSON that shows in Fiddler -- it is correctly escaped. The receiving app can successfully decode the JSON and convert to my object. I would just rather not have the generic Object type in my parameter field. – nismonster Feb 24 '15 at 23:26
  • Would love to see the actual exception you are getting - not sure if this is the problem but you have declared `fileByteArray` as `Byte[]` in `RemoteFileInfo` but in the JSON you posted it's just a `string`. I'm not even sure you can desrialise to an array of `Byte` (and this would explain why it can only be model bound using object; basically it's WebAPI telling you that the data received is not a `RemoteFileInfo` as far as it can tell)...try changing it to `string` and see what happens – Stephen Byrne Feb 25 '15 at 00:04
  • @StephenByrne The exception is that if the method signature of public ResultStruct UploadFileToAlfresco(object incomingInformation) is changed to public ResultStruct UploadFileToAlfresco(RemoteFileInfo incomingInformation), the incomingInformation parameter is NULL. I would like to know why this requires an "object" type rather than "RemoteFileInfo" – nismonster Feb 25 '15 at 00:46

3 Answers3

0

The reason for a such behaviour is that your parameters must be transportable via the request string. In your Route attribute you are saying that the request for your method should be like this: api/xx/uploadfiletoalfresco/. And there is no way the browser will be available to create a string representation for your object in a query string - this can be done only for a strings or value types such as integers.

So you have two choices: let the things stay as they are or use the WCF services, where you can provide a WSDL for your types.

VMAtm
  • 27,943
  • 17
  • 79
  • 125
  • There is no parameter passed in the browser query string -- there is only post data. The post data can be represented as a string (json). I do see many examples where the custom type is successfully posted, for instance the thread http://stackoverflow.com/questions/20226169/how-to-pass-json-post-data-to-web-api-method-as-object – nismonster Feb 24 '15 at 22:40
  • @nismonster Can you provide a working sample with `Route` attribute? – VMAtm Feb 24 '15 at 23:05
  • I removed the Route attribute and observe the same treatment -- the incomingInformation is null unless defined as Object type on my receiving app. So the problem still exists regardless of the Route attribute. I originally had the Route there to aid visually while developing, but since I am following the default routeTemplate of api/{controller}/{method}, it is was not necessary. – nismonster Feb 24 '15 at 23:17
0

I just tested this with WebAPI 2 using the data you supplied in your post, and using string for fileByteArray and declaring the model as RemoteFileInfo works just fine for me.

 public class RemoteFileInfo
    {

        public int CategoryID;
        public string FileName;
        public long Length;
        public string Note;
        public string SFSubmissionID;
        public string SourceInstance;
        public string Subject;
        public int UserID;
        public bool Visibility;
        public int riskID;
        public string fileByteArray;
    }


    public class ValuesController : ApiController
    {
        [HttpPost]
        public string Test(object incomingInformation)
        {
            JObject deserializedJObject = (JObject)JsonConvert.DeserializeObject(incomingInformation.ToString());
            var convertedRemoteFileInfo = deserializedJObject.ToObject<RemoteFileInfo>();
            return convertedRemoteFileInfo.fileByteArray;
        }
    }

No manual deserialisation required to bind the model

Actually if you do use string then the Web API model binder will do all of the work for you:

 [HttpPost]
 public string Test(RemoteFileInfo incomingInformation)
 {
      return incomingInformation.fileByteArray; //or whatever
 }

But, you will need to do some conversion from RemoteFileInfo (which is just a DTO and therefore should really never leave the API Controller) to another class if you really need fileByteArray to be an array of Byte[] before you process it.

You could write a custom model binder but I think it's simpler to accept the simpler model and convert explicitly.

Update

I think there may be a bit of confusion about my answer so I'll attempt to clarify my thinking and what I think is going on in this case.

  1. WebAPI Model Binder is looking at the POST data and then trying to see if it can fit it into the parameter type declared on the Action method. In the case that this is object then that will work and so that allows it to supply a non-null object instance to the method in the form of incomingInformation

  2. Your code is manually handling serialisation and obviously JsonConvert is able to handle converting a string to a Byte[]

  3. The reason why you are seeing failure when trying to declare incomingInformation as type RemoteFileInfo is because the default WebAPI Model Binder is not able to handle a conversion of string->Byte[] and therefore gives up trying to serialise the incoming POST Data into an instance of RemoteFileInfo and instead passes null to the method's incomingInformation parameter.

  4. At that point of course any attempt to refer to or perform any action on incomingInformation will fail with NullReferenceException and so on.

  5. The only way to get the Web API Model binder to sucessfully bind that model is a)change fileByteArray to a string (because it is a string on the wire) in which case my second code sample should just work, or b) write a custom Model Binder (personally, not a fan)

I could of course be totally wrong but since your code works fine for me using Fiddler with the chanes I mentioned, I suspect that the points above are the situation or that there is some other factor you haven't included here.

Stephen Byrne
  • 7,400
  • 1
  • 31
  • 51
  • Hi Stephen, thank you for your post. Actually, completely removing the FileByteArray causes the same issue. I am not having a problem serializing or deserializing the JSON; I am only having trouble receiving the POST data as a "RemoteFileInfo" rather than an object. In your example code, you have public string Test(object model); to translate my problem to your code is that I want to have public string Test(RemoteFileInfo model). The file byte array is not the problem since removing it gets the same results. – nismonster Feb 25 '15 at 00:45
  • @nismonster that's very odd because I observed no issues at all using your code and your data, except when the receiving type used Byte[] for `fileByteArray` - in that case the model variable would be `null` and I observed an exception at the ToString() call (because of the null model). I have noted this in the answer. – Stephen Byrne Feb 25 '15 at 08:27
0

Thanks to all who commented. It turns out the problem was something to do with using the JsonConvert.SerializeObject. Although in Fiddler the json string looked perfect, when on a whim I switched serialization methods to JavaScriptSerializer().Serialize, the RemoteFileInfo object is successfully passed and received (with full byte array in tact!)

Here is the final code of my request, which will allow the receiving app to have the RemoteFileInfo as the parameter type. Still not sure why the JsonConvert.Serialize won't work; the Newstonsoft package was installed on both the client and receiving app.

var httpWebRequest = (HttpWebRequest)WebRequest.Create(@"http://yyyy/api/xxx/uploadfiletoalfresco/");
            httpWebRequest.ContentType = "application/json";
            httpWebRequest.Method = "POST";

            using (var streamWriter = new StreamWriter(httpWebRequest.GetRequestStream()))
            {
                string json = new JavaScriptSerializer().Serialize(remoteFileInfo);
                streamWriter.Write(json);
            }

            var response = (HttpWebResponse)httpWebRequest.GetResponse();
            using (var streamReader = new StreamReader(response.GetResponseStream()))
            {
                var result = streamReader.ReadToEnd();
            }
nismonster
  • 61
  • 9