【问题标题】:Multiple Calls to HttpContent ReadAsAsync多次调用 HttpContent ReadAsAsync
【发布时间】:2015-01-12 14:00:29
【问题描述】:

使用 Web API 2.2,假设我想从 HttpContent 读取两次,每次都是不同的类型。

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

当我运行上面的代码时,XT 的非空实例,而 Y 是空的。如果我切换顺序,Y 将是一个非空字典,而 X 将是空的。换句话说,对ReadAsAsync 的第二次和后续调用将始终返回 null,除非它们使用相同的泛型类型参数调用。独立地,对ReadAsAsync 的调用都按预期工作(即使在不必要地调用LoadIntoBufferAsync 时)。

这出乎我的意料——似乎我应该能够根据需要多次读取不同类型的缓冲内容。如果我添加另一行:

var Z = await httpContent.ReadAsString();

结果是Z 将是一个非空字符串,无论分配给X, Y, Z 的顺序如何。

那么为什么会发生这种情况,为什么我不能使用具有多种类型的ReadAsAsyncHttpContent 读取?

【问题讨论】:

  • 注意:您可能想知道我为什么要这样做。我正在尝试制定一种简单的方法来确定哪些属性最终绑定到T,以便在Web API 中实现部分更新/PATCH。第二轮阅读字典给了我一个键(属性名)列表,用提交的数据覆盖。我知道 OData Delta&lt;T&gt; 类,但不幸的是它在 OData 控制器之外似乎无法正常工作。 stackoverflow.com/questions/15561874/…

标签: c# .net asp.net-web-api


【解决方案1】:

关于这个问题的文档很少,但HttpContent 就像一个流,因为您只能阅读一次,这对我来说并不奇怪。 .NET 中几乎所有名称中带有“read”的方法都以这种方式运行。

我不知道为什么多次读取相同的数据甚至每次都以不同的方式解释它是有意义的,除非可能是出于调试目的。你的例子对我来说似乎是做作的。但是如果你真的想这样做,你可以试试ReadAsStreamAsync(),然后你可以直接从Stream读取,每次你想再次读取时将Position属性重置为0,或者ReadAsByteArrayAsync(),为您提供一个字节数组,您可以根据需要多次读取。

当然,您必须明确使用格式化程序来转换为所需的类型。但这不应该成为太大的障碍。

【讨论】:

  • 关于人为设计的示例,您对如何最好地确定在使用单个调用 ReadAsAsync 时实际提供了哪些属性有什么建议吗?
  • 我不确定这里的“属性”是什么意思。但是,如果您的意思是您只是试图确定提供了哪种类型的对象,那么在我看来,这应该由上下文定义。不幸的是,这个问题没有上下文,所以我真的无法就它的工作原理提供任何建议。
  • 通过属性我的意思是我想知道我的模型的哪些属性实际上是在请求中提交的,因为实际上可能只有其中的一部分被发送以进行更新。虽然它没有解释为什么 ReadAsAsync 失败,但您建议读取为字节数组并使用格式化程序显式读取该数组可以按预期工作。谢谢。
  • 我明白了,我想。因此,您允许远程端仅传输对象的一部分(即属性的适当子集),并且您只想复制数据中实际包含的属性。我想我希望可以通过可空值来支持它,但我想我理解字典的使用方式(即你实际上并不关心此时的值,只关心键,因为你正在反序列化其他地方的)。
  • 它可以通过可为空的值来尝试,但是你不知道客户端是省略了属性(即没有更新)还是传输了 null(即更新为 null)——在这两种情况下都是模型属性将简单地为空。
【解决方案2】:

@Peter 是正确的。如果您想一次又一次地阅读,您可能希望以流的形式阅读,并在每次阅读流时都从头开始。但是,如果您现在想做什么,但要让第二次读取正常工作,您可以在第一次读取之后寻找到流的开头,就像这样。

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>>();

【讨论】:

  • 有趣。那么ReadAsStreamAsync() 返回的Stream 实际上是数据的底层流?并且寻找该流实际上会影响从HttpContent 对象访问数据的任何其他方法?如果微软能记录那当然会很好。事实上,它闻起来有点“实现泄漏”的味道,我担心该技术将来可能会被破坏。 :(
  • +1 这是一个有趣的观察。我想知道,但没有尝试过,如果实现打算支持从请求中顺序读取多种类型(而不是将整个请求读取为不同的类型)。例如,请求流可以是 [Type A][Type B][Type C]... 的序列
  • 对于那些担心可搜索性的人:“但是,如果您希望能够多次读取内容,那么您可以使用 LoadIntoBufferAsync 方法来做到这一点。这将导致内容被读入一个内部缓冲区,以便它可以多次使用,而无需通过网络再次检索。” (来源:blogs.msdn.microsoft.com/henrikn/2012/02/17/…
【解决方案3】:

我为此找到了一个可行的解决方案,但是它需要使用ReadAsync 的重载,它明确采用媒体格式化程序列表。它看起来很像一个讨厌的 hack,但它确实有效。

确实,HttpContent 在底层充当流,一旦被格式化程序读取,它就不会自动倒带。但是有一种方法可以进行手动倒带,这是如何做到的。

首先,为媒体类型格式化程序创建一个装饰器,如下所示:

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
}

其次,将格式化程序列表转换为装饰格式化程序列表:

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

...现在您可以根据需要多次调用ReadAsAsync

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

我在自定义模型绑定器中使用了此解决方案,因此我从 HttpParameterDescriptor 的实例中获取了格式化程序集合,并将其传递给构造函数。您可能会从执行上下文中的某个地方获得一个这样的集合,但如果没有,只需像 ASP.NET 一样创建一个默认集合:

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

【讨论】:

    【解决方案4】:

    您应该将内容读入一个字符串,然后将其反序列化为您需要的任何数据类型:

    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);
    

    两次异步读取内容没有任何意义。

    【讨论】:

    • 这只有在您事先知道 JSON 中的内容时才有效。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-10-19
    • 2013-03-31
    • 2019-12-16
    • 2015-01-16
    • 2017-12-17
    相关资源
    最近更新 更多