HttpWebRequest download file and progress bar - c#

I searched the net but couldn't find any way to get progress while downloading file with HttpWebRequest. Does this class support progress at all? Any link, tutorial, hint would be greatly appreciated.
Thanks.
P.S. Here's the code...
private static Task<HttpResponse> MakeAsyncRequest(string requestString)
{
var request = (HttpWebRequest)WebRequest.Create(requestString);
Task<WebResponse> requestTask = Task.Factory.FromAsync(
request.BeginGetResponse,
asyncResult => request.EndGetResponse(asyncResult),
null);
return requestTask.ContinueWith(t => ReadStreamFromResponce(t.Result));
}
private static HttpResponse ReadStreamFromResponce(WebResponse result)
{
var responseobject = new HttpResponse();
var response = (HttpWebResponse)result;
responseobject.StatusCode = (short)response.StatusCode;
if (!IsSuccess(responseobject.StatusCode))
return responseobject;
using (var responseStream = response.GetResponseStream())
using (var ms = new MemoryStream())
{
responseStream.CopyTo(ms);
responseobject.SetResponse(ms.ToArray());
return responseobject;
}
}

Instead of copying response stream in Synchronous, try to copy in Asynchronous and hook the Delegates in the below function, you should be able to get the progress in number of bytes downloaded, display it to progress bar....
public static void CopyToStreamAsync(this Stream source, Stream destination,
Action<Stream, Stream, Exception> completed, Action<uint> progress,
uint bufferSize, uint? maximumDownloadSize, TimeSpan? timeout)
{
byte[] buffer = new byte[bufferSize];
Action<Exception> done = exception =>
{
if (completed != null)
{
completed(source, destination, exception);
}
};
int maxDownloadSize = maximumDownloadSize.HasValue
? (int)maximumDownloadSize.Value
: int.MaxValue;
int bytesDownloaded = 0;
IAsyncResult asyncResult = source.BeginRead(buffer, 0, new[] { maxDownloadSize, buffer.Length }.Min(), null, null);
Action<IAsyncResult, bool> endRead = null;
endRead = (innerAsyncResult, innerIsTimedOut) =>
{
try
{
int bytesRead = source.EndRead(innerAsyncResult);
if (innerIsTimedOut)
{
done(new TimeoutException());
}
int bytesToWrite = new[] { maxDownloadSize - bytesDownloaded, buffer.Length, bytesRead }.Min();
destination.Write(buffer, 0, bytesToWrite);
bytesDownloaded += bytesToWrite;
if (!progress.IsNull() && bytesToWrite > 0)
{
progress((uint)bytesDownloaded);
}
if (bytesToWrite == bytesRead && bytesToWrite > 0)
{
asyncResult = source.BeginRead(buffer, 0, new[] { maxDownloadSize, buffer.Length }.Min(), null, null);
// ReSharper disable PossibleNullReferenceException
// ReSharper disable AccessToModifiedClosure
asyncResult.FromAsync((ia, isTimeout) => endRead(ia, isTimeout), timeout);
// ReSharper restore AccessToModifiedClosure
// ReSharper restore PossibleNullReferenceException
}
else
{
done(null);
}
}
catch (Exception exc)
{
done(exc);
}
};
asyncResult.FromAsync((ia, isTimeout) => endRead(ia, isTimeout), timeout);
}

Related

Get response for HttpCompletionOption ResponseHeadersRead

I am trying to get progress of an api along with the response. ResponseHeadersRead works fine to get the progress but I can't figure out why it doesn't return the response.
Download part
public async Task StartDownload()
{
_httpClient = new HttpClient { Timeout = TimeSpan.FromDays(1), };
using (var response = await _httpClient.GetAsync(_downloadUrl, HttpCompletionOption.ResponseHeadersRead))
{
await DownloadFileFromHttpResponseMessage(response);
string strResp = await response.Content.ReadAsStringAsync();
Debug.WriteLine(strResp); // Doesn't print anything
}
}
Reading Stream part
private async Task DownloadFileFromHttpResponseMessage(HttpResponseMessage response)
{
response.EnsureSuccessStatusCode();
var totalBytes = response.Content.Headers.ContentLength;
using (var contentStream = await response.Content.ReadAsStreamAsync())
{
await ProcessContentStream(totalBytes, contentStream);
}
}
The code is actually from another answer.
I am just not getting the response. If I use ResponseContentRead I get response but it defeats the purpose of progress.
EDIT
ProcessContentStream code - This part read the response as it comes bit by bit and posts the progress in TriggerProgressChanged
private async Task ProcessContentStream(long? totalDownloadSize, Stream contentStream)
{
var totalBytesRead = 0L;
var readCount = 0L;
var buffer = new byte[8192];
var isMoreToRead = true;
using (var fileStream = new FileStream(DestinationFilePath, FileMode.Create, FileAccess.Write, FileShare.None, 8192, true))
{
do
{
var bytesRead = await contentStream.ReadAsync(buffer, 0, buffer.Length);
if (bytesRead == 0)
{
isMoreToRead = false;
//TriggerProgressChanged(totalDownloadSize, totalBytesRead);
continue;
}
await fileStream.WriteAsync(buffer, 0, bytesRead);
totalBytesRead += bytesRead;
readCount += 1;
if (readCount % 100 == 0)
{
//TriggerProgressChanged(totalDownloadSize, totalBytesRead);
}
}
while (isMoreToRead);
}
}
Post the progress
private void TriggerProgressChanged(long? totalDownloadSize, long totalBytesRead)
{
if (ProgressChanged == null)
return;
double? progressPercentage = null;
if (totalDownloadSize.HasValue)
progressPercentage = Math.Round((double)totalBytesRead / totalDownloadSize.Value * 100, 2);
ProgressChanged(totalDownloadSize, totalBytesRead, progressPercentage);
}
ProgressChanged is a delegate method.
Project link
Ok so I found the solution. I needed to read the file on which I wrote the bytes as it was coming in with a StreamReader. Reading file with StreamReader has been explained here.
private async Task DownloadFileFromHttpResponseMessage(HttpResponseMessage response)
{
response.EnsureSuccessStatusCode();
var totalBytes = response.Content.Headers.ContentLength;
using (var contentStream = await response.Content.ReadAsStreamAsync())
{
await ProcessContentStream(totalBytes, contentStream);
// Added code
char[] buffer;
using (StreamReader sr = new StreamReader(DestinationFilePath))
{
buffer = new char[(int)sr.BaseStream.Length];
await sr.ReadAsync(buffer, 0, (int)sr.BaseStream.Length);
}
Debug.WriteLine(new string(buffer)); // Now it prints the response
}
}

