问题
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:
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.
回答2:
@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>>();
回答3:
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()
};
回答4:
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.
来源:https://stackoverflow.com/questions/26942514/multiple-calls-to-httpcontent-readasasync