private async Task <byte[]> GetByteArrayAsyncCore(Task <HttpResponseMessage> getTask) { // Wait for the response message. using (HttpResponseMessage responseMessage = await getTask.ConfigureAwait(false)) { // Make sure it completed successfully. responseMessage.EnsureSuccessStatusCode(); // Get the response content. HttpContent c = responseMessage.Content; if (c != null) { #if NET46 return(await c.ReadAsByteArrayAsync().ConfigureAwait(false)); #else HttpContentHeaders headers = c.Headers; using (Stream responseStream = c.TryReadAsStream() ?? await c.ReadAsStreamAsync().ConfigureAwait(false)) { long? contentLength = headers.ContentLength; Stream buffer; // declared here to share the state machine field across both if/else branches if (contentLength.HasValue) { // If we got a content length, then we assume that it's correct and create a MemoryStream // to which the content will be transferred. That way, assuming we actually get the exact // amount we were expecting, we can simply return the MemoryStream's underlying buffer. buffer = new HttpContent.LimitMemoryStream(_maxResponseContentBufferSize, (int)contentLength.GetValueOrDefault()); await responseStream.CopyToAsync(buffer).ConfigureAwait(false); if (buffer.Length > 0) { return(((HttpContent.LimitMemoryStream)buffer).GetSizedBuffer()); } } else { // If we didn't get a content length, then we assume we're going to have to grow // the buffer potentially several times and that it's unlikely the underlying buffer // at the end will be the exact size needed, in which case it's more beneficial to use // ArrayPool buffers and copy out to a new array at the end. buffer = new HttpContent.LimitArrayPoolWriteStream(_maxResponseContentBufferSize); try { await responseStream.CopyToAsync(buffer).ConfigureAwait(false); if (buffer.Length > 0) { return(((HttpContent.LimitArrayPoolWriteStream)buffer).ToArray()); } } finally { buffer.Dispose(); } } } #endif } // No content to return. return(Array.Empty <byte>()); } }
/// <summary>Copies a source stream to a LimitMemoryStream by writing directly to the LimitMemoryStream's buffer.</summary> /// <param name="source">The source stream from which to copy.</param> /// <param name="destination">The destination LimitMemoryStream to write to.</param> /// <param name="bufferSize">The size of the buffer to allocate if one needs to be allocated.</param> /// <param name="disposeSource">Whether to dispose of the source stream after the copy has finished successfully.</param> private static async Task CopyAsyncToPreSizedLimitMemoryStream(Stream source, HttpContent.LimitMemoryStream destination, int bufferSize, bool disposeSource) { // When a LimitMemoryStream is constructed to represent a response with a particular ContentLength, its // Capacity is set to that amount in order to pre-size it. We can take advantage of that in this copy // by handing the destination's pre-sized underlying byte[] to the source stream for it to read into // rather than creating a temporary buffer, copying from the source into that, and then copying again // from the buffer into the LimitMemoryStream. long capacity = destination.Capacity; Debug.Assert(capacity > 0, "Caller should have checked that there's capacity"); // Get the initial length of the stream. When the length of a LimitMemoryStream is increased, the newly available // space is zero-filled, either due to allocating a new array or due to an explicit clear. As a result, we can't // write into the array directly and then increase the length to the right size afterward, as doing so will overwrite // all of the data newly written. Instead, we need to increase the length to the capacity, write in our data, and // then subsequently trim back the length to the end of the written data. long startingLength = destination.Length; if (startingLength < capacity) { destination.SetLength(capacity); } int bytesRead; try { // Get the LimitMemoryStream's buffer. ArraySegment <byte> entireBuffer; bool gotBuffer = destination.TryGetBuffer(out entireBuffer); Debug.Assert(gotBuffer, "Should only be in CopyAsyncToMemoryStream if we were able to get the buffer"); Debug.Assert(entireBuffer.Offset == 0, "LimitMemoryStream's are only constructed with a 0-offset"); Debug.Assert(entireBuffer.Count == entireBuffer.Array.Length, $"LimitMemoryStream's buffer count {entireBuffer.Count} should be the same as its length {entireBuffer.Array.Length}"); // While there's space remaining in the destination buffer, do another read to try to fill it. // Each time we read successfully, we update the position of the destination stream to be // at the end of the data read. int spaceRemaining = (int)(entireBuffer.Array.Length - destination.Position); while (spaceRemaining > 0) { // Read into the buffer bytesRead = await source.ReadAsync(entireBuffer.Array, (int)destination.Position, spaceRemaining).ConfigureAwait(false); if (bytesRead == 0) { DisposeSource(disposeSource, source); return; } destination.Position += bytesRead; spaceRemaining -= bytesRead; } } finally { // Now that we're done reading directly into the buffer, if we previously increased the length // of the stream, set it be at the end of the data read. if (startingLength < capacity) { destination.SetLength(destination.Position); } } // A typical case will be that we read exactly the amount requested. This means that the next // read will likely return 0 bytes, but we need to try to do the read to know that, which means // we need a buffer to read into. Use a cached single-byte array to do a read for 1-byte. // Ideally this read returns 0, and we're done. byte[] singleByteArray = RentCachedSingleByteArray(); bytesRead = await source.ReadAsync(singleByteArray, 0, 1).ConfigureAwait(false); if (bytesRead == 0) { ReturnCachedSingleByteArray(singleByteArray); DisposeSource(disposeSource, source); return; } // The read actually returned data, which means there was more data available then // the capacity of the LimitMemoryStream. This is likely an error condition, but // regardless we need to finish the copy. First, we write out the byte we read... await destination.WriteAsync(singleByteArray, 0, 1).ConfigureAwait(false); ReturnCachedSingleByteArray(singleByteArray); // ...then we fall back to doing the normal read/write loop. await CopyAsyncAnyStreamToAnyStreamCore(source, destination, bufferSize, disposeSource).ConfigureAwait(false); }
/// <summary>Copies the source stream from its current position to the destination stream at its current position.</summary> /// <param name="source">The source stream from which to copy.</param> /// <param name="destination">The destination stream to which to copy.</param> /// <param name="bufferSize">The size of the buffer to allocate if one needs to be allocated.</param> /// <param name="disposeSource">Whether to dispose of the source stream after the copy has finished successfully.</param> public static Task CopyAsync(Stream source, Stream destination, int bufferSize, bool disposeSource) { Debug.Assert(source != null); Debug.Assert(destination != null); Debug.Assert(bufferSize > 0); try { // If the source is a MemoryStream from which we can extract the internal buffer, // then we can perform the copy in a single write operation. ArraySegment <byte> sourceBuffer; MemoryStream sourceMemoryStream = source as MemoryStream; if (sourceMemoryStream != null && sourceMemoryStream.TryGetBuffer(out sourceBuffer)) { // It's possible source derives from MemoryStream but has a bad override of Position. // If we get back a value that doesn't make sense, don't take the optimized path. long pos = source.CanSeek ? source.Position : -1; if (pos >= 0 && pos < sourceBuffer.Count) { // We need to copy from the current position, so if necessary update the buffer // based on the position of the stream. if (pos != 0) { sourceBuffer = new ArraySegment <byte>( sourceBuffer.Array, (int)checked (sourceBuffer.Offset + pos), (int)checked (sourceBuffer.Count - pos)); } // Update position to simulate reading all the data source.Position += sourceBuffer.Count; // Now write the buffer to the destination stream. This will complete synchronously if the // destination's WriteAsync completes synchronously, such as if destination is also a MemoryStream. // Thus we don't need to special case it. return(WriteToAnyStreamAsync(sourceBuffer, source, destination, disposeSource)); } } // The source is not a MemoryStream whose buffer we can use directly, but the destination might be our special // LimitMemoryStream, in which case if it's been pre-sized with a capacity, we can optimize the copy // by giving its buffer to the source directly, avoiding the need for an intermediate buffer. HttpContent.LimitMemoryStream destinationLimitStream = destination as HttpContent.LimitMemoryStream; if (destinationLimitStream != null) { // We primarily care about the case where the stream has been presized based on a Content-Length. // If capacity is 0, we have no buffer to copy to, so we skip. If capacity is <= the max size allowed, // then we can fill the capacity that's there, and if it's not enough (which should be very rare), // we'll just fall back to continuing with a read/write loop for any additional data. If the capacity // is greater than the max size, we don't want to copy to it, as if we use all of the capacity, we'll // end up exceeding the max, so we skip the optimization for that case. That case could happen if // there's no Content-Length, and the stream is written to by something before it's given to us, in which // case the growth-algorithm inside the MemoryStream could cause its capacity to grow beyond the MaxSize; // that case should also be extremely rare, and we don't care about it from an optimization perspective. int capacity = destinationLimitStream.Capacity; if (capacity > 0 && capacity <= destinationLimitStream.MaxSize) { return(CopyAsyncToPreSizedLimitMemoryStream(source, destinationLimitStream, bufferSize, disposeSource)); } } // If the source is a MemoryStream, at this point we know we can't get its buffer. But, since // we're about to allocate a new byte[] of length bufferSize, if the MemoryStream's Length is // no larger than that and we're at the beginning of the stream, we can just ask it for its array // and still do a single write to the target. if (sourceMemoryStream != null && sourceMemoryStream.CanSeek && sourceMemoryStream.Position == 0 && sourceMemoryStream.Length <= bufferSize) { var buffer = new ArraySegment <byte>(sourceMemoryStream.ToArray()); sourceMemoryStream.Position = buffer.Count; // Update position to simulate reading all the data return(WriteToAnyStreamAsync(buffer, sourceMemoryStream, destination, disposeSource)); } // No special-stream cases worked, so we need to fall back to doing a normal copy, involving // allocating a byte[] of length bufferSize. However, if we don't need to dispose of the source, // then there's no work to be performed after the copy operation finishes, and we can simply delegate // to the source's CopyToAsync implementation to provide the best implementation the source can muster. // If we do need to dispose of the source, then using CopyToAsync would result in needing an extra // async method wrapper and its potential allocations, so we fall back to our own read/write loop that // does the copy and the disposal in a single async method. return(disposeSource ? CopyAsyncAnyStreamToAnyStreamCore(source, destination, bufferSize, disposeSource) : source.CopyToAsync(destination, bufferSize)); } catch (Exception e) { // For compatibility with the previous implementation, catch everything (including arg exceptions) and // store errors into the task rather than letting them propagate to the synchronous caller. return(Task.FromException(e)); } }
private async Task<byte[]> GetByteArrayAsyncCore(Task<HttpResponseMessage> getTask) { // Wait for the response message. using (HttpResponseMessage responseMessage = await getTask.ConfigureAwait(false)) { // Make sure it completed successfully. responseMessage.EnsureSuccessStatusCode(); // Get the response content. HttpContent c = responseMessage.Content; if (c != null) { HttpContentHeaders headers = c.Headers; using (Stream responseStream = await c.ReadAsStreamAsync().ConfigureAwait(false)) { long? contentLength = headers.ContentLength; Stream buffer; // declared here to share the state machine field across both if/else branches if (contentLength.HasValue) { // If we got a content length, then we assume that it's correct and create a MemoryStream // to which the content will be transferred. That way, assuming we actually get the exact // amount we were expecting, we can simply return the MemoryStream's underlying buffer. buffer = new HttpContent.LimitMemoryStream(_maxResponseContentBufferSize, (int)contentLength.GetValueOrDefault()); await responseStream.CopyToAsync(buffer).ConfigureAwait(false); if (buffer.Length > 0) { return ((HttpContent.LimitMemoryStream)buffer).GetSizedBuffer(); } } else { // If we didn't get a content length, then we assume we're going to have to grow // the buffer potentially several times and that it's unlikely the underlying buffer // at the end will be the exact size needed, in which case it's more beneficial to use // ArrayPool buffers and copy out to a new array at the end. buffer = new HttpContent.LimitArrayPoolWriteStream(_maxResponseContentBufferSize); try { await responseStream.CopyToAsync(buffer).ConfigureAwait(false); if (buffer.Length > 0) { return ((HttpContent.LimitArrayPoolWriteStream)buffer).ToArray(); } } finally { buffer.Dispose(); } } } } // No content to return. return Array.Empty<byte>(); } }