Stream CopyAsync and WriteAsync on network failures

I am doing a copy asynchronous operation on a file stream. I noticed that if an error happen during the operation, I don't receive any error exception.
Tested with a large file, in the middle of the copy operation, suddenly I close the network connection.
After some timeout the test concludes passed.
I Want to be able to capture the whatever error happen during the copy operation.
I copy below.. the code samples, just asking some help.
BR Alex
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using static Microsoft.VisualStudio.TestTools.UnitTesting.Assert;
using static System.Console;
namespace CopyAsync
{
[TestClass]
public class UnitTest
{
public int BufferSize = 10;
[TestMethod]
public void CopyFileAsyncSouldCopyFile()
{
BufferSize = 10;
const string source = #"..\..\UnitTest.cs";
var destination = Path.GetRandomFileName();
WriteLine($"Start...");
var task = CopyAsync(source, destination, action: (total) => WriteLine($"Copying... {total}"));
var bytes = task.Result;
WriteLine($"Bytes copied... {bytes}");
IsTrue(File.Exists(destination));
AreEqual((new FileInfo(source)).Length, bytes);
File.Delete(destination);
}
[TestMethod]
public void CopyFileAsyncCancelledSouldCancelCopyFile()
{
BufferSize = 10;
const string source = #"..\..\UnitTest.cs";
var destination = Path.GetRandomFileName();
var cts = new CancellationTokenSource();
WriteLine($"Start...");
var task = CopyAsync(source, destination, cts.Token,
(total) =>
{
WriteLine($"Copying... {total}");
if (total > 1677)
return;
cts.Cancel();
WriteLine($"Canceled...");
});
try
{
var bytes = task.Result; // exception WILL BE thrown here
WriteLine($"Bytes copied... {bytes}"); // WON'T BE executed
}
catch (AggregateException ex) when (ex.InnerException.GetType() == typeof(TaskCanceledException))
{
WriteLine($"TaskCanceledException...");
File.Delete(destination);
}
}
[TestMethod]
// Exception not captured
// missed: System.AggregateException: One or more errors occurred. ---> System.IO.IOException: The network path was not found.
public void CopyFileAsyncNetworkErrorShouldFail()
{
const string source = #"..\..\verybigfile.iso";
var destination = Path.GetRandomFileName();
BufferSize = 4096;
WriteLine($"Start...");
var task = CopyAsync(source, destination, action: (total) => WriteLine($"Copying... {total}"));
var bytes = task.Result; // exception WON'T BE thrown here
WriteLine($"Bytes copied... {bytes}"); // WILL BE executed
}
public async Task<int> CopyAsync(string input, string output, CancellationToken token = default(CancellationToken), Action<long> action = null)
{
using (var source = new FileStream(input, FileMode.Open, FileAccess.Read, FileShare.Read, BufferSize, true))
using (var destination = new FileStream(output, FileMode.Create, FileAccess.Write, FileShare.None, BufferSize, true))
{
int bytes;
var total = 0;
var buffer = new byte[BufferSize];
while ((bytes = await source.ReadAsync(buffer, 0, buffer.Length, token)) > 0)
{
await destination.WriteAsync(buffer, 0, bytes, token);
total += bytes;
action?.Invoke(total);
}
return total;
}
}
}
}
Here I changed a while the code, but here is the working code..
(but indeed I can't figure why now is working, since is more or less the same workflow)
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using static Microsoft.VisualStudio.TestTools.UnitTesting.Assert;
using static System.Console;
namespace CopyAsync
{
[TestClass]
public class UnitTest
{
private int _bufferSize = 4096;
[TestMethod]
public void CopyFileAsyncSouldCopyFile()
{
_bufferSize = 100;
const string source = #"..\..\UnitTest.cs";
var destination = Path.GetRandomFileName();
WriteLine($"Start...");
var task = FileCopyAsync(source, destination, action: total => WriteLine($"Copying... {total}"));
var bytes = task.Result;
WriteLine($"Bytes copied... {bytes}");
IsTrue(File.Exists(destination));
AreEqual((new FileInfo(source)).Length, bytes);
File.Delete(destination);
}
[TestMethod]
public void CopyFileAsyncCancelledSouldCancelCopyFile()
{
_bufferSize = 100;
const string source = #"..\..\UnitTest.cs";
var destination = Path.GetRandomFileName();
var cts = new CancellationTokenSource();
WriteLine($"Start...");
var task = FileCopyAsync(source, destination,
token: cts.Token,
action: total =>
{
WriteLine($"Copying... {total}");
if (total < 2000)
return;
cts.Cancel();
WriteLine($"Canceled... at {total}");
});
try
{
task.Wait(); // exception WILL BE thrown here... PERFECT!!!!
}
catch (AggregateException ex) when (ex.InnerException.GetType() == typeof(TaskCanceledException))
{
WriteLine($"TaskCanceledException...");
File.Delete(destination);
}
}
[TestMethod]
public void CopyFileAsyncNetworkErrorShouldFail()
{
_bufferSize = 4096;
const string source = #"\\server\sharedfolder\bigfile.iso"; // to test close network connection while copying...
var destination = Path.GetRandomFileName();
WriteLine($"Start...");
var task = FileCopyAsync(source, destination, action: total => WriteLine($"Copying... {total}"));
try
{
task.Wait(); // exception WILL BE thrown here... PERFECT!!!! more than PERFECT
}
catch (AggregateException ex) when (ex.InnerException.GetType() == typeof(IOException))
{
WriteLine($"IOException...");
File.Delete(destination);
}
}
// ##########################
public async Task<int> FileCopyAsync(string sourceFileName, string destFileName, bool overwrite = false, CancellationToken token = default(CancellationToken), Action<long> action = null)
{
if (string.Equals(sourceFileName, destFileName, StringComparison.InvariantCultureIgnoreCase))
throw new IOException($"Source {sourceFileName} and destination {destFileName} are the same");
using (var sourceStream = new FileStream(sourceFileName, FileMode.Open, FileAccess.Read, FileShare.Read, _bufferSize, true))
using (var destStream = new FileStream(destFileName, FileMode.Create, FileAccess.Write, FileShare.None, _bufferSize, true))
{
var bytesCopied = await StreamCopyAsync(sourceStream, destStream, token, action);
if (bytesCopied != (new FileInfo(sourceFileName)).Length)
throw new IOException($"Source {sourceFileName} and destination {destFileName} don't match");
return bytesCopied;
}
}
public async Task<int> StreamCopyAsync(Stream sourceStream, Stream destStream, CancellationToken token = default(CancellationToken), Action<long> action = null)
{
if (Equals(sourceStream, destStream))
throw new ApplicationException("Source and destination are the same");
using (var reg = token.Register(() => Close(sourceStream, destStream))) // disposes registration for token cancellation callback
{
int bytes;
var bytesCopied = 0;
var buffer = new byte[_bufferSize];
while ((bytes = await sourceStream.ReadAsync(buffer, 0, buffer.Length, token)) > 0)
{
if (token.IsCancellationRequested)
break;
await destStream.WriteAsync(buffer, 0, bytes, token);
bytesCopied += bytes;
action?.Invoke(bytesCopied);
}
return bytesCopied;
}
}
private static void Close(Stream source, Stream destination) // fires on token cancellation
{
source.Close();
destination.Close();
}
}
}

Saving downloaded zip file to local file

I'm trying to save a zip file stream that I've downloaded from a server.
Now I've the Stream But I'm not able to save to a file. here is my attempt =>
private async Task DownloadCompleted(Stream inputStream, CancellationToken ct)
{
var file = await _downloadFolder.CreateFileAsync(_productDescription.ProductFileName, CreationCollisionOption.ReplaceExisting, ct);
using (Stream str = await file.OpenAsync(FileAccess.ReadAndWrite, ct))
{
await inputStream.CopyToAsync(str);
}
}
I'm trying to do it Xamarin.Android Project, I'm not good at streams, Also some good pointer are highly appreciated.
Edit- here I got the stream
private async Task DownloadFileFromUrl(string url, CancellationToken ct)
{
try
{
var receivedBytes = 0;
using (var client = new WebClient())
using (var stream = await client.OpenReadTaskAsync(url))
{
var buffer = new byte[4096];
var totalBytes = int.Parse(client.ResponseHeaders[HttpResponseHeader.ContentLength]);
for (;;)
{
var bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length, ct);
if (bytesRead == 0)
{
await Task.Yield();
break;
}
receivedBytes += bytesRead;
if (_downloadProgressHandler != null)
{
_downloadProgressHandler((int)(((double)receivedBytes / totalBytes) * 100), false);
}
}
await DownloadCompleted(stream, ct);
}
}
catch (System.OperationCanceledException)
{
throw;
}
catch (Exception ex)
{
Analytics.AddHandledExceptionEvent(ex, "Ex-ProductDownloader-DownloadFileFromUrl");
throw new NetworkNotAvailableException("");
}
}

