Search code examples
c#filefile-uploaduwpdotnet-httpclient

HttpClient, implement progress when uploading File


I'm working on a UWP app that uploads files to a server using a custom API method. The file upload itself is functional, but I'm having trouble implementing progress tracking during the upload. I've tried several approaches, including creating a custom ProgressStreamContent class, but none seem to work

Here's the original API method without progress tracking:

public async Task<ApiResult<FileUploadResult>> UploadFile(string metadata, Stream content, string sessionId, string treeId, string nodeId,Action<long, long> progressCallback = null, CancellationToken? cancellationToken = null)
        {
            string url = $"{_httpHelper.GetFileUploadsBaseUrl()}/{sessionId}/{treeId}/{nodeId}";

            MultipartFormDataContent multipartContent = new MultipartFormDataContent
            {
                { new StringContent(metadata), "metadata" },
                { new StreamContent(content), "content" }

            };

            ApiResult<FileUploadResult> result = await _httpHelper.PostAsync<FileUploadResult>(url, multipartContent, cancellationToken);

            return result;

        }

And here's my attempt to add progress tracking using a custom ProgressStreamContent class:

    public class ProgressStreamContent : HttpContent
    {


        private const int defaultBufferSize = 5 * 4096;

        private HttpContent content;
        private int bufferSize;
        //private bool contentConsumed;
        private Action<long, long> progress;

        public ProgressStreamContent(HttpContent content, Action<long, long> progress) : this(content, defaultBufferSize, progress) { }

        public ProgressStreamContent(HttpContent content, int bufferSize, Action<long, long> progress)
        {
            if (content == null)
            {
                throw new ArgumentNullException("content");
            }
            if (bufferSize <= 0)
            {
                throw new ArgumentOutOfRangeException("bufferSize");
            }

            this.content = content;
            this.bufferSize = bufferSize;
            this.progress = progress;

            foreach (var h in content.Headers)
            {
                this.Headers.Add(h.Key, h.Value);
            }
        }

        protected override Task SerializeToStreamAsync(Stream stream, TransportContext context)
        {

            return Task.Run(async () =>
            {
                var buffer = new Byte[this.bufferSize];
                long size;
                TryComputeLength(out size);
                var uploaded = 0;


                using (var sinput = await content.ReadAsStreamAsync())
                {
                    while (true)
                    {
                        var length = sinput.Read(buffer, 0, buffer.Length);
                        if (length <= 0) break;

         
                        uploaded += length;
                        progress?.Invoke(uploaded, size);

                        stream.Write(buffer, 0, length);
                        stream.Flush();
                    }
                }
                stream.Flush();
            });
        }

        protected override bool TryComputeLength(out long length)
        {
            length = content.Headers.ContentLength.GetValueOrDefault();
            return true;
        }

        protected override void Dispose(bool disposing)
        {
            if (disposing)
            {
                content.Dispose();
            }
            base.Dispose(disposing);
        }
}

and here How I used it, I created DelegatingHandler to handle the progress

 public class ProgressMessageHandler : DelegatingHandler
    {
        private Action<long, long> _onUploadProgress;

        public event Action<long, long> HttpProgress
        {
            add => _onUploadProgress += value;
            remove => _onUploadProgress -= value;
        }
        public ProgressMessageHandler(HttpMessageHandler innerHandler) : base(innerHandler)
        {
        }
        protected async override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
        {
            var progressContent = new ProgressStreamContent(
                request.Content,
                4096,
                (sent, total) =>
                {
                    Console.WriteLine("Uploading {0}/{1}", sent, total);
                    OnUploadProgress(sent, total);
                });
            request.Content = progressContent;
            return await base.SendAsync(request, cancellationToken);
        }
        private void OnUploadProgress(long bytesTransferred, long totalBytes)
        {
            _onUploadProgress?.Invoke(bytesTransferred, totalBytes);
        }
    }

and update the Api function to be like this

        public async Task<ApiResult<FileUploadResult>> UploadFile(string metadata, Stream content, string sessionId, string treeId, string nodeId,Action<long, long> progressCallback = null, CancellationToken? cancellationToken = null)
        {
            string url = $"{_httpHelper.GetFileUploadsBaseUrl()}/{sessionId}/{treeId}/{nodeId}";

            MultipartFormDataContent multipartContent = new MultipartFormDataContent
            {
                { new StringContent(metadata), "metadata" },
                { new ProgressStreamContent(new StreamContent(content), progressCallback), "content" }

            };

            ApiResult<FileUploadResult> result = await _httpHelper.PostAsync<FileUploadResult>(url, multipartContent, cancellationToken);

            return result;

        }

I'm seeking guidance on how to correctly implement progress tracking during the file upload process in my UWP app. Any insights or alternative approaches would be greatly appreciated. Thank you!


Solution

  • I think you are over-engineering this. You can use the ProgressStream from this answer, then simply add an event handler to that.

    I've modified this to add the necessary async versions.

    public class ProgressStream : Stream
    {
        private Stream _input;
        private long _progress;
    
        public event Action<long, long>? UpdateProgress;
    
        public ProgressStream(Stream input)
        {
            _input = input;
        }
    
        public override void Flush() => _input.Flush();
    
        public override Task FlushAsync(CancellationToken cancellationToken = default) => _input.FlushAsync(cancellationToken);
    
        public override int Read(Span<byte> buffer)
        {
            int n = _input.Read(buffer, offset, count);
            _progress += n;
            UpdateProgress?.Invoke(_progress, _input.Length);
            return n;
        }
    
        public override async ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken = default)
        {
            int n = await _input.ReadAsync(buffer, cancellationToken);
            _progress += n;
            UpdateProgress?.Invoke(_progress, _input.Length);
            return n;
        }
    
        protected override void Dispose(bool disposing) => _input.Dispose();
    
        public override ValueTask DisposeAsync() => _input.DisposeAsync();
    
        public override void Write(byte[] buffer, int offset, int count) => throw new System.NotImplementedException();
    
        public override long Seek(long offset, SeekOrigin origin) => throw new System.NotImplementedException();
    
        public override void SetLength(long value) => throw new System.NotImplementedException();
    
        public override bool CanRead => true;
        public override bool CanSeek => false;
        public override bool CanWrite => false;
        public override long Length => _input.Length;
        public override long Position
        {
            get {  return _input.Position; }
            set {  throw new System.NotImplementedException();}
        }
    }
    

    Then you connect the event handler and just use a normal StreamContent.

    public async Task<ApiResult<FileUploadResult>> UploadFile(
      string metadata, Stream content, string sessionId, string treeId, string nodeId,
      Action<long, long> progressCallback = null, CancellationToken? cancellationToken = null)
    {
        var url = $"{_httpHelper.GetFileUploadsBaseUrl()}/{sessionId}/{treeId}/{nodeId}";
    
        using var progressStream = progressCallback != null ? new ProgressStream(content) : content;
        // for performance use original stream if no callback
        if (progressCallback != null)
            progressStream.UpdateProgress += progressCallback;
    
        using var multipartContent = new MultipartFormDataContent
        {
            { new StringContent(metadata), "metadata" },
            { new StreamContent(progressStream), "content" },
        };
    
        var result = await _httpHelper.PostAsync<FileUploadResult>(url, multipartContent, cancellationToken);
        return result;
    }