Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Multiple Calls to HttpContent ReadAsAsync

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?

like image 317
Michael Petito Avatar asked Nov 15 '14 04:11

Michael Petito


3 Answers

@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>>(); 
like image 184
Badri Avatar answered Sep 19 '22 13:09

Badri


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.

like image 38
Peter Duniho Avatar answered Sep 19 '22 13:09

Peter Duniho


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()
};
like image 20
Konamiman Avatar answered Sep 20 '22 13:09

Konamiman