38

Using Web API 2.2, suppose I want to read from HttpContent twice, each time as a different type.

await httpContent.LoadIntoBufferAsync(); //necessary to buffer content for multiple reads var X = await httpContent.ReadAsAsync<T>(); //read as first type var Y = await httpContent.ReadAsAsync<Dictionary<string, object>>(); //read as second type 

When I run the above code, X is a non-null instance of T while Y is null. If I switch the order, Y will be a non-null dictionary while X will be null. In other words, the second and subsequent calls to ReadAsAsync will always return null unless they're called with the same generic type parameter. Independently, either call to ReadAsAsync works as expected (even when needlessly calling LoadIntoBufferAsync).

This is unexpected to me - it seems that I should be able to read buffered content as differing types as many times as I want. If I add another line:

var Z = await httpContent.ReadAsString(); 

The result is Z will be a non-null string, no matter the order of assignment to X, Y, Z.

So how come this happens, and why can't I read from HttpContent using ReadAsAsync with multiple types?

1
  • Note: You might wonder why I'm doing this. I'm trying to work out a simple method for determining which properties were ultimately bound to T in order to implement partial updates / PATCH in Web API. The second round of reading to a dictionary gives me a list of keys (property names) to overwrite with the submitted data. I am aware of the OData Delta<T> class, but unfortunately it doesn't appear to work correctly outside of OData controllers. stackoverflow.com/questions/15561874/… Commented Nov 15, 2014 at 4:25

4 Answers 4

38

@Peter is correct. If you want to read again and again, you would probably want to read as stream and seek to beginning every time you read the stream. But then if you want to do what do you now but get the second read working, you can seek to the beginning of the stream, after the first read, like this.

await httpContent.LoadIntoBufferAsync(); var X = await httpContent.ReadAsAsync<T>(); Stream stream = await httpContent.ReadAsStreamAsync(); stream.Seek(0, SeekOrigin.Begin); var Y = await httpContent.ReadAsAsync<Dictionary<string, object>>(); 
Sign up to request clarification or add additional context in comments.

3 Comments

Interesting. So the Stream returned by ReadAsStreamAsync() is in fact the underlying stream for the data? And seeking that stream actually affects any other method of accessing the data from the HttpContent object? Sure would be nice if Microsoft would document that. As it is, it smells a bit of "implementation leakage" and I worry that the technique could wind up broken in the future. :(
+1 this is an interesting observation. I wonder, but have not tried, if the implementation intends to support reading multiple types sequentially from the request (instead of reading the entire request as different types). E.g the request stream could be a sequence of [Type A][Type B][Type C]...
For those worrying about the seekability: "However, if you want to be able to read the content multiple times then you can use the LoadIntoBufferAsync method to do that. This will cause the content to get read into an internal buffer so that it can consumed multiple times without retrieving it again over the network." (Source: blogs.msdn.microsoft.com/henrikn/2012/02/17/…)
16

The documentation is sparse on the question, but it's not too surprising to me that HttpContent acts like a stream, in that you can read it just once. Pretty much every method in .NET with "read" in the name acts this way.

I don't have any idea why it even makes sense to read the same data multiple times, interpreting it differently each time, except possibly for debugging purposes. Your example seems contrived to me. But if you really want to do that, you can try ReadAsStreamAsync(), which you can then read from the Stream directly, resetting the Position property to 0 each time you want to read it again, or ReadAsByteArrayAsync(), giving you a byte array you can read from as many times as you like.

Of course, you'll have to use the formatters explicitly to convert to the desired type. But that shouldn't be too much of an impediment.

6 Comments

Regarding the example being contrived, do you have a suggestion for how best to determine which properties were actually provided when using a single call to ReadAsAsync?
I'm not sure what you mean by "properties" here. But if you mean you are simply trying to determine what type of object was provided, it seems to me that that ought to be defined by the context. Unfortunately, there's no context in this question, so I can't really offer any advice as to how that might work.
By properties I mean I'd like to know which properties of my model were actually submitted in the request, since it's possible that only a subset of them were actually sent to be updated. While it doesn't explain why ReadAsAsync fails, your suggestion to read as a byte array and read from that explicitly using formatters works as expected. Thanks.
I see, I think. So you're allowing the remote end to transmit just part (i.e. a proper subset of the properties) of an object, and you want to copy just the properties actually included in the data. I guess I would've hoped that would be supportable via nullable values, but I think I understand how the dictionary is being used (i.e. you don't actually care about the values at that point, just the keys, since you're deserializing those elsewhere).
It could be attempted via nullable values, but then you don't know if the client omitted the property (i.e. no update) or transmitted null (i.e. update to null) - in both cases the model property will simply be null.
|
6

I got a working solution for this, however it requires to use the overload of ReadAsync that explicitly takes a list of media formatters. It looks pretty much as a nasty hack but it works.

Indeed, HttpContent acts as a stream under the hoods, and once it's read by the formatter, it is not automatically rewinded. But there is a way to do a manual rewind, and here's how this can be done.

First, create a decorator for media type formatters as follows:

public class RewindStreamFormatterDecorator : MediaTypeFormatter { private readonly MediaTypeFormatter formatter; public RewindStreamFormatterDecorator(MediaTypeFormatter formatter) { this.formatter = formatter; this.SupportedMediaTypes.Clear(); foreach(var type in formatter.SupportedMediaTypes) this.SupportedMediaTypes.Add(type); this.SupportedEncodings.Clear(); foreach(var encoding in formatter.SupportedEncodings) this.SupportedEncodings.Add(encoding); } public override bool CanReadType(Type type) { return formatter.CanReadType(type); } public override Task<object> ReadFromStreamAsync( Type type, Stream readStream, HttpContent content, IFormatterLogger formatterLogger, CancellationToken cancellationToken) { var result = formatter.ReadFromStreamAsync (type, readStream, content, formatterLogger, cancellationToken); readStream.Seek(0, SeekOrigin.Begin); return result; } //There are more overridable methods but none seem to be used by ReadAsAsync } 

Second, convert the list of formatters to a list of decorated formatters:

formatters = formatters.Select(f => new RewindStreamFormatterDecorator(f)).ToArray(); 

...and now you can invoke ReadAsAsync as many times as you want:

var X = await httpContent.ReadAsAsync<T>(formatters); var Y = await httpContent.ReadAsAsync<Dictionary<string, object>>(formatters); 

I used this solution in a custom model binder so I got the formatters collection from the instance of HttpParameterDescriptor passed to the constructor. You will probably have one such collection at hand from somewhere in the execution context, but if not, just create a default collection the same way as ASP.NET does:

formatters = new MediaTypeFormatter[] { new JsonMediaTypeFormatter(), new XmlMediaTypeFormatter(), new FormUrlEncodedMediaTypeFormatter() }; 

Comments

3

You should read the contents into a string, then deserialize that into whatever datatypes you need:

var content = await httpContent.ReadAsString(); // read as first type var X = JsonConvert.DeserializeObject<T>(content); // read as second type var Y = JsonConvert.DeserializeObject<Dictionary<string, object>>(content); 

it doesn't make any sense to read the contents asynchronously twice.

1 Comment

This works only if you know beforehand that the content in JSON.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.