10

I am transforming HttpContent into the following dto:

public class ContentDto 
{
     public string ContentType {get; set;}
     public string Headers {get; set; }
     public object Data { get; set; }

     public ContentDto(HttpContent content)
     {
          Headers = content.Headers.Flatten();
          // rest of the setup
     }
}

And am running some unit tests on it:

[Fact]
public void CanBuild()
{
     var content = new StringContent("some json", Enconding.UTF8, "application/json");
     var dto = new ContentDto(content);

     var contentHeaders = content.Headers.Flatten();

     Assert.Equal(contentHeaders, dto.Headers);
}

And that test fails since the Content-Length header is not being captured on my dto. However if I do:

[Fact]
public void CanBuild()
{
     var content = new StringContent("some json", Enconding.UTF8, "application/json");

     var contentHeaders = content.Headers.Flatten();

     var dto = new ContentDto(content);

     Assert.Equal(contentHeaders, dto.Headers);
}

The test passes and all headers are captured. Even more I also tried this:

 [Fact]
 public void CanBuild()
 {
     var content = new StringContent("some json", Enconding.UTF8, "application/json");

     var dto = new ContentDto(content);

     var contentHeaders = content.Headers.Flatten();

     var dto1 = new ContentDto(content);

     Assert.Equal(contentHeaders, dto.Headers);                
     Assert.Equal(contentHeaders, dto1.Headers);
}

and it fails since dto doesn't have the Content-Length header, but dto1 does. I even tried getting the headers inside a Factory-like method like this:

 public static ContentDto FromContent<T>(T content) where T : HttpContent
 {
      // same as the constructor
 }

to see if there was something special about the StringContent class regarding the Content-Length headers, but it made no difference, no matter if I used the constructor (which uses the base class HttpContent) or the generic method FromContent (using the actual StringContent in this case) the result was the same.

So my questions are:

Is that the intended behavior of HttpContent.Headers?
Are there some headers specific to the actual HttpContent type?
What am I missing here?

Note: This is the code for the Flatten extension method:

 public static string Flatten(this HttpHeaders headers)
 {
      var data = headers.ToDictionary(h => h.Key, h => string.Join("; ", h.Value))
                        .Select(kvp => $"{kvp.Key}: {kvp.Value}");

      return string.Join(Environment.NewLine, data)
 }
Nkosi
  • 235,767
  • 35
  • 427
  • 472
Luiso
  • 4,173
  • 2
  • 37
  • 60
  • Order of items in ToDictionary is not guaranteed, does this produce same result `headers.ToDictionary(h => h.Key, h => string.Join("; ", h.Value)) .Orderby(x=>x.Key).Select(kvp => $"{kvp.Key}: {kvp.Value}")`? – Akash Kava Aug 02 '16 at 17:32
  • @AkashKava the issue is not about order. The problem is the `Content-Length` headers which is not always there. – Luiso Aug 02 '16 at 18:31
  • @Luiso, can you show a [mcve] so that your issue can be accurately replicated. This will assist in finding a solution to your problem. – Nkosi Aug 03 '16 at 03:10

2 Answers2

4

Your example is incomplete. I was only able to recreate your issue when I accessed the ContentLength property before calling the extension method. Somewhere in your code (most probably //rest of setup) you are either directly or indirectly calling that property which is most probably following a lazy loading pattern and it is then included in the header when next you call your extension method and it is included in the constructed string. They don't match because you are generating your manual string before accessing the content length property.

In the source code for HttpContentHeaders.ContentLength

public long? ContentLength
{
    get
    {
        // 'Content-Length' can only hold one value. So either we get 'null' back or a boxed long value.
        object storedValue = GetParsedValues(HttpKnownHeaderNames.ContentLength);

        // Only try to calculate the length if the user didn't set the value explicitly using the setter.
        if (!_contentLengthSet && (storedValue == null))
        {
            // If we don't have a value for Content-Length in the store, try to let the content calculate
            // it's length. If the content object is able to calculate the length, we'll store it in the
            // store.
            long? calculatedLength = _calculateLengthFunc();

            if (calculatedLength != null)
            {
                SetParsedValue(HttpKnownHeaderNames.ContentLength, (object)calculatedLength.Value);
            }

            return calculatedLength;
        }

        if (storedValue == null)
        {
            return null;
        }
        else
        {
            return (long)storedValue;
        }
    }
    set
    {
        SetOrRemoveParsedValue(HttpKnownHeaderNames.ContentLength, value); // box long value
        _contentLengthSet = true;
    }
}

you can see that if you did not explicitly set a content length then it will add it (lazy load) to the headers when you first try to access it.

This proves my original theory about it being added after you generated/flatten your string and then accessed the ContentLength property and explains the inconsistent enumeration.

Nkosi
  • 235,767
  • 35
  • 427
  • 472
  • I just doubled checked my code and I cannot see where I could be accessing the `Content-Length` property, at least not an evident one, however I might just be missing it. Thank you very much – Luiso Aug 03 '16 at 14:44
  • 1
    I noticed in your `ContentDto` you have a `Data` property. how and where do you populate that property. If you are reading the stream or string of the htto content, the source code shows that just before reading the stream it calls the content length in order to know how much to read. That might be your culprit. – Nkosi Aug 03 '16 at 15:00
  • yes, you are right, that's exactly what happened. You have been really helpful thank you very much – Luiso Aug 03 '16 at 16:07
0

It seems that the HttpContent class has a pretty strange behavior with the headers properties. Somehow the content length seems to be computed as it is stated here. It does not address your issue specifically, but you can make a test with a new httpContent object similar to the initial one. I am pretty sure that you`ll be able to get the content length without a problem.

Florin V
  • 346
  • 2
  • 13