HttpClient.PutAsJsonAsync<T>(requestUri, value) does not post value to controller

Topics: ASP.NET Web API, General
Sep 11, 2013 at 2:39 AM
Edited Sep 11, 2013 at 8:20 PM

HttpClient.PutAsJsonAsync<T>(string requestUri, T value) does not post value to controller.

If the passed JSON string values are not string or null, the JSON object is not passed to the controller.

This works
{"Id":"0","GenreId":"5","ArtistId":"5","Title":"555","Price":"5.99","AlbumArtUrl":"","RowVersion":"AAAAAAAAqqo=","Artist":null,"Genre":null}

This dos not work
{"Id":0,"GenreId":5,"ArtistId":5,"Title":"555","Price":5.99,"AlbumArtUrl":"","RowVersion":"AAAAAAAAqqo=","Artist":null,"Genre":null,"Carts":[]}

Problem
"Id":0
"ArtistId":5
"Price":5.99
"Carts":[]

Not getting the value if called from console app. It works when called from Kendo UI Grid Update (javascript ajax).
  • value OK (Kendo) - All properties are strings
  • value null (Console App) - Some properties are numbers. Additional header parameter - Expect: 100-continue
Client - Console App
var handler = new WebRequestHandler { UseProxy = false };

using (var client = new HttpClient(handler) { BaseAddress = new Uri(baseAddress)} )
{
    client.DefaultRequestHeaders.Accept.ParseAdd("application/json, text/javascript, */*; q=0.01");

    var response = await client.PutAsJsonAsync(request, value);

    response.EnsureSuccessStatusCode();
    if (response.IsSuccessStatusCode)
    {
        var result = await response.Content.ReadAsStringAsync();
        dynamic dataObject = JObject.Parse(result);

        var entity = JsonConvert.DeserializeObject<TEntity>(dataObject.ToString());
    }
}
Server - OData AsyncEntitySetController - It works when called from Kendo UI Grid Update (javascript ajax)
protected override async Task<T> UpdateEntityAsync(int key, T value)
{
}
Microsoft.AspNet.WebApi 5.0.0-rtm-130910
Microsoft.AspNet.WebApi.OData 5.0.0-rtm-130910
Microsoft.AspNet.WebApi.Client 5.0.0-rtm-130910
Newtonsoft.Json 5.0.6

Http Request Header. It works when called from Kendo UI Grid Update (javascript ajax).
PUT http://localhost:8080/odata/Album(1) HTTP/1.1
Accept: application/json, text/javascript, /; q=0.01
Content-Type: application/json
X-Requested-With: XMLHttpRequest
Referer: http://127.0.0.1:8888/Home/Album
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
User-Agent: Mozilla/5.0 (Windows NT 6.3; WOW64; Trident/7.0; rv:11.0) like Gecko
Host: localhost:8080
Content-Length: 166
DNT: 1
Connection: Keep-Alive
Cache-Control: no-cache

{"Id":"1","GenreId":"4","ArtistId":"83","Title":"...And Justice For All","Price":"7.99","AlbumArtUrl":"/Content/Images/AlbumArt/CD16.png","RowVersion":"AAAAAAAARlg="}

Http Request Header. Call from console app. I tried to mimick the call that works.
PUT http://localhost:8080/odata/Album(1) HTTP/1.1
Connection: Keep-Alive
Accept: application/json, text/javascript, /; q=0.01
Accept-Language: en-US, en; q=0.5
Accept-Encoding: gzip, deflate
X-Requested-With: XMLHttpRequest
Cache-Control: no-cache
Referer: http://127.0.0.1:8888/odata/Album(1)
User-Agent: Mozilla/5.0 (Windows NT 6.3; WOW64; Trident/7.0; rv:11.0) like Gecko
DNT: 1
Content-Type: application/json; charset=utf-8
Host: localhost:8080
Content-Length: 155
Expect: 100-continue

{"Id":1,"GenreId":4,"ArtistId":83,"Title":"09/11/2013 02:34:40","Price":7.99,"AlbumArtUrl":"/Content/Images/AlbumArt/CD16.png","RowVersion":"AAAAAAAARmI="}
Sep 11, 2013 at 2:56 PM
What is the response that you get? 400 or 404 or 500? Also, can you enable IcludeErrorDetailPolicy to Always on Httpconfiguration and give us the exception message/trace if any?
Sep 11, 2013 at 5:29 PM
Edited Sep 11, 2013 at 5:35 PM
There is no error.

Not getting the value if called from console app. It works when called from Kendo UI Grid Update (javascript ajax).
  • value OK (Kendo) - All properties are strings
  • value null (Console App) - Some properties are numbers. Additional header parameter - Expect: 100-continue
Sep 11, 2013 at 6:09 PM
Edited Sep 11, 2013 at 8:22 PM
The POST not submitting the value either. Working with Kendo UI grid (javascript ajax)