Progress bar with HttpClient

i have a file downloader function:
HttpClientHandler aHandler = new HttpClientHandler();
aHandler.ClientCertificateOptions = ClientCertificateOption.Automatic;
HttpClient aClient = new HttpClient(aHandler);
aClient.DefaultRequestHeaders.ExpectContinue = false;
HttpResponseMessage response = await aClient.GetAsync(url);
InMemoryRandomAccessStream randomAccessStream = new InMemoryRandomAccessStream();
// To save downloaded image to local storage
var imageFile = await ApplicationData.Current.LocalFolder.CreateFileAsync(
filename, CreationCollisionOption.ReplaceExisting);
var fs = await imageFile.OpenAsync(FileAccessMode.ReadWrite);
DataWriter writer = new DataWriter(fs.GetOutputStreamAt(0));
writer.WriteBytes(await response.Content.ReadAsByteArrayAsync());
await writer.StoreAsync();
//current.image.SetSource(randomAccessStream);
writer.DetachStream();
await fs.FlushAsync();
How can i realize progress bar functionality?
Maybe i can get the writers bytes written so far? Or something?
P.S. I cant use DownloadOperation(Background transferring) because data from server requests certificate - and this functionality doesn't exist in DownloadOperations.
From .Net 4.5 onwards: Use IProgress<T>
Since .Net 4.5 you can handle asynchronous progress reporting with the IProgress<T> interface. You can write an extension method for downloading files using the HttpClient that can be called like this where progress is the implementation of IProgress<float> for your progress bar or other UI stuff:
// Seting up the http client used to download the data
using (var client = new HttpClient()) {
client.Timeout = TimeSpan.FromMinutes(5);
// Create a file stream to store the downloaded data.
// This really can be any type of writeable stream.
using (var file = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.None)) {
// Use the custom extension method below to download the data.
// The passed progress-instance will receive the download status updates.
await client.DownloadAsync(DownloadUrl, file, progress, cancellationToken);
}
}
Implementation
The code for this extension method looks like this. Note that this extension depends on another extension for handling asynchronous stream copying with progress reporting.
public static class HttpClientExtensions
{
public static async Task DownloadAsync(this HttpClient client, string requestUri, Stream destination, IProgress<float> progress = null, CancellationToken cancellationToken = default) {
// Get the http headers first to examine the content length
using (var response = await client.GetAsync(requestUri, HttpCompletionOption.ResponseHeadersRead)) {
var contentLength = response.Content.Headers.ContentLength;
using (var download = await response.Content.ReadAsStreamAsync()) {
// Ignore progress reporting when no progress reporter was
// passed or when the content length is unknown
if (progress == null || !contentLength.HasValue) {
await download.CopyToAsync(destination);
return;
}
// Convert absolute progress (bytes downloaded) into relative progress (0% - 100%)
var relativeProgress = new Progress<long>(totalBytes => progress.Report((float)totalBytes / contentLength.Value));
// Use extension method to report progress while downloading
await download.CopyToAsync(destination, 81920, relativeProgress, cancellationToken);
progress.Report(1);
}
}
}
}
With stream extension for the real progress reporting:
public static class StreamExtensions
{
public static async Task CopyToAsync(this Stream source, Stream destination, int bufferSize, IProgress<long> progress = null, CancellationToken cancellationToken = default) {
if (source == null)
throw new ArgumentNullException(nameof(source));
if (!source.CanRead)
throw new ArgumentException("Has to be readable", nameof(source));
if (destination == null)
throw new ArgumentNullException(nameof(destination));
if (!destination.CanWrite)
throw new ArgumentException("Has to be writable", nameof(destination));
if (bufferSize < 0)
throw new ArgumentOutOfRangeException(nameof(bufferSize));
var buffer = new byte[bufferSize];
long totalBytesRead = 0;
int bytesRead;
while ((bytesRead = await source.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false)) != 0) {
await destination.WriteAsync(buffer, 0, bytesRead, cancellationToken).ConfigureAwait(false);
totalBytesRead += bytesRead;
progress?.Report(totalBytesRead);
}
}
}
Here's a self-contained class that'll do the download, and report back the progress percentage, based on code from TheBlueSky on this SO answer, and eriksendc on this GitHub comment.
public class HttpClientDownloadWithProgress : IDisposable
{
private readonly string _downloadUrl;
private readonly string _destinationFilePath;
private HttpClient _httpClient;
public delegate void ProgressChangedHandler(long? totalFileSize, long totalBytesDownloaded, double? progressPercentage);
public event ProgressChangedHandler ProgressChanged;
public HttpClientDownloadWithProgress(string downloadUrl, string destinationFilePath)
{
_downloadUrl = downloadUrl;
_destinationFilePath = destinationFilePath;
}
public async Task StartDownload()
{
_httpClient = new HttpClient { Timeout = TimeSpan.FromDays(1) };
using (var response = await _httpClient.GetAsync(_downloadUrl, HttpCompletionOption.ResponseHeadersRead))
await DownloadFileFromHttpResponseMessage(response);
}
private async Task DownloadFileFromHttpResponseMessage(HttpResponseMessage response)
{
response.EnsureSuccessStatusCode();
var totalBytes = response.Content.Headers.ContentLength;
using (var contentStream = await response.Content.ReadAsStreamAsync())
await ProcessContentStream(totalBytes, contentStream);
}
private async Task ProcessContentStream(long? totalDownloadSize, Stream contentStream)
{
var totalBytesRead = 0L;
var readCount = 0L;
var buffer = new byte[8192];
var isMoreToRead = true;
using (var fileStream = new FileStream(_destinationFilePath, FileMode.Create, FileAccess.Write, FileShare.None, 8192, true))
{
do
{
var bytesRead = await contentStream.ReadAsync(buffer, 0, buffer.Length);
if (bytesRead == 0)
{
isMoreToRead = false;
TriggerProgressChanged(totalDownloadSize, totalBytesRead);
continue;
}
await fileStream.WriteAsync(buffer, 0, bytesRead);
totalBytesRead += bytesRead;
readCount += 1;
if (readCount % 100 == 0)
TriggerProgressChanged(totalDownloadSize, totalBytesRead);
}
while (isMoreToRead);
}
}
private void TriggerProgressChanged(long? totalDownloadSize, long totalBytesRead)
{
if (ProgressChanged == null)
return;
double? progressPercentage = null;
if (totalDownloadSize.HasValue)
progressPercentage = Math.Round((double)totalBytesRead / totalDownloadSize.Value * 100, 2);
ProgressChanged(totalDownloadSize, totalBytesRead, progressPercentage);
}
public void Dispose()
{
_httpClient?.Dispose();
}
}
Usage:
var downloadFileUrl = "http://example.com/file.zip";
var destinationFilePath = Path.GetFullPath("file.zip");
using (var client = new HttpClientDownloadWithProgress(downloadFileUrl, destinationFilePath))
{
client.ProgressChanged += (totalFileSize, totalBytesDownloaded, progressPercentage) => {
Console.WriteLine($"{progressPercentage}% ({totalBytesDownloaded}/{totalFileSize})");
};
await client.StartDownload();
}
Result:
7.81% (26722304/342028776)
8.05% (27535016/342028776)
8.28% (28307984/342028776)
8.5% (29086548/342028776)
8.74% (29898692/342028776)
8.98% (30704184/342028776)
9.22% (31522816/342028776)
The best way to go is using Windows.Web.Http.HttpClient instead of System.Net.Http.HttpClient. The first one supports progress.
But if for some reason you want to stick to the System.Net one, you will need to implement your own progress.
Remove the DataWriter, remove the InMemoryRandomAccessStream and add HttpCompletionOption.ResponseHeadersRead to GetAsync call so it returns as soon as headers are received, not when the whole response is received. I.e.:
// Your original code.
HttpClientHandler aHandler = new HttpClientHandler();
aHandler.ClientCertificateOptions = ClientCertificateOption.Automatic;
HttpClient aClient = new HttpClient(aHandler);
aClient.DefaultRequestHeaders.ExpectContinue = false;
HttpResponseMessage response = await aClient.GetAsync(
url,
HttpCompletionOption.ResponseHeadersRead); // Important! ResponseHeadersRead.
// To save downloaded image to local storage
var imageFile = await ApplicationData.Current.LocalFolder.CreateFileAsync(
filename,
CreationCollisionOption.ReplaceExisting);
var fs = await imageFile.OpenAsync(FileAccessMode.ReadWrite);
// New code.
Stream stream = await response.Content.ReadAsStreamAsync();
IInputStream inputStream = stream.AsInputStream();
ulong totalBytesRead = 0;
while (true)
{
// Read from the web.
IBuffer buffer = new Windows.Storage.Streams.Buffer(1024);
buffer = await inputStream.ReadAsync(
buffer,
buffer.Capacity,
InputStreamOptions.None);
if (buffer.Length == 0)
{
// There is nothing else to read.
break;
}
// Report progress.
totalBytesRead += buffer.Length;
System.Diagnostics.Debug.WriteLine("Bytes read: {0}", totalBytesRead);
// Write to file.
await fs.WriteAsync(buffer);
}
inputStream.Dispose();
fs.Dispose();
The simplest way to implement progress tracking for both uploading and downloading is to use ProgressMessageHandler from the Microsoft.AspNet.WebApi.Client nuget package.
Note: this library was originally named System.Net.Http.Formatting, and was renamed to Microsoft.AspNet.WebApi.Client. However, this library is not related to ASP.Net and can be used by any project looking for official Microsoft extensions to HttpClient. The source code is available here.
Example:
var handler = new HttpClientHandler() { AllowAutoRedirect = true };
var ph = new ProgressMessageHandler(handler);
ph.HttpSendProgress += (_, args) =>
{
Console.WriteLine($"upload progress: {(double)args.BytesTransferred / args.TotalBytes}");
};
ph.HttpReceiveProgress += (_, args) =>
{
Console.WriteLine($"download progress: {(double)args.BytesTransferred / args.TotalBytes}");
};
var client = new HttpClient(ph);
await client.SendAsync(...);
Note that this will not report progress if uploading a byte array. The request message content must be a stream.
The following code shows a minimal example of what must be done against the HttpClient api to get download progress.
HttpClient client = //...
// Must use ResponseHeadersRead to avoid buffering of the content
using (var response = await client.GetAsync(uri, HttpCompletionOption.ResponseHeadersRead)){
// You must use as stream to have control over buffering and number of bytes read/received
using (var stream = await response.Content.ReadAsStreamAsync())
{
// Read/process bytes from stream as appropriate
// Calculated by you based on how many bytes you have read. Likely incremented within a loop.
long bytesRecieved = //...
long? totalBytes = response.Content.Headers.ContentLength;
double? percentComplete = (double)bytesRecieved / totalBytes;
// Do what you want with `percentComplete`
}
}
The above does not tell you how to process the stream, how to report the process, or try to provide a direct solution to the code in the original question. However, this answer may be more accessible to future readers who wish to apply having progress to in their code.
same as #René Sackers solution above, but added the ability to cancel the download
class HttpClientDownloadWithProgress : IDisposable
{
private readonly string _downloadUrl;
private readonly string _destinationFilePath;
private readonly CancellationToken? _cancellationToken;
private HttpClient _httpClient;
public delegate void ProgressChangedHandler(long? totalFileSize, long totalBytesDownloaded, double? progressPercentage);
public event ProgressChangedHandler ProgressChanged;
public HttpClientDownloadWithProgress(string downloadUrl, string destinationFilePath, CancellationToken? cancellationToken = null)
{
_downloadUrl = downloadUrl;
_destinationFilePath = destinationFilePath;
_cancellationToken = cancellationToken;
}
public async Task StartDownload()
{
_httpClient = new HttpClient { Timeout = TimeSpan.FromDays(1) };
using (var response = await _httpClient.GetAsync(_downloadUrl, HttpCompletionOption.ResponseHeadersRead))
await DownloadFileFromHttpResponseMessage(response);
}
private async Task DownloadFileFromHttpResponseMessage(HttpResponseMessage response)
{
response.EnsureSuccessStatusCode();
var totalBytes = response.Content.Headers.ContentLength;
using (var contentStream = await response.Content.ReadAsStreamAsync())
await ProcessContentStream(totalBytes, contentStream);
}
private async Task ProcessContentStream(long? totalDownloadSize, Stream contentStream)
{
var totalBytesRead = 0L;
var readCount = 0L;
var buffer = new byte[8192];
var isMoreToRead = true;
using (var fileStream = new FileStream(_destinationFilePath, FileMode.Create, FileAccess.Write, FileShare.None, 8192, true))
{
do
{
int bytesRead;
if (_cancellationToken.HasValue)
{
bytesRead = await contentStream.ReadAsync(buffer, 0, buffer.Length, _cancellationToken.Value);
}
else
{
bytesRead = await contentStream.ReadAsync(buffer, 0, buffer.Length);
}
if (bytesRead == 0)
{
isMoreToRead = false;
continue;
}
await fileStream.WriteAsync(buffer, 0, bytesRead);
totalBytesRead += bytesRead;
readCount += 1;
if (readCount % 10 == 0)
TriggerProgressChanged(totalDownloadSize, totalBytesRead);
}
while (isMoreToRead);
}
//the last progress trigger should occur after the file handle has been released or you may get file locked error
TriggerProgressChanged(totalDownloadSize, totalBytesRead);
}
private void TriggerProgressChanged(long? totalDownloadSize, long totalBytesRead)
{
if (ProgressChanged == null)
return;
double? progressPercentage = null;
if (totalDownloadSize.HasValue)
progressPercentage = Math.Round((double)totalBytesRead / totalDownloadSize.Value * 100, 2);
ProgressChanged(totalDownloadSize, totalBytesRead, progressPercentage);
}
public void Dispose()
{
_httpClient?.Dispose();
}
}
René Sackers version is excellent but it could be better. Specifically, it has a subtle race condition caused by TriggerProgressChanged firing before the stream closes. The fix is to fire the event after the stream is explicitly disposed. The version below includes the above change, inherits from HttpClient and adds support for cancellation tokens.
public delegate void ProgressChangedHandler(long? totalFileSize, long totalBytesDownloaded, double? progressPercentage);
public class HttpClientWithProgress : HttpClient
{
private readonly string _DownloadUrl;
private readonly string _DestinationFilePath;
public event ProgressChangedHandler ProgressChanged;
public HttpClientWithProgress(string downloadUrl, string destinationFilePath)
{
_DownloadUrl = downloadUrl;
_DestinationFilePath = destinationFilePath;
}
public async Task StartDownload()
{
using (var response = await GetAsync(_DownloadUrl, HttpCompletionOption.ResponseHeadersRead))
await DownloadFileFromHttpResponseMessage(response);
}
public async Task StartDownload(CancellationToken cancellationToken)
{
using (var response = await GetAsync(_DownloadUrl, HttpCompletionOption.ResponseHeadersRead, cancellationToken))
await DownloadFileFromHttpResponseMessage(response);
}
private async Task DownloadFileFromHttpResponseMessage(HttpResponseMessage response)
{
response.EnsureSuccessStatusCode();
long? totalBytes = response.Content.Headers.ContentLength;
using (var contentStream = await response.Content.ReadAsStreamAsync())
await ProcessContentStream(totalBytes, contentStream);
}
private async Task ProcessContentStream(long? totalDownloadSize, Stream contentStream)
{
long totalBytesRead = 0L;
long readCount = 0L;
byte[] buffer = new byte[8192];
bool isMoreToRead = true;
using (FileStream fileStream = new FileStream(_DestinationFilePath, FileMode.Create, FileAccess.Write, FileShare.None, 8192, true))
{
do
{
int bytesRead = await contentStream.ReadAsync(buffer, 0, buffer.Length);
if (bytesRead == 0)
{
isMoreToRead = false;
continue;
}
await fileStream.WriteAsync(buffer, 0, bytesRead);
totalBytesRead += bytesRead;
readCount += 1;
if (readCount % 10 == 0)
TriggerProgressChanged(totalDownloadSize, totalBytesRead);
}
while (isMoreToRead);
}
TriggerProgressChanged(totalDownloadSize, totalBytesRead);
}
private void TriggerProgressChanged(long? totalDownloadSize, long totalBytesRead)
{
if (ProgressChanged == null)
return;
double? progressPercentage = null;
if (totalDownloadSize.HasValue)
progressPercentage = Math.Round((double)totalBytesRead / totalDownloadSize.Value * 100, 2);
ProgressChanged(totalDownloadSize, totalBytesRead, progressPercentage);
}
}
This is my variation on the answer of René Sackers. Main differences:
A more functional style.
Only one method instead of a whole object.
Can cancel the download
public async static Task Download(
string downloadUrl,
string destinationFilePath,
Func<long?, long, double?, bool> progressChanged)
{
using var httpClient = new HttpClient { Timeout = TimeSpan.FromDays(1) };
using var response = await httpClient.GetAsync(downloadUrl, HttpCompletionOption.ResponseHeadersRead);
response.EnsureSuccessStatusCode();
var totalBytes = response.Content.Headers.ContentLength;
using var contentStream = await response.Content.ReadAsStreamAsync();
var totalBytesRead = 0L;
var readCount = 0L;
var buffer = new byte[8192];
var isMoreToRead = true;
static double? calculatePercentage(long? totalDownloadSize, long totalBytesRead) => totalDownloadSize.HasValue ? Math.Round((double)totalBytesRead / totalDownloadSize.Value * 100, 2) : null;
using var fileStream = new FileStream(destinationFilePath, FileMode.Create, FileAccess.Write, FileShare.None, 8192, true);
do
{
var bytesRead = await contentStream.ReadAsync(buffer);
if (bytesRead == 0)
{
isMoreToRead = false;
if (progressChanged(totalBytes, totalBytesRead, calculatePercentage(totalBytes, totalBytesRead)))
{
throw new OperationCanceledException();
}
continue;
}
await fileStream.WriteAsync(buffer.AsMemory(0, bytesRead));
totalBytesRead += bytesRead;
readCount++;
if (readCount % 100 == 0)
{
if (progressChanged(totalBytes, totalBytesRead, calculatePercentage(totalBytes, totalBytesRead)))
{
throw new OperationCanceledException();
}
}
}
while (isMoreToRead);
}
It can be called this way:
// Change this variable to stop the download
// You can use a global variable or some kind of state management
var mustStop = false;
var downloadProgress = (long? _, long __, double? progressPercentage) =>
{
if (progressPercentage.HasValue)
progressBar.Value = progressPercentage.Value;
// In this example only the variable is checked
// You could write other code that evaluates other conditions
return mustStop;
};
SomeClass.Download("https://example.com/bigfile.zip", "c:\downloads\file.zip", downloadProgress);
Hm, you could have another thread check the current size of the stream being written (you'd also pass the expected file size to it) and then update the progress bar accordingly.
This is a modified version of René Sackers answer with the following functional changes:
http client not disposed (because it should not be disposed)
better progress handling
callback to create httpRequest (custom header support)
utilizes ArrayPool to reduce memory footprint
automatic event subscribe+unsubscribe to prevent memory leaks by event handlers
You can also use this nuget package https://www.nuget.org/packages/Amusoft.Toolkit.Http to gain all benefits. Since it supports net462 and above that is probably the easiest way.
Usage:
await DownloadWithProgress.ExecuteAsync(HttpClients.General, assetUrl, downloadFilePath, progressHandler, () =>
{
var requestMessage = new HttpRequestMessage(HttpMethod.Get, assetUrl);
requestMessage.Headers.Accept.TryParseAdd("application/octet-stream");
return requestMessage;
});
I guess i am not the only one who needs custom headers so i figured i would share this rewrite
Implementation:
public delegate void DownloadProgressHandler(long? totalFileSize, long totalBytesDownloaded, double? progressPercentage);
public static class DownloadWithProgress
{
public static async Task ExecuteAsync(HttpClient httpClient, string downloadPath, string destinationPath, DownloadProgressHandler progress, Func<HttpRequestMessage> requestMessageBuilder = null)
{
requestMessageBuilder ??= GetDefaultRequestBuilder(downloadPath);
var download = new HttpClientDownloadWithProgress(httpClient, destinationPath, requestMessageBuilder);
download.ProgressChanged += progress;
await download.StartDownload();
download.ProgressChanged -= progress;
}
private static Func<HttpRequestMessage> GetDefaultRequestBuilder(string downloadPath)
{
return () => new HttpRequestMessage(HttpMethod.Get, downloadPath);
}
}
internal class HttpClientDownloadWithProgress
{
private readonly HttpClient _httpClient;
private readonly string _destinationFilePath;
private readonly Func<HttpRequestMessage> _requestMessageBuilder;
private int _bufferSize = 8192;
public event DownloadProgressHandler ProgressChanged;
public HttpClientDownloadWithProgress(HttpClient httpClient, string destinationFilePath, Func<HttpRequestMessage> requestMessageBuilder)
{
_httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
_destinationFilePath = destinationFilePath ?? throw new ArgumentNullException(nameof(destinationFilePath));
_requestMessageBuilder = requestMessageBuilder ?? throw new ArgumentNullException(nameof(requestMessageBuilder));
}
public async Task StartDownload()
{
using var requestMessage = _requestMessageBuilder.Invoke();
using var response = await _httpClient.SendAsync(requestMessage, HttpCompletionOption.ResponseHeadersRead);
await DownloadAsync(response);
}
private async Task DownloadAsync(HttpResponseMessage response)
{
response.EnsureSuccessStatusCode();
var totalBytes = response.Content.Headers.ContentLength;
using (var contentStream = await response.Content.ReadAsStreamAsync())
await ProcessContentStream(totalBytes, contentStream);
}
private async Task ProcessContentStream(long? totalDownloadSize, Stream contentStream)
{
var totalBytesRead = 0L;
var readCount = 0L;
var buffer = ArrayPool<byte>.Shared.Rent(_bufferSize);
var isMoreToRead = true;
using (var fileStream = new FileStream(_destinationFilePath, FileMode.Create, FileAccess.Write, FileShare.None, _bufferSize, true))
{
do
{
var bytesRead = await contentStream.ReadAsync(buffer, 0, buffer.Length);
if (bytesRead == 0)
{
isMoreToRead = false;
ReportProgress(totalDownloadSize, totalBytesRead);
continue;
}
await fileStream.WriteAsync(buffer, 0, bytesRead);
totalBytesRead += bytesRead;
readCount += 1;
if (readCount % 100 == 0)
ReportProgress(totalDownloadSize, totalBytesRead);
}
while (isMoreToRead);
}
ArrayPool<byte>.Shared.Return(buffer);
}
private void ReportProgress(long? totalDownloadSize, long totalBytesRead)
{
double? progressPercentage = null;
if (totalDownloadSize.HasValue)
progressPercentage = Math.Round((double)totalBytesRead / totalDownloadSize.Value * 100, 2);
ProgressChanged?.Invoke(totalDownloadSize, totalBytesRead, progressPercentage);
}
}
Im not really sure how to measure how the completion logic, but for now this seems to do it.
public event ProgressChangedHandler ProgressChanged;
public event ProgressCompleteHandler DownloadComplete;
...
TriggerProgressChanged(totalDownloadSize, totalBytesRead);
TriggerDownloadComplete(totalBytesRead == totalDownloadSize);
private void TriggerDownloadComplete(bool status)
{
DownloadComplete(status);
}
client.DownloadComplete += (status) =>
{
if (status)
{
// success
}
};

Kill Asynchronous thread

I'm trying to read the HttpResponse.GetResponseStream() Asynchronously
Stream source=myHttpResponse.GetResponseStream();
IAsyncResult asyncResult = source.BeginRead(buffer, 0, new[] { maxDownloadSize, buffer.Length }.Min(), null, null);
But for some reason my application hangs at source.Endread()
int bytesRead = source.EndRead(innerAsyncResult);
I knew that all the Asynchronous calls rely on Threadpool, but is there any way i can kill the thread,
I did placed few controls to kill the asynchronous operation
I have placed an external timer, On Trigger im closing the Stream
System.Threading.Timer timer = new System.Threading.Timer(OnTimerCallback, source, Convert.ToInt32(new TimeSpan(0, 10, 0).TotalMilliseconds), 1);
private static void OnTimerCallback(Object state)
{
Stream stream = state as Stream;
if (stream == null)
return;
try
{
stream.Close();
stream.Dispose();
}
catch (Exception ex)
{
//Log Error
}
throw new Exception("Successfully closed the Stream and killing the Thread");
}
It is closing the stream, but for some reason the application is still hung.
I have placed an external timer, and closing the asyncResult.AsyncWaitHandle.Close()
Both the mechanisms seems to be vain, is there any way i can kill the thread.
Basically what i want to achieve is Read from NetworkStream and Save it in FileStream, and it works fine for a while and later it hung at source.EndRead(innerAsyncResult); Reading from Networkstream is asynchronous , where as saving the data to filestream is synchronous.
Actual Code:
public static void CopyToStreamAsync(this Stream source, Stream destination,
Action<Stream, Stream, Exception> completed, Action<uint> progress,
uint bufferSize)
{
byte[] buffer = new byte[bufferSize];
Action<Exception> done = exception =>
{ //Completed event handler
};
int maxDownloadSize = maximumDownloadSize.HasValue
? (int)maximumDownloadSize.Value
: int.MaxValue;
int bytesDownloaded = 0;
IAsyncResult asyncResult = source.BeginRead(buffer, 0, new[] { maxDownloadSize, buffer.Length }.Min(), null, null);
Action<IAsyncResult, bool> endRead = null;
endRead = (innerAsyncResult, innerIsTimedOut) =>
{
try
{
int bytesRead = source.EndRead(innerAsyncResult);
int bytesToWrite = new[] { maxDownloadSize - bytesDownloaded, buffer.Length, bytesRead }.Min();
destination.Write(buffer, 0, bytesToWrite);
bytesDownloaded += bytesToWrite;
if (!progress.IsNull() && bytesToWrite > 0)
{
//Progress Handlers
}
if (bytesToWrite == bytesRead && bytesToWrite > 0)
{
asyncResult = source.BeginRead(buffer, 0, new[] { maxDownloadSize, buffer.Length }.Min(), null, null);
asyncResult.FromAsync((ia, isTimeout) => endRead(ia, isTimeout), timeout);
}
else
{
done(null);
}
}
catch (Exception exc)
{
done(exc);
}
};
asyncResult.FromAsync((ia, isTimeout) => endRead(ia, isTimeout), timeout);
}

Categories