I need to post HTTP API requests whose bodies are given as List<ArraySegment<byte>>
(the usual fixed header + variable middle + fixed footer stuff). The .NET Socket can send List<ArraySegment<byte>>
since time immemorial but I cannot find the analogous functionality in the context of the .NET HttpClient.
In order to send a body, HttpClient
needs an HttpRequestMessage where the body to be sent is given in the Content property (of type HttpContent).
Neither HttpContent
nor any of its descendants has a constructor that takes a List<ArraySegment<byte>>
or something similar; they only deal with a single array slices. The same holds for MemoryStream, which might otherwise be used via one of the HttpContent
descendants.
At this point the outlook seems bleak: I could either consolidate the body fragments into a single byte buffer for sending, or implement HttpContent
on top of List<ArraySegment<byte>>
.
Neither option seems particularly attractive. A message body can have tens of megabytes, so needless copying will put unnecessary load on the memory subsystem and drag down performance (the server is on the same physical host and is reached either via the loopback device or via virtual NICs). HttpContent
and MemoryStream
have needlessly large and complex APIs that seem challenging to implement.
Is there an easier way of posting a List<ArraySegment<byte>>
? If not, does someone know from experience which route is the less painful one - implementing HttpContent
or implementing MemoryStream
? (The latter has a slightly larger API but it seems more straightforward and easier to understand.)
P.S.: to put things in context: I'm redesigning a tool we are using for measuring the performance of an API server as well as for function and stress testing; it is currently based on the .NET Socket
API but the use of plain TCP sockets effectively limits it to HTTP 1.1. This is where HttpClient
comes in.
The good news is that implementing HttpContent for use in an HttpRequestMessage is simple and straightforward; only TryComputeLength() and the three functions for serialising the content to the (TCP) stream need to be implemented.
The mind-bending - and mostly undocumented - system of functions around LoadIntoBuffer()
, CreateReadStream()
and friends does not come into play, and a semi-official comment in the HttpContent
documentation says as much.
Here's a minimal working example implementation:
class ArraySegmentsContent : HttpContent
{
private readonly List<ArraySegment<byte>> m_segments;
public ArraySegmentsContent (List<ArraySegment<byte>> segments)
{
m_segments = segments;
}
protected override Task SerializeToStreamAsync (Stream stream, TransportContext? context)
{
return SerializeToStreamAsync(stream, context, default);
}
protected override async Task SerializeToStreamAsync (Stream stream, TransportContext? context, CancellationToken cancellationToken)
{
foreach (var segment in m_segments)
{
await stream.WriteAsync(segment, cancellationToken);
}
}
protected override void SerializeToStream (Stream stream, TransportContext? context, CancellationToken cancellationToken)
{
foreach (var segment in m_segments)
{
if (cancellationToken.IsCancellationRequested)
{
return;
}
stream.Write(segment);
}
}
protected override bool TryComputeLength (out long length)
{
length = m_segments.Sum(segment => (long) segment.Count);
return true;
}
}
The oddity that I mentioned is that fact that the async function that takes a cancellation token is implemented by HttpContent
in terms of the function that does not take such a token; the cancellation token gets dropped to the floor. Normally it would be the other way around: the basic implementation function would be the one that takes a cancellation token, and the function without a token would call it with CancellationToken.None
.
Among the missing bells and whistles is overriding the remaining virtuals to throw NotImplementedException
and the thorny issue around the mutability of List<>
in the absence of any synchronisation/serialisation logic.
I used List<ArraySegment<byte>>
because it is what Socked.SendAsync() takes but more realistically it should be IReadOnlyCollection<ArraySegment<byte>>
or somesuch here, combined with a means for preventing alteration of the underlying byte buffers until the HTTP request has been sent.
With this little class I can represent a 30 MB request with 300 electronic prescriptions (the maximum allowed for the API in question) for perf/load/stress testing by referencing the meat of a single prescription 300 times, plus tiny interstitial segments for taking care of the framing and for giving each prescription a unique id. That's about 200 KB total instead of 30 MB, and of these 200 KB, 100 KB (the prescription) can be shared by all concurrent senders. This means that all the data fits neatly into the L2 caches of the CPUs involved, even when simulating 100 concurrent clients.