If the passed JSON string values are not string or null, the JSON object is not passed to the controller.

This works
{"Id":"0","GenreId":"5","ArtistId":"5","Title":"555","Price":"5.99","AlbumArtUrl":"","RowVersion":"AAAAAAAAqqo=","Artist":null,"Genre":null}

This dos not work
{"Id":0,"GenreId":5,"ArtistId":5,"Title":"555","Price":5.99,"AlbumArtUrl":"","RowVersion":"AAAAAAAAqqo=","Artist":null,"Genre":null,"Carts":[]}

Problem
"Id":0
"ArtistId":5
"Price":5.99
"Carts":[]

Client - Console App
var handler = new WebRequestHandler { AllowAutoRedirect = false, UseProxy = false };

using (var client = new HttpClient(handler) {BaseAddress = new Uri(baseAddress)})
{
    client.DefaultRequestHeaders.Accept.ParseAdd("application/json, text/javascript, */*; q=0.01");

    var response = await client.PostAsJsonAsync(request, value);

    response.EnsureSuccessStatusCode();
    if (response.IsSuccessStatusCode)
    {
        var result = await response.Content.ReadAsStringAsync();
        dynamic dataObject = JObject.Parse(result);

        var entity = JsonConvert.DeserializeObject<TEntity>(dataObject.ToString());
    }
}
Server - OData AsyncEntitySetController - It works when called from Kendo UI Grid Insert (javascript ajax)
protected override async Task<Album> CreateEntityAsync(Album entity)
{
    if (entity == null)
    {
        throw new HttpResponseException(HttpStatusCode.BadRequest);
    }
    _db.Repository<Album>().Insert(entity);
    await _db.SaveAsync();
    return entity;
}
Http Header
POST http://localhost:8080/odata/Album HTTP/1.1
Accept: application/json, text/javascript, /; q=0.01
Content-Type: application/json; charset=utf-8
Host: localhost:8080
Content-Length: 105
Expect: 100-continue
Connection: Keep-Alive

{"Id":0,"GenreId":5,"ArtistId":5,"Title":"555","Price":5.99,"AlbumArtUrl":"","RowVersion":"AAAAAAAAqqo="}
Sep 11, 2013 at 7:28 PM
Edited Sep 11, 2013 at 8:22 PM

The PostAsJsonAsync and PutAsJsonAsync has a bug.

If I construct the data myself and all properties are strings it works.

If the passed JSON string values are not string or null, the JSON object is not passed to the controller.

This works
{"Id":"0","GenreId":"5","ArtistId":"5","Title":"555","Price":"5.99","AlbumArtUrl":"","RowVersion":"AAAAAAAAqqo=","Artist":null,"Genre":null}

This dos not work
{"Id":0,"GenreId":5,"ArtistId":5,"Title":"555","Price":5.99,"AlbumArtUrl":"","RowVersion":"AAAAAAAAqqo=","Artist":null,"Genre":null,"Carts":[]}

Problem
"Id":0
"ArtistId":5
"Price":5.99
"Carts":[]
var handler = new WebRequestHandler { UseProxy = false };

using (var client = new HttpClient(handler) { BaseAddress = new Uri(baseAddress)} )
{
    client.DefaultRequestHeaders.Accept.ParseAdd("application/json, text/javascript, */*; q=0.01");

    var value= "{\"Id\":\"1\",\"GenreId\":\"4\",\"ArtistId\":\"83\",\"Title\":\"...And Justice For All\",\"Price\":\"7.99\",\"AlbumArtUrl\":\"/Content/Images/AlbumArt/CD16.png\",\"RowVersion\":\"AAAAAAAARlg=\"}";
    var response = await client.PostAsync(request, new StringContent(value, Encoding.UTF8, "application/json"));

    response.EnsureSuccessStatusCode();
    if (response.IsSuccessStatusCode)
    {
        var result = await response.Content.ReadAsStringAsync();
        dynamic dataObject = JObject.Parse(result);

        var entity = JsonConvert.DeserializeObject<TEntity>(dataObject.ToString());
    }
}
If I pass a serialized json string from the object it dos not pass the value to the controller
var handler = new WebRequestHandler { UseProxy = false };

using (var client = new HttpClient(handler) { BaseAddress = new Uri(baseAddress)} )
{
    client.DefaultRequestHeaders.Accept.ParseAdd("application/json, text/javascript, */*; q=0.01");

    var entity= new Album { Id = 0, GenreId = 5, ArtistId = 5, Price = 5.99m, Title = "555", AlbumArtUrl = "", RowVersion = new byte[] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xAA, 0xAA } };
    string data = JsonConvert.SerializeObject(entity);
    var response = await client.PostAsync(request, new StringContent(data, Encoding.UTF8, "application/json"));

    response.EnsureSuccessStatusCode();
    if (response.IsSuccessStatusCode)
    {
        var result = await response.Content.ReadAsStringAsync();
        dynamic dataObject = JObject.Parse(result);

        var entity = JsonConvert.DeserializeObject<TEntity>(dataObject.ToString());
    }
}
POST http://localhost:8080/odata/Album HTTP/1.1
Accept: application/json, text/javascript, /; q=0.01
Content-Type: application/json; charset=utf-8
Host: localhost:8080
Content-Length: 105
Expect: 100-continue
Connection: Keep-Alive

