File: System\Net\Http\StreamContent.cs
Web Access
Project: src\src\libraries\System.Net.Http\src\System.Net.Http.csproj (System.Net.Http)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
 
namespace System.Net.Http
{
    public class StreamContent : HttpContent
    {
        private Stream _content;
        private int _bufferSize;
        private bool _contentConsumed;
        private long _start;
 
        public StreamContent(Stream content)
        {
            ArgumentNullException.ThrowIfNull(content);
 
            // Indicate that we should use default buffer size by setting size to 0.
            InitializeContent(content, 0);
 
            if (TryComputeLength(out long contentLength))
            {
                Headers.ContentLength = contentLength;
            }
        }
 
        public StreamContent(Stream content, int bufferSize)
        {
            ArgumentNullException.ThrowIfNull(content);
            ArgumentOutOfRangeException.ThrowIfNegativeOrZero(bufferSize);
 
            InitializeContent(content, bufferSize);
 
            if (TryComputeLength(out long contentLength))
            {
                Headers.ContentLength = contentLength;
            }
        }
 
        [MemberNotNull(nameof(_content))]
        private void InitializeContent(Stream content, int bufferSize)
        {
            _content = content;
            _bufferSize = bufferSize;
            if (content.CanSeek)
            {
                _start = content.Position;
            }
            if (NetEventSource.Log.IsEnabled()) NetEventSource.Associate(this, content);
        }
 
        protected override void SerializeToStream(Stream stream, TransportContext? context, CancellationToken cancellationToken)
        {
            Debug.Assert(stream != null);
            PrepareContent();
            // If the stream can't be re-read, make sure that it gets disposed once it is consumed.
            StreamToStreamCopy.Copy(_content, stream, _bufferSize, !_content.CanSeek);
        }
 
        protected override Task SerializeToStreamAsync(Stream stream, TransportContext? context) =>
            SerializeToStreamAsyncCore(stream, default);
 
        protected override Task SerializeToStreamAsync(Stream stream, TransportContext? context, CancellationToken cancellationToken) =>
            // Only skip the original protected virtual SerializeToStreamAsync if this
            // isn't a derived type that may have overridden the behavior.
            GetType() == typeof(StreamContent) ? SerializeToStreamAsyncCore(stream, cancellationToken) :
            base.SerializeToStreamAsync(stream, context, cancellationToken);
 
        private Task SerializeToStreamAsyncCore(Stream stream, CancellationToken cancellationToken)
        {
            Debug.Assert(stream != null);
            PrepareContent();
            return StreamToStreamCopy.CopyAsync(
                _content,
                stream,
                _bufferSize,
                !_content.CanSeek, // If the stream can't be re-read, make sure that it gets disposed once it is consumed.
                cancellationToken);
        }
 
        protected internal override bool TryComputeLength(out long length)
        {
            if (_content.CanSeek)
            {
                length = _content.Length - _start;
                return true;
            }
            else
            {
                length = 0;
                return false;
            }
        }
 
        protected override void Dispose(bool disposing)
        {
            if (disposing)
            {
                _content.Dispose();
            }
            base.Dispose(disposing);
        }
 
        protected override Stream CreateContentReadStream(CancellationToken cancellationToken)
        {
            SeekToStartIfSeekable();
            return new ReadOnlyStream(_content);
        }
 
        protected override Task<Stream> CreateContentReadStreamAsync()
        {
            SeekToStartIfSeekable();
            // Wrap the stream with a read-only stream to prevent someone from writing to the stream.
            return Task.FromResult<Stream>(new ReadOnlyStream(_content));
        }
 
        internal override Stream? TryCreateContentReadStream() =>
            GetType() == typeof(StreamContent) ? new ReadOnlyStream(_content) : // type check ensures we use possible derived type's CreateContentReadStreamAsync override
            null;
 
        internal override bool AllowDuplex => false;
 
        private void PrepareContent()
        {
            if (_contentConsumed)
            {
                // If the content needs to be written to a target stream a 2nd time, then the stream must support
                // seeking (e.g. a FileStream), otherwise the stream can't be copied a second time to a target
                // stream (e.g. a NetworkStream).
                if (_content.CanSeek)
                {
                    _content.Position = _start;
                }
                else
                {
                    throw new InvalidOperationException(SR.net_http_content_stream_already_read);
                }
            }
 
            _contentConsumed = true;
        }
 
        private void SeekToStartIfSeekable()
        {
            if (_content.CanSeek)
            {
                _content.Position = _start;
            }
        }
 
        private sealed class ReadOnlyStream : DelegatingStream
        {
            public ReadOnlyStream(Stream innerStream) : base(innerStream)
            {
            }
 
            public override bool CanWrite => false;
 
            public override void Flush() { }
 
            public override Task FlushAsync(CancellationToken cancellationToken) => Task.CompletedTask;
 
            public override void SetLength(long value) =>
                throw new NotSupportedException(SR.net_http_content_readonly_stream);
 
            public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback? callback, object? state) =>
                throw new NotSupportedException(SR.net_http_content_readonly_stream);
 
            public override void EndWrite(IAsyncResult asyncResult) =>
                throw new NotSupportedException(SR.net_http_content_readonly_stream);
 
            public override void Write(byte[] buffer, int offset, int count) =>
                throw new NotSupportedException(SR.net_http_content_readonly_stream);
 
            public override void Write(ReadOnlySpan<byte> buffer) =>
                throw new NotSupportedException(SR.net_http_content_readonly_stream);
 
            public override void WriteByte(byte value) =>
                throw new NotSupportedException(SR.net_http_content_readonly_stream);
 
            public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) =>
                throw new NotSupportedException(SR.net_http_content_readonly_stream);
 
            public override ValueTask WriteAsync(ReadOnlyMemory<byte> buffer, CancellationToken cancellationToken = default) =>
                throw new NotSupportedException(SR.net_http_content_readonly_stream);
 
            public override int WriteTimeout
            {
                get => throw new InvalidOperationException(SR.net_http_content_readonly_stream);
                set => throw new InvalidOperationException(SR.net_http_content_readonly_stream);
            }
        }
    }
}