Matas Vaitkevicius Matas Vaitkevicius - 1 month ago 14
JSON Question

WebApi Formatter that is using ReadAsAsync and WriteToStreamAsync - Json gets truncated while xml works fine

I am working with

C#
,
WebAPI 2
and getting a weird behavior.

I have service that calls Controller and content can be sent sometimes as Json and others as XML mechanism that is used to make a call looks following:

using (var client = new HttpClient())
{
var pricingControllerUrl = CreateEndpoint(apiUrl);
using (var response = (int)request.Metadata.InputType >= 3 ? client.PostAsJsonAsync(pricingControllerUrl, request) : client.PostAsXmlWithSerializerAsync(pricingControllerUrl, request))
{
if (response.Result.IsSuccessStatusCode)
{
var session = response.Result.Content.ReadAsAsync<Session>(new List<MediaTypeFormatter>() { new XmlMediaTypeFormatter { UseXmlSerializer = true }, new JsonMediaTypeFormatter() }).Result;
return session;
}
}
}

public static class HttpExtensions
{
public static Task<HttpResponseMessage> PostAsXmlWithSerializerAsync<T>(this HttpClient client, string requestUri, T value)
{
return client.PostAsync(new Uri(requestUri), value,
new XmlMediaTypeFormatter { UseXmlSerializer = true }
);
}
}


On the receiving end (controller),

public async Task<IHttpActionResult> PostSession([FromBody] Session session)
{
//do the calculations
return Content(HttpStatusCode.OK, sessionResponse, new ReducedSessionFormatter(), this.Request.Content.Headers.ContentType);
}


response has to be reduced by removing some information just before it gets dispatched, formatter below is used to facilitate this:

public class ReducedSessionFormatter : MediaTypeFormatter
{
public ReducedSessionFormatter()
{
SupportedMediaTypes.Add(new MediaTypeHeaderValue("text/json"));
SupportedMediaTypes.Add(new MediaTypeHeaderValue("text/xml"));
SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/xml"));
}

public ReducedSessionFormatter(MediaTypeFormatter formatter) : base(formatter)
{
}

public override bool CanReadType(Type type)
{
return false;
}

public override bool CanWriteType(Type type)
{
return type.IsAssignableFrom(typeof (Session));
}

protected XDocument ReduceXml(XDocument doc)
{

//removing stuff from xml
return doc;
}

protected JObject ReduceJson(JObject serializedJson)
{
//removing stuff from json
return serializedJson;
}

public override Task WriteToStreamAsync(Type type, object value, Stream writeStream, HttpContent content, TransportContext transportContext)
{
if (content.Headers.ContentType.MediaType.Contains("xml"))
{
var doc = SerializeToXmlDocument(type, value).ToXDocument();
doc = ReduceXml(doc);

var settings = new XmlWriterSettings {Encoding = new UTF8Encoding(false)};
using (XmlWriter w = XmlWriter.Create(writeStream, settings))
{
doc.Save(w);
}
}
else
{
var json = new JavaScriptSerializer().Serialize(value);
var serializedJson = (JObject)JsonConvert.DeserializeObject(json);
var serializedJsonString = ReduceJson(serializedJson).ToString(Newtonsoft.Json.Formatting.None);
var writer = new StreamWriter(writeStream);
writer.Write(serializedJsonString);
}

var tcs = new TaskCompletionSource<object>();
tcs.SetResult(null);
return tcs.Task;
}

public XmlDocument SerializeToXmlDocument(Type type, object value)
{
var serializer = new XmlSerializer(type);

XmlDocument xmlDocument = null;

using (var memoryStream = new MemoryStream())
{
serializer.Serialize(memoryStream, value);
memoryStream.Position = 0;
using (var xtr = XmlReader.Create(memoryStream, new XmlReaderSettings {IgnoreWhitespace = true}))
{
xmlDocument = new XmlDocument();
xmlDocument.Load(xtr);
}
}

return xmlDocument;
}
}

public static class XmlExtensions
{
public static XDocument ToXDocument(this XmlDocument xmlDocument)
{
using (var nodeReader = new XmlNodeReader(xmlDocument))
{
nodeReader.MoveToContent();
return XDocument.Load(nodeReader);
}
}
}
public static class JsonExtensions
{
public static bool IsNullOrEmpty(this JToken token)
{
return (token == null) ||
(token.Type == JTokenType.Array && !token.HasValues) ||
(token.Type == JTokenType.Object && !token.HasValues) ||
(token.Type == JTokenType.String && token.ToString() == String.Empty) ||
(token.Type == JTokenType.Null);
}
}


When aggregated in formatter both xml and json are valid and fine and if data is sent back without using formatter everything works

Weird stuff: when I am using formatter and send back Json it gets truncated independent from length of it. Even very tiny objects (less than 10k length) get cut off always at same place for same object but at different length for different objects, and only for json and work just fine for xml...

it also fails if json is not being toStringed like:

var writer = new StreamWriter(writeStream);
writer.Write(ReduceJson(serializedJson));


I have added minimal solution to show the issue

What is going on here? Why using formatter truncates response content for Json but not XML?

Answer

Ok I have found what the issue was it was usage of Task .Result; in

var session = response.Result.Content.ReadAsAsync<Session>(new List<MediaTypeFormatter>() { new XmlMediaTypeFormatter { UseXmlSerializer = true }, new JsonMediaTypeFormatter() }).Result;

(and some other mechanisms that are not listed in the question) that should have been awaited.

Once replaced .Results with await and marked all methods in chain as async - I started getting full responses.