{"Id":0,"GenreId":5,"ArtistId":5,"Title":"555","Price":5.99,"AlbumArtUrl":"","RowVersion":"AAAAAAAAqqo="}
Sep 11, 2013 at 9:38 PM
From what you explained, it looks like a deserialization error due to some format differences. We do call the action on the controller even if there deserialization errors with null for the parameter value and appropriate errors in the ModelState. Are you sure that your controller is not getting called at all? Or, if you have an action filter that sends back 400 if ModelState is invalid?
Sep 11, 2013 at 9:58 PM
Edited Sep 13, 2013 at 4:17 PM
There are no exceptions
The controller is getting called in both cases.
  1. This works (Controller called with proper entity value) Unfortunatly this is hand edited
    "Id":"0","GenreId":"5","ArtistId":"5","Title":"555","Price":"5.99","AlbumArtUrl":"","RowVersion":"AAAAAAAAqqo=","Artist":null,"Genre":null}
  2. This dos not work (Controller called with null entity value) Fortunately created by PutAsJsonAsync Microsoft developed code :-)
    "Id":0,"GenreId":5,"ArtistId":5,"Title":"555","Price":5.99,"AlbumArtUrl":"","RowVersion":"AAAAAAAAqqo=","Artist":null,"Genre":null,"Carts":[]}
  3. The controller is getting called in both cases.
  4. There are no exceptions.
  5. OData controller does not like (Album entity = null) the Microsoft developed HttpClient.PutAsJsonAsync() call.
  6. The "deserialization errors" that you mention is in a Microsoft developed code.
Sep 11, 2013 at 10:10 PM
Edited Sep 13, 2013 at 4:13 PM
See comprehensive answer below from danroth27.
Thank you Daniel.
Sep 11, 2013 at 10:11 PM
Edited Sep 13, 2013 at 4:13 PM
See comprehensive answer below from danroth27.
Thank you Daniel.
Sep 11, 2013 at 10:13 PM
Edited Sep 13, 2013 at 4:13 PM
See comprehensive answer below from danroth27.
Thank you Daniel.
Developer
Sep 11, 2013 at 10:25 PM
Could you share a repro?
Sep 11, 2013 at 10:49 PM
Edited Sep 13, 2013 at 5:12 PM
Your following conclusion
The only logical conclusion, proven by the FACTS, is that the Microsoft developed controller does NOT like (Album entity = null) the Microsoft developed HttpClient.PutAsJsonAsync() call.
is right.

Our web API OData story lacks the client piece for now i.e a client side OData formatter. HttpClient and the json.net formatter are not supported with the ODataController and EntitySetController as there are some differences in how json.net serializes an object vs what the OData protocol expects. And, our ODataMediaTypeFormatter can only be used on the server. You have to use the WCF Data services client to talk to an web API OData service.

There is a codeplex issue open for adding client support. You could show your support by voting for it. You can also contribute this feature and send us a pull request.. We would be more than happy to review it and take it.
Sep 11, 2013 at 10:55 PM
Edited Sep 13, 2013 at 4:13 PM
See comprehensive answer below from danroth27.
Thank you Daniel.
Coordinator
Sep 13, 2013 at 3:54 PM
There are really two JSON formats in play here: The AsyncEntitySetController only supports interpreting JSON data that is formatted using the various OData JSON formats. The JSON produced by JSON.NET (which is used by the default Web API JsonMediaTypeFormatter and the PostAsJsonAsync method) does not automatically conform to the OData standard – it’s really its own format. So you need to make sure you send valid OData-based JSON requests when calling your OData service.

While you could try and hand-craft OData requests using HttpClient and JSON.NET it would be difficult and error prone to do so. To call an OData service we actually recommend using the WCF Data Service Client instead of HttpClient. You can use WCF Data Service Client to consume your $metadata document and generate a strongly typed client for your OData service. You can find a sample that shows how to use the WCF Data Service Client with a Web API based OData service here.

We have had some discussions about providing a client-side OData formatter so that you could use HttpClient for these kinds of tests, but that feature hasn’t been implemented yet. Please do vote up the issue tracking this feature on our CodePlex site if this feature is important to you. Or, if you are interested in doing an implementation yourself we do accept external contributions and we would be happy to look at a pull request.

Hope this helps,

Daniel Roth